TCP协议
TCP协议
TCP流程
建立连接(三次握手)
- 双方初始阶段都是从 close 状态开始,
- 服务端从close 状态,监听某个端口,然后进行 listen 状态。
- 客户端主动发起连接,发送 SYN,变成 SYN-SENT 状态。
- 服务端收到,将连接插入到半连接队列,返回 SYN 和 ACK(客户端得 SYN),自己变成 SYN-REVD
- 客户端发送 ACK 给 服务端, 变更为 ESTABLISHED 状态。
- 服务端收到,将连接从半连接队列取出,移入全连接队列,变更为 ESTABLISHED 状态。
- 进程调用 accept 函数,从全连接队列中取出已完成得连接建立得 socket 连接。
断开连接(四次挥手)
- 客户端向服务器发送 FIN 报文,从 ESTABLEISHED 状态 切换至 FIN-WAIT-1,此时客户端变成了 half-close(半关闭) 状态,无法发送报文,只能接受;
- 服务收到客户端确认,发送ACK,变成 CLOSED-WAIT 状态。
- 客户端收到ACK,变成 FIN-WAIT2 状态。
- 服务端再向 客户端发送 FIN, 进入 LAST-ACK 状态。
- 客户端收到 FIN 后,进入 TIME—WAIT 状态,发送ACK 给服务端。
- 服务端收到关闭,客户端进入等待, 最长 2MSL 后或者没有服务端重发请求后,客户端关闭,否则要重新 发送 ACK
画图 ———————————————–
建立连接的问题
为什么不是两次
根本原因:无法确认客户端得接受能力
如果存在客户端发送 SYN 报文滞留在网络中,进行重传,可能会建立两次连接,
这时客户端连接已经关闭,服务端却发送资源,造成资源浪费。
SYN 包丢失原因
- 开启 tcp_tw_recycle 参数,并且处于 NAT 环境下 ;
- Accpet 队列满了
- SYN 队列满了(SYN 队列满了,应该时 服务器返回给客户端信息,客户端没有应答,即SYN 攻击现象 )
SYN 包丢失原因之一 Accept 队列满了
Linux 内核会维护两个队列:
- 半连接队列,也称 SYN 队列
- 全连接队列,也称 Accept 队列
全连接队列太小,或者已满,会造成后续连接被废弃。
如果出现 Recv-Q 超过 Send-Q,就说明发生了 Accpet 队列满得情况。
解决办法:
- 调大 Accept 队列的最大长度,调大的方式增大 backlog 或 somaxconn 的值;
- 检查系统或者代码为什么调用 accpet() 不及时
检查办法,tcp_abort_on_overflow 的值
为 0: 表示当全连接队列满了,server 会扔掉 client 发过来的 ack
为1:表示当全连接队列满了,server 发送 reset 包给 client,表示废弃这个握手过程和这个连接
设置为1 ,客户端异常中可以看到很多的 connection reset by peer 的错误, 无论 0, 1,SYN 报文都不会被正常应答。
只有当 全连接队列有空位时,再次收到的请求由于包含 ACK,仍然会触发服务器成功建立连接。
设置为0 提高连接简历的成功率,长期溢出时,才需要设置1通知客户端
SYN Flood 攻击原理
原理:客户端再短时间伪造大量不存在的 IP 地址,并向服务端发送 SYN。
如何应对?
- 增加SYN 连接(适当增加半连接队列大小 tcp_max_syn_backlog,但需要同时增加全连接队列大小)
- 减少 SYN + ACK 重试次数,避免大量的超时重试(针对 大量 SYN_RECV 状态的 TCP 连接,设置 tcp_synack_retries ,降低重试次数,加快连接断开)
- 利用 SYN cookie 技术,收到 SYN 不立即分配,计算出 cookie,携带返回客户端,再次收到时,验证合法性分配资源。(设置 tcp_syncookies = 1 ,0 关闭,1 仅当 SYN 队列无法容纳时,启用它, 2 无条件开启功能)
backlog 可以再应用程序中配置,python 再 listen 方法中可以设置,nginx 可以在配置文件中设置。
断开连接的问题
TIME_WAIT作用
- 防止旧的连接数据包
- 保证连接正确关闭
防止旧的连接数据包
1MSL 表示报文得最大生存时间
经过2MSL 即可让两个方向上的数据包在网络中都自然消失。
TIME_WAIT 状态的连接过多会造成内存资源和本地端口资源的占用。
linux 提供两个系统参数快速回收处于 TIME_WAIT 状态的连接
- net.ipv4.tcp_tw_reuse(适用发起方),开始后,客户端调用 connect() 函数时,内核会随机找一个time_wait 状态超过 1s 的连接给新的连接复用
- net.ipv4.tcp_tw_recycle, 开始后允许处于 time_wait 状态的 连接被快速回收
以上条件生效,前提需要打开 tcp 时间戳,设置 tcp_timestamps = 1
如果开启 tcp_tw_recycle 和 tcp_timestamps ,会开启 https://v.flomoapp.com/mine/?memo_id=NDU0NjgzOTg per-host 的PAWS 机制, 该机制会导致报文丢失。
为什么是四次挥手而不是三次
服务器收到 FIN ,往往不会立即返回,因为这个时候不是所有报文都已经发送完成,所以需要等全部完成,才发送 FIN,先发送 ACK 表示已经收到,再发 FIN,就造成四次挥手。
改为三次挥手?
会导致客户端因为服务端长时间未发送,可能没有收到 FIN 包,不断重试。
等待2MSL 意义
- 1MSL 确保四次挥手中主动关闭方最后的ACK 报文最终能够到达对端
- 1MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达
TCP 中的 机制
拥塞控制
处理的问题:网络环境差,发生丢包时,需要发送端注意。
维护两个核心状态:
- 拥塞窗口(Congestion Window, cwnd)
- 慢启动阈值 (Slow Start Threshold, ssthresh)
涉及算法:
- 慢启动
- 拥塞避免
- 快速重传和快速恢复
拥塞窗口
拥塞窗口是发送端的限制。
根据接收窗口和拥塞窗口的大小,取最小值,确定 发送窗口。
慢启动
- 三次握手,确定双方接收窗口大小
- 确定拥塞窗口大小
- 开始传输一段时间后,发送端每收到一个 ack,拥塞窗口 + 1 (即每经过一个 RTT, cwnd 翻倍)
慢启动阈值就是控制 cwnd 到达阈值后,不需要再无限翻倍的扩大。
拥塞避免
到达阈值后, cwnd = 1/ cwnd + cwnd
快速重传
当发生丢包时,发送端收到重复ack,里面进行重传。
选择性重传 (SACK, Selective Acknowledgment)
接收端在回复ack 报文时,增加 SACK 属性,通过 left edge和 right edge 通知发送端,收到哪些数据,然后进行选择重传。
快速恢复
此阶段会发生的改变:
- 拥塞阈值降低为 cwnd 一半
- cwnd 的大小变为 拥塞阈值
- cwnd 线性增加
TCP的流量控制 (滑动窗口)
流量控制,主要体现的地方就是,发送数据存放的发送缓存区,接收数据存放的接收缓存区。
发送窗口
四个部分
- 已发送已确认
- 已发送未确认
- 未发送可发送
- 未发送不可发送
发送窗口就是图中被框住的范围。SND 即send, WND 即window, UNA 即unacknowledged, 表示未被确认,NXT 即next, 表示下一个发送的位置。
接收窗口
REV 机 receive, NXT 表示下一个接受的位置, WND 表示接收窗口大小。
流量控制过程
- 初始化,双方都有 200 字节。
- 发送端发送 100 字节,所以nxt需要向右移动 100 字节,可用窗口减少 100 字节。
- 接收端处理能力不够,处理40字节,所以发送端要缩小,缓冲队列中 60 字节没有接收。接收端在 ack 报文中带上滑动窗口 140 字节,发送端调整。
- 发送端 una 调整向右 40 字节。
PAWS 机制
原理:在客户端与服务端发送 seq 时,加入 时间戳,如果时间戳过期,那么默认会丢弃数据包。
作用:防止TCP 包中的序列化发送绕回。
per-host 的 PAWS 机制
定义:针对每一个 IP 进行 PAWS 检查,不同 IP 之间是独立的。
为什么不能在 NAT 中开启
由于NAT 技术时网络地址转换技术,会将一个 IP 地址对应给多个客户端,导致 不同客户端之前发送 SYN 包时,携带时间戳时不一致的,会造成丢包情况。