对于服务端
重连逻辑一般很简单,A一旦发现与B断开连接,就立即尝试与B重新连接,如果连接不上,则隔一段时间重试,再此期间可以发送警报邮件或输出错误日志
对于客户端
一般会一个比前一个间隔时间更长的时间间隔去重连,此外还可以监听网络状态,如果网络状态发生波动,程序就应该检测网络状态,如果网络状态恢复正常,就应该立即进行一次重连
1、不需要重连的场景
(1)用户使用客户端主动放弃重连(2)因为一些业务上的要求,禁止客户端连接
2、技术上的断线重连和业务上的断线重连
技术上的断线重连为套接字的connect,业务上的重连指的还有如用户身份验证等业务流程包括在内
情景一:A与B端之间,有必经的路由器或交换机故障,无法正常通信,那么一段时间二者之间没有信息交换,由于TCP连接是状态机,这种情况两端都无法感知与对方的连接是否正常,称为“死链”,只要此时在任意一端向对端发送一个数据包,即可检测链路是否正常,这类数据包称为“心跳包”,这种操作称为“心跳检测”
情景二:客户端在连接服务器后,如果长时间没有数据往来,可能会被防火墙程序关闭连接。有时我们业务必须要求连接正常,这种需求称为“保活”
心跳检测的两个作用:保活和检测死链
操作系统的TCP/IP协议栈提供了keepalive选项用于socket的保活,发送心跳检测包的时间间隔默认为7200秒(两个小时)
int on=1;
(fd,SOL_SOCKET,SO_KEEPALIVE,&on,sizeof(on)); setsockopt
修改发送检测包的时间间隔
int val=7200;
(fd,IPPROTO_TCP,TCP_KEEPIDLE,&val,sizeof(val)); setsockopt
如果发送检测包,对方没回应,在进行重发的间隔时间
int interval=75;//seconds
(fd,IPPROTO_TCP,TCP_KEEPINTVL,&interval,sizeof(interval)); setsockopt
没回应最多重发间隔次数
int cnt=9;
(fd,IPPROTO_TCP,TCP_KEEPCNT,&cnt,sizeof(cnt)); setsockopt
TCP_KEEPIDLE设置发送keepalive报文的时间间隔,如果对端恢复ACK,则本端TCP认为连接依然存活,等TCP_KEEPIDLE秒后发送下一个keepalive包,如果对端回复RESET,则说明对端进程已重启,本段应用层与应该关闭此连接
如果对端没有任何回复,则进行重传,重传时间间隔为TCP_KEEPINTVL,若连续尝试TCP_KEEPCNT此仍不可达,则向程序返回ETIMEOUT(无任何应答)或EHOST错误信息
gaowanlu@DESKTOP-QDLGRDB:/$ sysctl -a | grep keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
在使用keepalive时,需要对每个连接中的socket进行设置,而不一定是必须的可能会产生毫无意义的贷款浪费,而且也不能和应用层很好地交互
应用层可以设计,在每个连接的收发数据的最新时间,当send和recv时进行更新,通知设置一个最大间隔时间,在Loop时进行检查如果超过了心跳间隔时间则发送一次心跳包
另一端的话同理,在send、recv时更新时间,设置一个超时时间,如果间隔多长时间还没有收发数据则关闭连接
有的系统架构中间有代理服务器,有时客户端与代理服务器已经断开了,而后端服务器与代理服务器还在连接,这种心跳检测,可以检测客户端的上行数据时间,让客户端发送检测包
后端服务器|
代理服务器|————————
————————| | |
client1 client2 client3
应用层设计心跳包可以设计为携带某些业务数据,在心跳包数据结构中加上需要的业务字段信息,然后在定时器中定时发送即可
心跳包的数据结构尽可能设计为小空间,为服务端与用户客户端节省流量带宽
在开发调试中可能不需要进行心跳包的调试,可以为其设置开关
在实际生产环境中,一般将程序收到的和发出去的数据报写到日志中,但是心跳包是特例,记录它毫无意义
对于生产环境下的服务器或产品,一般不允许开发人员进行调用调试器排查问题,可以通过打印日志,将当时的程序行为上下文现场记录下来,从日志系统中找到某次不正常的行为的上下文信息
最初的原型就是std::cout、printf等,但是对于工程项目一般将日志内容记录在文件系统中,也就是写到文件中
同步写日志
就是在输出日志的地方将日志即时写入文件中,如果是在客户端如主线程即UI线程,调用同步写日志,一般没问题,但是如果频繁写或者数据量大可能就会因写日志等待时间长导致程序卡顿。
同步写日志可能出现得问题,事件戳大得日志比时间戳小得日志靠前,这是产生日志的时间和实际写入磁盘的时间不是一个原子操作,但是对于同一个线程的不同时间的日志记录顺序肯定是正确的,就是不能保证不同线程之间的日志顺序。
在Linux中同一个进程对同一个FILE*的操作是线程安全的,比如多个线程分别向文件追加AAA、BBB、CCC,只可能出现AAABBBCCC这种情景而不会ABCABCABC。
异步写日志
异步写日志即在产生日志的地方不会将日志实时写入文件中,而是通过一些线程同步技术将日志暂存下来,然后通过一个或多个专门的日志写入线程将其写入磁盘中。本质上是生产者与消费者模式,一般没有特殊要求会将日志消费者线程数量设置为1,怎样将日志内容提交给日志线程,可以使用队列加互斥体
可以使用当队列无内容时,用条件变量等待方式先让日志线程挂起,然后等待通知。可以使用信号量机制进行优化。
日志的级别
级别从高到低一般有,FATAL(致命的)、ERROR(错误)、WARNING(警告)、INFO(信息)、TRACE(追踪)、VERBOSE(冗余的)。(1) FATAL一般用来记录无法让程序继续运行的错误。(2) ERROR一般用于记录程序中产生的错误。(3) WARNING用于记录程序产生的但不太影响使用的错误。(4) INFO记录程序运行过程中的各种状态信息。(5) TRACE、VERBOSE、INFO类似,一般在调试、测试时记录程序非常细粒度的代码执行情况。
每行日志都应该包含哪些信息
一般应该包含有,行写入的时间、打印日志所在的线程ID、打印日志所在文件的名称和行号
日志文件的命名规则
一般比较自由,通常为以日志文件创建时的时间戳,如20230526164800567.log,2023年5月26日16点48分00秒567毫秒,不过更推荐tubekit-20230526164800567.log的形式
日志文件的大小限制
当日志文件太大的时候,非常不利于筛查信息。可以在超过一定体积时创建并使用新的日志文件,这种大小限制一般称为rollSize
需要格外注意的是,ASCII码0,也就是’\0’或NULL,因为大多数API函数处理字符串一般到0截至
#include <iostream>
#include <cstring>
#include <string>
using namespace std;
int main(int argc, char **argv)
{
const wchar_t *str = L"hello";
<< std::strlen((char *)str) << endl; // 1
cout return 0;
}
这是因为,一个宽字符两个字节
0e\0l\0l\0o\0 h\
所以为了解决这类问题,应该使用安全的函数指定字符串的长度,或者在日志记录时逐字节进行处理地输出
std::string str;
//错误
.append((const char*)&m);
str//正确
.append((const char*)&m,sizeof(m)); str
是否开启日志打印到终端,可以采用运行时变量或者宏变量控制
可以设计日志系统将不同级别的日志信息写到不同的日志文件中,如xxx.error.log、xxx.info.log、xxx.run.log
在实际的公司项目中,可能有很多的程序模块,不同的组别开发的程序用自己的日志,那么就很难维护,日志文件散落在各处
1--- ---服务3
服务| |
|-日志服务-|
| |
2--- ---服务4 服务
日志文件的存储也可以采用像云原生、HDFS等解决方案
日志记录应该精炼、详细、反映当时出错的现场参数、生产的环境等信息。如一个注册失败的请求至少要描述当时注册的用户名、密码、用户状态(是否已经注册)、请求的注册地址等。日志报错不一定是程序BUG、业务的错误记录也很重要。
在商业系统研发中,在日志信息里不应该出现敏感的信息,如关键服务的IP地址、端口号、用户的用户名和密码、sessionKey等
随着项目上线运营,不断优化改进,应该将日志内容高级别的修复,低级别的减少,把日志的级别不断提高、原来需要输出到日志中的一些细粒度信息的日志代码也应该减少
如浏览网页的404,HTTP中的状态响应码
1、可以迅速定位是用户输入的问题还是服务器自身的问题
2、快速定位是那个步骤或服务出了问题
例如有一个邮件系统
服务名称 | 正值错误码范围 | 负值错误码范围 |
---|---|---|
邮件综合操作接口 | 100~199 | -100~-199 |
邮件同步服务 | 200~299 | -200~-299 |
邮件配置服务 | 300~399 | -300~-399 |
邮件基础服务 | 400~499 | -400~-499 |
1、正值错误码表示发来的请求不满足业务需求,如100:用户名不存在,101:密码无效等
2、负值错误码表示程序内部错误,-100:数据库操作错误,-101:网络错误,-102:内存分配失败,-103:无法连接数据同步服务等。对于负值错误码,一般只返回码不给用户返回详细原因以免被不法分子利用
也就是相当于Web系统的后台管理系统,技术人员和运维人员可以使用nc、telnet也可以设计HTTP形式等,向用户提供命令行功能,如查看整个监控端口支持那些命令、显示当前内存中在线的用户信息、输出指定用户的信息等,以及日志开关控制等。
夏令时、UTC时间、本地时间、本机时间和UNIX时间戳是与时间相关的不同概念,它们之间存在一些关系,但表示的是不同方面的时间信息:
夏令时是一种时间制度,通常在夏季将时钟调快一小时,以充分利用白天的光照,并减少用电。它是一种地区性的时间调整。 夏令时影响了本地时间,通常在夏令时生效期间,本地时间相对于标准时间(通常是UTC)会提前一小时。
UTC是国际标准的时间表示方式,基于原子钟的精确时间,不受时区、夏令时等因素的影响。它用于协调全球时间。 UTC时间是一个绝对的时间标准,与夏令时无关。
本地时间是指特定地区或城市的当地时间,通常受到时区和夏令时的影响。 本地时间可以是UTC时间的特定偏移量,也可以包括夏令时的调整。
本机时间是指计算机或设备上的系统时间,通常是一个系统级别的时间设置。 本机时间可以是UTC时间或本地时间,具体取决于操作系统和配置。它通常会考虑时区和夏令时规则。
UNIX时间戳是一种表示时间的方式,它是从1970年1月1日午夜(UTC时间)以来的秒数。它是一个相对时间值,不包括夏令时的影响。 UNIX时间戳是一个整数,用于在计算机系统中跟踪时间和日期。
本地时间可以基于UTC时间的偏移量和夏令时规则来计算。 本机时间可以是UTC时间或本地时间,具体取决于系统配置。 UTC时间是一种绝对的时间标准,不受夏令时和时区影响。 UNIX时间戳是一个相对时间值,通常基于UTC时间,不考虑夏令时。 在编程中,通常可以使用标准库函数来进行不同时间表示之间的转换和操作,以满足具体应用的需求。
#include <iostream>
#include <chrono>
#include <ctime>
int main() {
// 获取当前系统时间点(本机时间)
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
// 将当前时间点转换为time_t类型(UNIX时间戳)
std::time_t unix_time = std::chrono::system_clock::to_time_t(now);
std::cout << "UNIX时间戳: " << unix_time << std::endl;
// 将当前时间点转换为UTC时间
std::tm* utc_tm = std::gmtime(&unix_time);
std::cout << "UTC时间: " << std::asctime(utc_tm);
// 将当前时间点转换为本地时间
std::tm* local_tm = std::localtime(&unix_time);
std::cout << "本地时间: " << std::asctime(local_tm);
// 检查是否处于夏令时
int is_dst = local_tm->tm_isdst;
if (is_dst > 0) {
std::cout << "当前处于夏令时" << std::endl;
} else if (is_dst == 0) {
std::cout << "当前不处于夏令时" << std::endl;
} else {
std::cout << "夏令时信息不可用" << std::endl;
}
return 0;
}