收发包
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字节。这块内存需要在断开连接后,自己释放。
- 收包体(LT模式,比较简单):
- 封装内存分配:会频繁分配小块内存,可考虑用内存池
- TCP粘包:
- 客户端黏包:Nagle算法,可能会把三个小包合并成一个大包发送出去,就只用一次send()了。关闭这个算法客户端就没有粘包问题了。
- 服务端粘包:服务器肯定会粘包
- 因为服务端recvfrom后,处理是需要时间的,然后三个小包全都过来了,就在服务端粘在一起了
- 解决办法:c/s都按照固定的格式,包头+包体的格式。其中包头固定长度,包头中一个变量记录总长
- 约定包头包体结构
- 包处理:
-
把消息头+包头+包体这块内存放到消息队列里(cahr*类型的双链表),这块内存的释放就由消息队列来控制
-
为了不使服务器崩溃,约定最多1000个包。(Nginx有应用漏桶算法,最大并发连接)
-
拆包处理ngx_request_handler()
TimeWait状态
-
先发起关闭连接请求的最后一次挥手会进入这个状态假,设服务器先关闭连接
-
最长数据包生命周期为2MSL(1到4分钟),无论客户端收没收到这个包,服务端都会关闭连接。TimeWait等待也是等待2MSL
-
引入这个状态的原因:
- 如果第四次挥手,客户端的ack丢失了,服务端无法正常关闭,这时候服务器会重发第三次挥手的fin报文(一来一回就是2MSL)。客户端如果立即关闭连接,第四次挥手的ack一旦丢失,服务器就无法关闭连接了
- 允许老的重复的数据包在网络中消逝:2MSL后,所有的报文生命周期都完结了。
- RST攻击:
- 服务器都关闭连接了,客户端再发送FIN,服务器不是返回ACK,而是RST(连接复位),因为此时服务端判断和客户端的连接异常。此时,缓存区的数据一般都会丢失。
- TCP连接正常的情况下,客户端发送SYN,服务器也会判断和客户端的连接异常。需要复位。
- 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
- 阻塞IO:调用系统函数时,这个函数是否会导致我们的进程进入sleep()休眠状态阻塞IO:
- accept():从已完成连接队列的队首,取出来一个返回给进程。如果队列为空,accept休眠
- accept() 也可以是不阻塞的,一直while循环呗;socket也可以是阻塞的,也可以是不阻塞的
- recvfrom是个阻塞函数,无数据就卡住了,内核得有准备好得数据,从接收缓存区返回到用户空间buff。
- 非阻塞IO,充分利用时间片,执行效率高。比如recvfrom,即使没数据,就返回-1(错误标记),然后继续轮询,就是不让出时间片。
- 但其实内核拷贝数据到用户态的时候,还是卡住的,此时处于阻塞状态。
- 同步、异步IO:不要和阻塞,非阻塞混肴
调用异步IO函数时,得给这个函数指定一个接收缓存区,我还要给定一个回调函数,然后执行完这个函数就返回。其余交给操作系统,操作系统判断是否有数据到来,有,就把数据拷贝到缓存区里,并用你指定得回调函数来通知你。
异步IO是完全没阻塞的,拷贝数据也是内核进行的,然后通过回调函数通知你拿回去,异步去实现非阻塞IO,快,开销小
epoll
多路复用:可简单理解为,一个进程(函数)可以判断多个socket是否需要处理。
-
select,poll连接1000多就下降了;epoll支持上百万连接,但其实连接有百万,但发送数据的只有几百个。
-
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:等待拷贝的时间(如数据较少或没数据)
-
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:
内核只通知一次,必须立马处理,不管是否处理,内核都会把他从双向链表拿走。效率高,编码难度大,必须一次性处理完改处理的数据。