TCP协议

TCP协议

TCP流程

建立连接(三次握手)

  1. 双方初始阶段都是从 close 状态开始,
  2. 服务端从close 状态,监听某个端口,然后进行 listen 状态。
  3. 客户端主动发起连接,发送 SYN,变成 SYN-SENT 状态。
  4. 服务端收到,将连接插入到半连接队列,返回 SYN 和 ACK(客户端得 SYN),自己变成 SYN-REVD
  5. 客户端发送 ACK 给 服务端, 变更为 ESTABLISHED 状态。
  6. 服务端收到,将连接从半连接队列取出,移入全连接队列,变更为 ESTABLISHED 状态。
  7. 进程调用 accept 函数,从全连接队列中取出已完成得连接建立得 socket 连接。

断开连接(四次挥手)

  1. 客户端向服务器发送 FIN 报文,从 ESTABLEISHED 状态 切换至 FIN-WAIT-1,此时客户端变成了 half-close(半关闭) 状态,无法发送报文,只能接受;
  2. 服务收到客户端确认,发送ACK,变成 CLOSED-WAIT 状态。
  3. 客户端收到ACK,变成 FIN-WAIT2 状态。
  4. 服务端再向 客户端发送 FIN, 进入 LAST-ACK 状态。
  5. 客户端收到 FIN 后,进入 TIME—WAIT 状态,发送ACK 给服务端。
  6. 服务端收到关闭,客户端进入等待, 最长 2MSL 后或者没有服务端重发请求后,客户端关闭,否则要重新 发送 ACK

画图 ———————————————–

建立连接的问题

为什么不是两次

根本原因:无法确认客户端得接受能力

如果存在客户端发送 SYN 报文滞留在网络中,进行重传,可能会建立两次连接,

这时客户端连接已经关闭,服务端却发送资源,造成资源浪费。

SYN 包丢失原因

  • 开启 tcp_tw_recycle 参数,并且处于 NAT 环境下 ;
  • Accpet 队列满了
  • SYN 队列满了(SYN 队列满了,应该时 服务器返回给客户端信息,客户端没有应答,即SYN 攻击现象 )

SYN 包丢失原因之一 Accept 队列满了

Linux 内核会维护两个队列:

  • 半连接队列,也称 SYN 队列
  • 全连接队列,也称 Accept 队列

全连接队列太小,或者已满,会造成后续连接被废弃。

如果出现 Recv-Q 超过 Send-Q,就说明发生了 Accpet 队列满得情况。

解决办法:

  1. 调大 Accept 队列的最大长度,调大的方式增大 backlog 或 somaxconn 的值;
  2. 检查系统或者代码为什么调用 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。

如何应对?

  1. 增加SYN 连接(适当增加半连接队列大小 tcp_max_syn_backlog,但需要同时增加全连接队列大小)
  2. 减少 SYN + ACK 重试次数,避免大量的超时重试(针对 大量 SYN_RECV 状态的 TCP 连接,设置 tcp_synack_retries ,降低重试次数,加快连接断开)
  3. 利用 SYN cookie 技术,收到 SYN 不立即分配,计算出 cookie,携带返回客户端,再次收到时,验证合法性分配资源。(设置 tcp_syncookies = 1 ,0 关闭,1 仅当 SYN 队列无法容纳时,启用它, 2 无条件开启功能)

backlog 可以再应用程序中配置,python 再 listen 方法中可以设置,nginx 可以在配置文件中设置。

断开连接的问题

TIME_WAIT作用

  1. 防止旧的连接数据包
  2. 保证连接正确关闭

防止旧的连接数据包

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)

涉及算法:

  • 慢启动
  • 拥塞避免
  • 快速重传和快速恢复

拥塞窗口

拥塞窗口是发送端的限制。

根据接收窗口和拥塞窗口的大小,取最小值,确定 发送窗口。

慢启动

  1. 三次握手,确定双方接收窗口大小
  2. 确定拥塞窗口大小
  3. 开始传输一段时间后,发送端每收到一个 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 表示接收窗口大小。

流量控制过程

  1. 初始化,双方都有 200 字节。
  2. 发送端发送 100 字节,所以nxt需要向右移动 100 字节,可用窗口减少 100 字节。
  3. 接收端处理能力不够,处理40字节,所以发送端要缩小,缓冲队列中 60 字节没有接收。接收端在 ack 报文中带上滑动窗口 140 字节,发送端调整。
  4. 发送端 una 调整向右 40 字节。

PAWS 机制

原理:在客户端与服务端发送 seq 时,加入 时间戳,如果时间戳过期,那么默认会丢弃数据包。

作用:防止TCP 包中的序列化发送绕回。

per-host 的 PAWS 机制

定义:针对每一个 IP 进行 PAWS 检查,不同 IP 之间是独立的。

为什么不能在 NAT 中开启

由于NAT 技术时网络地址转换技术,会将一个 IP 地址对应给多个客户端,导致 不同客户端之前发送 SYN 包时,携带时间戳时不一致的,会造成丢包情况。