服务器开发中的常用模块

断线自动重连的应用场景和逻辑设计

1、不需要重连的场景

(1)用户使用客户端主动放弃重连(2)因为一些业务上的要求,禁止客户端连接

2、技术上的断线重连和业务上的断线重连

技术上的断线重连为套接字的connect,业务上的重连指的还有如用户身份验证等业务流程包括在内

保活机制与心跳包

情景一:A与B端之间,有必经的路由器或交换机故障,无法正常通信,那么一段时间二者之间没有信息交换,由于TCP连接是状态机,这种情况两端都无法感知与对方的连接是否正常,称为“死链”,只要此时在任意一端向对端发送一个数据包,即可检测链路是否正常,这类数据包称为“心跳包”,这种操作称为“心跳检测”

情景二:客户端在连接服务器后,如果长时间没有数据往来,可能会被防火墙程序关闭连接。有时我们业务必须要求连接正常,这种需求称为“保活”

心跳检测的两个作用:保活和检测死链

Tcp keepalive选项

操作系统的TCP/IP协议栈提供了keepalive选项用于socket的保活,发送心跳检测包的时间间隔默认为7200秒(两个小时)

int on=1;
setsockopt(fd,SOL_SOCKET,SO_KEEPALIVE,&on,sizeof(on));

修改发送检测包的时间间隔

int val=7200;
setsockopt(fd,IPPROTO_TCP,TCP_KEEPIDLE,&val,sizeof(val));

如果发送检测包,对方没回应,在进行重发的间隔时间

int interval=75;//seconds
setsockopt(fd,IPPROTO_TCP,TCP_KEEPINTVL,&interval,sizeof(interval));

没回应最多重发间隔次数

int cnt=9;
setsockopt(fd,IPPROTO_TCP,TCP_KEEPCNT,&cnt,sizeof(cnt));

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等,但是对于工程项目一般将日志内容记录在文件系统中,也就是写到文件中

在C/C++中输出网络数据包日志

需要格外注意的是,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";
    cout << std::strlen((char *)str) << endl; // 1
    return 0;
}

这是因为,一个宽字符两个字节

h\0e\0l\0l\0o\0

所以为了解决这类问题,应该使用安全的函数指定字符串的长度,或者在日志记录时逐字节进行处理地输出

std::string str;
//错误
str.append((const char*)&m);
//正确
str.append((const char*)&m,sizeof(m));

调试时的日志

是否开启日志打印到终端,可以采用运行时变量或者宏变量控制

根据类型将日志写入不同的文件中

可以设计日志系统将不同级别的日志信息写到不同的日志文件中,如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;
}