Linux C++通讯架构【五】:网络通讯实战

117 阅读9分钟

收发包

socket:可理解为四元组关联的一个数字,具有唯一性,socket在unix上是一个文件描述符

发包:

  1. 包头结构:
  • 报文总长度Len:包头+包体
  • 消息类型:这个包需要用服务器的哪个函数处理。(服务器端有个(函数名:消息码)向量)
  • crc32校验码:确保数据包的数据没有被修改过
    • ccrc32类
    • Get_crc():给你一段buff,也就是一段内存,以及内存长度,计算出一个crc32值
    • 客户端的包头就有这个crc32的值了 =》 服务器也用同样的算法计算crc32的值(根据包体计算),并和包头的crc32比较
    • 有点类似摘要算法
  1. 注意字节对齐问题
  • 如客户端发送的数据结构体长度是8字节,结果由于字节对齐问题,服务端收到16字节,包头包体读取混乱

  • 采用一字节对齐,不会进行填充。 #pragma pack(1)

  1. 大小端问题:发包时需要本地序转网络序,收包时网络序转本地序。

  2. 数据分片问题:MTU

  • 比如最大1.5k,我要发1M的数据,需拆分成66个包(分片)

  • 这66个分片可能走的网络路径都不一样,每个分片可能被再次分片(不过TCP本身会处理这个问题)

  1. send():
  • 只负责发送到发送缓存区
  • while循环发送:比如我想发1000个字节,发送缓存区满了,只发了100字节,那么就得while循环发送

收包:

  1. 先收包头
  • 如果包头没收完,只收了5字节,那我需要一个指针,指向后半部分包头的存储位置 buff[8],*precvbuf

  • 我们要求,客户端连接后,有义务主动给服务器发包,服务器接收包头+包体,并在前面加一个消息头 (如是否过期、连接池中的序号)

  • 内存攻击:消息头+包头+包体需要重新 new 内存,如果我想攻击你,我发个包头,将Len设为2000字节。这块内存需要在断开连接后,自己释放。

  1. 收包体(LT模式,比较简单):
  • 封装内存分配:会频繁分配小块内存,可考虑用内存池
  1. TCP粘包:
  • 客户端黏包:Nagle算法,可能会把三个小包合并成一个大包发送出去,就只用一次send()了。关闭这个算法客户端就没有粘包问题了。
  • 服务端粘包:服务器肯定会粘包
    • 因为服务端recvfrom后,处理是需要时间的,然后三个小包全都过来了,就在服务端粘在一起了
    • 解决办法:c/s都按照固定的格式,包头+包体的格式。其中包头固定长度,包头中一个变量记录总长
    • 约定包头包体结构
  1. 包处理:
  • 把消息头+包头+包体这块内存放到消息队列里(cahr*类型的双链表),这块内存的释放就由消息队列来控制

  • 为了不使服务器崩溃,约定最多1000个包。(Nginx有应用漏桶算法,最大并发连接)

  • 拆包处理ngx_request_handler()

TimeWait状态

  1. 先发起关闭连接请求的最后一次挥手会进入这个状态假,设服务器先关闭连接

  2. 最长数据包生命周期为2MSL(1到4分钟),无论客户端收没收到这个包,服务端都会关闭连接。TimeWait等待也是等待2MSL

  3. 引入这个状态的原因:

  • 如果第四次挥手,客户端的ack丢失了,服务端无法正常关闭,这时候服务器会重发第三次挥手的fin报文(一来一回就是2MSL)。客户端如果立即关闭连接,第四次挥手的ack一旦丢失,服务器就无法关闭连接了
  • 允许老的重复的数据包在网络中消逝:2MSL后,所有的报文生命周期都完结了。
  1. RST攻击:
  • 服务器都关闭连接了,客户端再发送FIN,服务器不是返回ACK,而是RST(连接复位),因为此时服务端判断和客户端的连接异常。此时,缓存区的数据一般都会丢失。
  • TCP连接正常的情况下,客户端发送SYN,服务器也会判断和客户端的连接异常。需要复位。
  1. Listen队列剖析:
  • 对于listen()进行监听,操作系统会为这个socket维持两个队列

    • 未完成连接队列:三次握手的第一发送syn包时,半连接
    • 已完成连接队列:三次握手,已连接
    • 服务端RTT(往返时延)其实是一个socket在半连接状态待的时间
    • 所以TCP连接比较慢,成本挺高=》短连接的缺点
    • 如果一个恶意客户端,迟迟不发送第三次的ack,那么这个socket就会一直占用在半连接队列中,所以要有超时,把这些socket清理掉
  1. bocklog参数:
  • 两个队列里面的最大连接总数,如果达到了这个参数,服务端直接忽视,客户端过一段时间再次发送,客户端连续三次都都被服务器忽视,就返回失败。

  • SYN攻击:

    • 客户端伪造ip和端口,只发送第一个syn,服务器回应ack,那由于ip和端口都是伪造的,客户端都不会收到,那就没有第三次syn,会使得半连接里的socket大于bocklog,再也没有连接能进来了。

    • 如果是多台服务器这样搞,就变成了分布式dos=》ddos

    • 所以操作系统明确进一步规定,bocklog为已完成连接队列中,最大条目数,,反正半连接队列的socket超过75s会gg,bocklog一般300左右。

阻塞与非阻塞IO

  1. 阻塞、非阻塞IO
  • 阻塞IO:调用系统函数时,这个函数是否会导致我们的进程进入sleep()休眠状态阻塞IO:
  • accept():从已完成连接队列的队首,取出来一个返回给进程。如果队列为空,accept休眠
  • accept() 也可以是不阻塞的,一直while循环呗;socket也可以是阻塞的,也可以是不阻塞的
  • recvfrom是个阻塞函数,无数据就卡住了,内核得有准备好得数据,从接收缓存区返回到用户空间buff。
  1. 非阻塞IO,充分利用时间片,执行效率高。比如recvfrom,即使没数据,就返回-1(错误标记),然后继续轮询,就是不让出时间片。
  • 但其实内核拷贝数据到用户态的时候,还是卡住的,此时处于阻塞状态。
  1. 同步、异步IO:不要和阻塞,非阻塞混肴

调用异步IO函数时,得给这个函数指定一个接收缓存区,我还要给定一个回调函数,然后执行完这个函数就返回。其余交给操作系统,操作系统判断是否有数据到来,有,就把数据拷贝到缓存区里,并用你指定得回调函数来通知你。

异步IO是完全没阻塞的,拷贝数据也是内核进行的,然后通过回调函数通知你拿回去,异步去实现非阻塞IO,快,开销小

epoll

多路复用:可简单理解为,一个进程(函数)可以判断多个socket是否需要处理。

  • select,poll连接1000多就下降了;epoll支持上百万连接,但其实连接有百万,但发送数据的只有几百个。

  • epoll事件驱动机制,在单独的进程或线程里运行,没有进程切换开销,非常适合高并发。

  • epoll三个函数(系统提供的):

    1. epoll_create(int size):创建一个epoll对象,返回这个对象的文件描述符(句柄,对象也是文件),句柄要用close()关闭。size>0

      • rbr(红黑树节点),rblist(双向链表节点)成员
    2. epoll_ctl:epoll对象监控socket,我们感兴趣的socket(笔记本)有数据时,系统会通知我们。

      • efpd:epoll句柄

      • op:增删查改指令,红黑树添加socket,socket就是红黑树的key,通过key指定在红黑树的位置。

    3. epoll_wait():

      • 阻塞小段时间,等待时间发生,返回事件集合。说白了就是拷贝双向链表的数据,拷贝完的就从双向链表移除。
      • events:能拷贝多少个事件(可理解为拷贝的内存)
      • timeout:等待拷贝的时间(如数据较少或没数据)

epoll封装:

  • 以前需设置最大连接数1024,现在不用设置了,但为了不使程序崩,还是设置了

  • 四个子进程同时监听2个端口

  • ngx_epoll_init():

    • 调用epoll_create()

    • 创建socket连接池,用于处理后续的连接。

      • 连接池:就是个结构数组,元素数量就是连接数1024,每个元素类型都是ngx_connetion_t,这个结构:把socket和内存绑定,读写更方便

      -那来了个连接,怎么快速找到一个空的连接池元素呢 =》空闲连接链表

    • 遍历socket监听端口

    • 这个函数要在子进程调用,和master没有关系,master不监听socket 端口

    • 连接池过滤过期事件:连接关闭后,把连接过期标志位fd=-1,后续该连接又有事件到来,通过fd=-1可以知道这个事件属于过期事件。怎么知道的?

  • ngx_process_events: 官方的是ngx_http_wait()

    • 调用epoll_wait()获取事件,nginx本身是事件驱动架构,那什么是事件驱动。

    • 三次握手连接进来,就是个可读事件

    • 就是接收到一个事件,选取合适的函数进行响应

    • 差不读就是epoll_wait()收集事件,epoll_accept和epoll_handler()处理事件

  • ngx_event_accpet()

    • accept() / accept4()

    • ngx_get_connection:绑定连接池元素

    • epoll_add_event()

LT和ET

Nginx采用ET模式

  • LT:事件进来后,不处理完会一直触发

比如说三次握手这个事件,如果你不用accpet4()处理,就会一直卡在这个循环里(获取事件,提示处理,获取同一个事件,提示处理),安全性高 =》所以监听端口须水平触发,或者设置监听套接字设置为非阻塞(setbloking)。

  • ET:

内核只通知一次,必须立马处理,不管是否处理,内核都会把他从双向链表拿走。效率高,编码难度大,必须一次性处理完改处理的数据。