1. POSIX API 网络相关
- socket函数: 用来获取一个listenfd的系统调用,相当于开启一个监听服务器;
- bind函数: 用来绑定listenfd和ip、端口等相关的协议地址;
- listen函数: 根据绑定的协议地址,用listenfd开启监听;
- accept函数: 接收客户端的连接请求,获取一个clientfd;
- connect函数: 这是客户端请求服务端建立连接的方法;
- send/recv函数: 当fd就绪之后,用来从socket缓冲区读写数据的方法;
2. 内核协议栈收数据包的流程
网络协议栈是抽象概念,内核协议栈是实现网络协议栈的具体表现
- 网卡收到数据包,通过DMA将数据包写到内存(ringbuffer);
- 网卡向cpu发起硬件中断,cpu根据中断调用中断处理函数;
- 中断函数会将硬件中断屏蔽掉(先执行完当前数据包接收后再开启),发起软件中断;
- 内核线程负责处理软中断函数,从ringbuffer中逐个取出数据到sk_buffer(此时数据包带有各层头部信息);
- 核心步骤: 将sk_buffer中的数据层层解析,去掉帧头帧尾、ip头、tcp头或udp头,然后根据五元组找到对应的socket
- 将sk_buffer解析出来的数据包复制 根据指针移动到socket缓冲区(此处不涉及数据拷贝),如果是epoll模型,则会触发socket对应的回调函数通知与其连接的fd的EPOLLIN就绪;

3. 连接建立 -三次握手
注意: 对于客户端调用connect函数起, 2 中的数据包收发过程就一直在使用(SYN、SYN-ACK、ACK包的收发),只不过不会有sk_buffer向socket缓冲区写数据的操作,因为建立连接的包本身不含有应用数据。
posix Api在三次握手中的函数: 首先客户端发起需要调用connect,服务端此时需要listen对应端口,等三次握手完成后才可调用accpet接收应用数据包
3.1 tcp(传输层控制协议)头结构

- Source port : 源端口;
- Destination port : 目标端口;
- seq Number: 请求序列号,表示在此次连接中第一个数据字节的序列号;后续也需要表示当前发送的数据字节序列号;
- ack Number : 确认序列号,仅当ACK标志位1的时候有效,表示期望下次收到的序列号 也就是seq Nmuber + 字节数 -1
- SYN:同步序号,用于建立连接;
- ACK: 确认号字段有效;
- window size: 窗口大小,用于流量控制,表示接收方当前可用接受缓冲区的大小,发送方可根据此字段控制发送速率
3.2 三次握手具体表现

前提是服务端已经调用了listen监听端口了,此时客户端调用connect发起连接请求
- 客户端发送SYN报文 : SYN = 1 , seq = x ;状态:closed -> syn_sent
- 服务端回复ACK报文 : SYN = 1 , seq = y ,ACK = 1 , ack = x +1 ; 状态 : listen -> syn_recv;将连接放入半连接队列
- 客户端回复ACK报文 : ACK = 1 ,ack = y + 1 ; 状态:syn_sent -> establish
服务端收到客户端的最后一次ack确认之后:状态变为establish,并且从半连接队列转移入全连接队列,等accept()调用之后再从全连接队列中取走!
3.3 为什么是三次,而不是两次或四次
① 本质上是以最小确认轮次问题,TCP需要双方各自确认:
- 第一次握手 : 让服务端确认 -- 能收到客户端 ;并且告知自己序列号;
- 第二次握手 : 让客户端确认 -- 能收到服务端,且服务端能收到客户端;告知自己序列化并确认对端序列号;
- 第三次握手 : 让服务端确认 -- 客户端能收到服务端;确认对端序列号;
这样的话双方就都确认了 ① 自己和对端都能发送/接收数据 ② 互相确认初始序列号!
② 为什么不是两次
- 如果ACK-SYN包发送出去后服务端就进入establish,那如果该数据包客户端并没有收到,服务端就会进入一直等待且永远收不到数据的情况。这样会导致客户端和服务端的状态不一致!
- 如果网络延迟,服务端收到一个旧的SYN并回复SYN-ACK,并建立连接,但是客户端发现这是超时放弃的就会发送RST,在服务端收到RST之前这个连接的建立会浪费资源。
③ 为什么不是四次
- 第三次握手已经足够确认双方的收发能力了,第四次握手完全没必要属于资源浪费。
3.4 如果第三次握手的ACK包丢失了怎么办?
- 超时重传:
- 当服务端发送SYN-ACK包后,会启动一个定时器,等待客户端的ACK;
- 若超时未收到ACK,则会重传ACK-SYN (通常重传 5 次左右,总时间约 1 分钟);
- 客户端进入
ESTABLISHED:- 在服务端重传之前发送数据包到服务器,此时也会建立连接;
- 收到超时重传的包,则继续完成ACK包的发送;
- 若一直没有收到超时重传的包,且也不发送数据,那么会收到服务器的RST包,重新建立连接
3.5 半连接和全连接队列
- 半连接队列(SYN Queue) : 存放已完成第一次握手(收到 SYN)、但未完成第三次握手(未收到 ACK)的连接,状态为 SYN_RCVD;作用: 半连接队列用于防止服务端过早分配资源,只有收到 ACK 后才移入全连接队列。
- 全连接队列(Accept Queue) : 存放已完成三次握手、但未被应用程序 accept() 取走的连接,状态为 ESTABLISHED;作用: 全连接队列用于缓冲已完成连接,等待应用层取用。若队列满,后续连接可能被丢弃或拒绝。
3.6 什么是syn泛洪,如何防范
**SYN 攻击:**攻击者伪造大量不同源 IP 的 SYN 报文发送给服务器,服务器回复 SYN-ACK 后将这些半连接放入半连接队列,但攻击者不回复 ACK,导致半连接队列迅速占满,正常用户的 SYN 被丢弃,服务不可用。
防范措施:
- 增大半连接队列
- 缩短半连接存活时间
- 启动syn cookie:syn收到后不分配资源,而是生成一个cookie作为序列号返回,客户端ack的时候鞋带,验证通过后建立连接
- 防火墙过滤可疑ip
4. 连接断开 -四次挥手
首先确定,四次挥手可以由客户端发起,也可以由服务端发起!
以客户端发起断开为例,客户端close调用
- 第一次挥手(发送FIN报文):
- 客户端: 调用
close(),发送 FIN 报文,序列号为 u(等于之前已传送数据的最后一个字节的序号加 1)。客户端状态由ESTABLISHED变为FIN_WAIT_1,表示主动关闭连接,不再发送数据。 - 服务端: 仍处于
ESTABLISHED,收到 FIN。
- 客户端: 调用
- 第二次挥手(发送ACK报文):
- 服务端: 收到 FIN 后,立即回复 ACK 报文,确认号为 u+1。服务端状态由
ESTABLISHED变为CLOSE_WAIT,表示被动关闭,此时应用程序还可以继续发送数据(半关闭状态)。 - 客户端: 收到 ACK 后,状态由
FIN_WAIT_1变为FIN_WAIT_2,等待服务端的 FIN。
- 服务端: 收到 FIN 后,立即回复 ACK 报文,确认号为 u+1。服务端状态由
- 第三次挥手(发送FIN报文):
- 服务端: 当应用程序也调用
close()时,服务端发送 FIN 报文,序列号为 v(可能不同于 u),确认号仍为 u+1(因为之前已确认)。服务端状态由CLOSE_WAIT变为LAST_ACK,等待客户端的最终 ACK。 - 客户端: 处于
FIN_WAIT_2,等待 FIN。
- 服务端: 当应用程序也调用
- 第四次挥手(发送ACK报文):
- 客户端: 收到服务端的 FIN 后,回复 ACK 报文,确认号为 v+1。客户端状态由
FIN_WAIT_2变为TIME_WAIT,进入等待状态(2MSL,即两倍最大报文段生存时间)。 - 服务端: 收到 ACK 后,状态由
LAST_ACK变为CLOSED,连接彻底关闭。 - 客户端: 在 TIME_WAIT 状态等待 2MSL 后,自动变为 CLOSED。
- 客户端: 收到服务端的 FIN 后,回复 ACK 报文,确认号为 v+1。客户端状态由
4.1 为什么不能合并为三次?
服务端有剩余数据未传递完毕: 服务端收到FIN时还有数据要发送,不能立即关闭;ack和syn中间可继续传输剩余数据
4.2 TIME_WAIT的作用(2MSL)?
确保最后的ACK被对方收到 ,如果丢失,服务端会重新发送FIN包;
让旧链接的报文在网络中消失 如果没有这个时间,新连接恰好用到了同样的四元组建立连接,并且序列化又恰巧出现在接受窗口,这样会扰乱新连接的数据。
1MSL问题: 因为1MSL只能保证一个方向报文消失,但是tcp是双向的。主动关闭方发送的最后一个 ACK 和被动力可能重传的 FIN 都需要时间消失,且报文可能来回传播,因此需要 2MSL 来覆盖两个方向的最大延迟。
5 滑动窗口 用来做流量控制

在接收端回复给客户端的tcp头部会有window这个字段,用来表示接收端还能最大接收的数据量,此时发送端会根据该字段调整三个指针的位置控制发送数据大小,实现流量控制。
6 超时重传
以三次握手的第二次为例:
服务端发送syn-ack包后,设置一个定时器时间设置为RTO =1秒(因为没有参考时间,linux默认),如果超过这个时间服务端没有收到客户端ack报文,则会重新发送syn-ack包,然后重置定时器但此时基于指数退避原则,每次超时重传后RTO =2 * RTO。
7. 拥塞控制
7.1 慢启动
初始的cwnd很小,可以为1,每次收到ack包后就翻倍,达到阈值或丢包为止。

7.2 拥塞避免

8. dpdk
8.1 什么是dpdk
由于在内核协议栈中 ① 数据包经过内核的DMA缓冲区拷贝到内核的sk_buf ,② sk_buff 通过指针移动到socket缓冲区后通过recv/read 拷贝到用户空间这两次拷贝,因此效率相对降低。DPDK可以拦截网卡数据,将数据包映射到在用户空间配置好的巨页中,省去了内核作为桥梁,但是需要自己实现数据包的解析:也就是用户态网络协议栈。