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

网络通讯实战

  • socket:可理解为四元组关联的一个数字,具有唯一性,socket在unix上是一个文件描述符
  • 发包:
    • 包头结构:
      • 报文总长度Len:包头+包体
      • 消息类型:这个包需要用服务器的哪个函数处理。(服务器端有个(函数名:消息码)向量)
      • crc32校验码:确保数据包的数据没有被修改过
        • ccrc32类

        • Get_crc():给你一段buff,也就是一段内存,以及内存长度,计算出一个crc32值

        • 客户端的包头就有这个crc32的值了 =》 服务器也用同样的算法计算crc32的值(根据包体计算),并和包头的crc32比较

        • 有点类似摘要算法

    • 注意字节对齐问题

      • 如客户端发送的数据结构体长度是8字节,结果由于字节对齐问题,服务端收到16字节,包头包体读取混乱

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

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

    • 数据分片问题:MTU

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

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

    • send():

      • 只负责发送到 发送缓存区

      • while循环发送:比如我想发1000个字节,发送缓存区满了,只发了100字节,那么就得while循环发送

  • 收包:

    • 先收包头

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

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

      • 内存攻击:消息头+包头+包体需要重新 new 内存,如果我想攻击你,我发个包头,将Len设为2000字节。这块内存需要在断开连接后,自己释放。=》但不是有MTU吗?呃,难道MTU无心之中解决了这个问题。。。

    • 收包体(LT模式,比较简单):

      • 封装内存分配:会频繁分配小块内存,可考虑用内存池

    • TCP粘包:
      1. 客户端黏包:Nagle算法,可能会把三个小包合并成一个大包发送出去,就只用一次send()了。关闭这个算法客户端就没有粘包问题了。
      2. 服务端粘包:服务器肯定会粘包
        1. 因为服务端recvfrom后,处理是需要时间的,然后三个小包全都过来了,就在服务端粘在一起了
        2. 解决办法:c/s都按照固定的格式,包头+包体的格式。其中包头固定长度,包头中一个变量记录总长
        3. 包头包体结构
    • 包处理:

      • 把消息头+包头+包体这块内存放到消息队列里(cahr*类型的双链表),这块内存的释放就由消息队列来控制

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

      • 拆包处理ngx_request_handler()

  • TimeWait

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

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

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

        • 未完成连接队列:三次握手的第一发送syn包时,半连接
        • 已完成连接队列:三次握手,已连接
        • 服务端RTT(往返时延)其实是一个socket在半连接状态待的时间

        • 所以TCP连接比较慢,成本挺高=》短连接的缺点

        • 如果一个恶意客户端,迟迟不发送第三次的ack,那么这个socket就会一直占用在半连接队列中,所以要有超时,把这些socket清理掉

      • bocklog参数:

        • 两个队列里面的最大连接总数,如果达到了这个参数,服务端直接忽视,客户端过一段时间再次发送,客户端连续三次都都被服务器忽视,就返回失败。
        • SYN攻击:

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

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

          • 所以操作系统明确进一步规定,bocklog为已完成连接队列中,最大条目数,,反正半连接队列的socket超过75s会gg,bocklog一般300左右。
    • 阻塞、非阻塞IO
      • 阻塞IO:调用系统函数时,这个函数是否会导致我们的进程进入sleep()休眠状态阻塞IO:
        • accept():从已完成连接队列的队首,取出来一个返回给进程。如果队列为空,accept休眠
        • accept() 也可以是不阻塞的,一直while循环呗;socket也可以是阻塞的,也可以是不阻塞的
        • recvfrom是个阻塞函数,无数据就卡住了,内核得有准备好得数据,从接收缓存区返回到用户空间buff。

      • 非阻塞IO,充分利用时间片,执行效率高。比如recvfrom,即使没数据,我就返回-1(错误标记),然后继续轮询,,,就是不让出时间片。
        • 但其实内核拷贝数据到用户态的时候,还是卡住的,此时处于阻塞状态。

      • Linux C++通讯架构【五】:网络通讯实战_第2张图片

      • 同步、异步IO:不要和阻塞,非阻塞混肴

        • 调用异步IO函数时,得给这个函数指定一个接收缓存区,我还要给定一个回调函数,然后执行完这个函数就返回。

        • 其余交给操作系统,操作系统判断是否有数据到来,有,就把数据拷贝到缓存区里,并用你指定得回调函数来通知你。

        • 异步IO是完全没阻塞的,拷贝数据也是内核进行的,然后通过回调函数通知你拿回去。 

        • 异步去实现非阻塞IO,快,开销小

      • epoll:

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

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

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

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

          • epoll_create(int size):创建一个epoll对象,返回这个对象的文件描述符(句柄,对象也是文件),句柄要用close()关闭。size>0
            • rbr(红黑树节点),rblist(双向链表节点)成员

          • epoll_ctl:epoll对象监控socket,我们感兴趣的socket(笔记本)有数据时,系统会通知我们。

            • efpd:epoll句柄

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

            • 其他的参数看不懂

          • epoll_wait():

            • 阻塞小段时间,等待时间发生,返回事件集合。说白了就是拷贝双向链表的数据,拷贝完的就从双向链表移除。

            • events:能拷贝多少个事件(可理解为拷贝的内存)

            • timeout:等待拷贝的时间(如数据较少或没数据)

          • socket文件描述符(socketfd)的节点(epitem),不仅有红黑树的三个指针封装(父,左子,右子),还记录了双向链表指针封装(pre,next),使得这个节点既可以存红黑树中,也可以存双向链表,一举两得。另外这个节点还有绑定的事件,标志位bool是否有事件发生。

          • 什么时候内核往双向链表添加数据,其实有4中情况。。。待续

      • 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:

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

你可能感兴趣的:(网络编程,linux,c++,架构)