网络通信框架设计

网络通信框架设计

  1. 使用epoll监听socket获得连接
  2. accept和新socket也应该设为非阻塞模式,可使用循环反复读取,直到返回-1,错误码为EAGAIN
  3. 使用IO复用接收到异常事件,如 EPOLLERRFD_CLOSE 则关闭socket。如果 send/recv 返回0则也可以关闭本端连接
  4. 向客户端发数据时对于epoll的水平触发模式,不能一开始就注册检测写事件标志,这会导致频繁触发写事件通知,但不是每次写事件都有数据发送。在epoll LT模式下发送数据时直接发送,发送不出去或剩余的部分先缓存起来再为socket注册写事件,写事件触发后反复发送数据,之后移除写事件标志

one thread one loop

再Reactor模式中,io线程负责处理io,网络线程中使用一个循环反复处理io事件其他事件

void* thread_fun(void* arg) {
    epoll_or_select();
    handle_io_events();
    handle_other_things();
}

在io复用函数中超时时间设置为0会使线程空转浪费CPU时间;设置为大于0则使其他事件不能及时执行。解决方法:创建一个 wakeup fd 用于唤醒并处理其他事件。

唤醒机制实现:

  1. 使用管道fd
    int pipe(int pipefd[2]);
    int pipe2(int pipefd[2], int flags);
  2. eventfd
  3. socketpair
    要使用 AF_UNIX

io线程本身不能处理耗时任务,除了IO复用函数的等待外其他步骤都不能阻塞或耗时。如果业务有定时器逻辑、读写处理、或其他耗时任务则应当新开一个线程处理。

收发数据

接收数据:使用epoll监听,事件触发后调用recv读取。根据业务读取部分或一次性接收完所有数据,将收到的数据放入缓冲区然后解包。对于epoll的ET模式每次都要将fd上数据全部接收完,直到recv或read返回-1,errno等于 EWOULDBLOCK 或 EAGAIN (这两个宏的值一样)

发送数据

除epoll的ET模式,通常不会一开始就为clientfd注册监听函数,因为这可能导致写事件一直触发。直接使用send或write发送数据,如果返回 EWOULDBLOCK 或 EAGAIN (表示资源暂时不可用)则为clientfd注册写事件。发送完数据后立即移除写事件。

发送接收缓冲区

接收缓冲区的作用:

  1. 一般业务都有自己的协议格式,为了让网络层跟通用,网络层应该与业务层解耦
  2. TCP是流式协议,处理粘包

缓冲区应当使用连续空间,预留空间用于存储元数据可以没有,分别使用读写指针用于读取数据和写数据。当缓冲区为空时读写指针位置相同

写入一段数据后写指针移动

读数据后移动读指针

当剩余空间不够使用时将为读取数据移动到前端,如果空间任不够使用则扩容。可使用 std::string std::vector 等现成类实现。

数据无法发送成功

当数据一直无法发送成功时,服务端发送缓冲区积压的数据越来越多,若不做处理则会耗尽内存再被os杀死。

  1. 发送缓冲区设置上限(如2MB),如果操作则任务该连接出现问题,则情况缓冲区断开连接
  2. 一些数据在缓冲区积压无法发送。一般会设置一个定时器每隔一段时间检查一下各连接的缓冲区数据是否发送成功。如果超过一定时间还存在未发送的数据,也认为该连接出现问题。

保活机制和心跳包

两个常见的网络问题

  1. 网络链路中的路由器或交换机出现故障无法通信,称为“死链”
    在任意一端对对端发送一个数据包即可检测链路是否正常,称为“心跳包”和“心跳检测”
  2. 客户端在连接服务器后,若长时间没有和服务器有数据来往,可能被防火墙关闭连接。
    当服务器和客户端在一定时间内没有有效业务数据往来时,只要发送心跳包就可实现“保活”

TCP提供了keepalice用于socket保活。

int on = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on));
// 默认时间间隔未7200秒,即两小时

// 发送keepalive报文的时间间隔
// 如果对端回复ACK则认为连接依然存活,继续等待间隔时间后再发送keepalive报文,如果回复RESET,则说明连接已断开
int val = 7200;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val));

// 设置两次重试报文的时间间隔
// 发送keepalive报文后如果对端没有回复,则每隔一段时间进行重试
int interval = 75;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));

// 设置重试次数
int cnt = 9;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));

# 在Linux上查看这3个值
sysctl -a | grep keepalive

socket 的keepalive选项可能产生大量无意义的带宽浪费,且不能很好的与应用层交互。心跳包最好是预先规定好格式的应用层数据包,当通信双方有正常业务数据往来时不需要心跳包。最佳做法时记录最近一次收发数据包地时间,心跳计时器每次检测与当前时间的时间间隔(可设为15~45秒不等)。

当server与client间存在proxy时,server与proxy间是长连接,client与proxy间也是长连接。当客户端与proxy间断开连接时,server的下行数据却仍然通畅。因此,存在代理服务器时应该使心跳包的时间间隔按照客户端的上行数据进行判断。实际应用中心跳包还带有业务数据,使用定时器定时发送。

 

posted @ 2022-12-09 11:11  某某人8265  阅读(61)  评论(0编辑  收藏  举报