1

系统学习TCP

 1 year ago
source link: https://dcbupt.github.io/2022/07/23/blog_article/%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0%E7%B3%BB%E5%88%97/%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0TCP/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

TIME_WAIT 状态

ref:https://zhuanlan.zhihu.com/p/450296852

ref:https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247502230&idx=1&sn=5fb86772de17ab650088944d4d0adf62&scene=21#wechat_redirect

什么是 TIME_WAIT 状态?

断开 TCP 连接的四次挥手流程中,主动发起断开的一方发送针对对端 FIN 消息的 ACK 消息后,进入 TIME_WAIT 状态。该状态会持续 2MSL 时间,即 2 倍的最大分片生命时长后结束。LINUX 系统中,2MSL 设置为 60s

为什么要有这个状态?

  • 防止历史连接中的延迟报文,被后面相同四元组的连接错误的接收;
    • 因为等待了 2MSL 时间,所以延迟报文肯定不会在后续的 tcp 连接上收到
    • 不能通过 TCP 的报文序列号来判断是否为延迟报文,因为序列号不是单调递增的,到一个极值后会回绕到初始值
  • 保证「被动关闭连接」的一方,能被正确的关闭;
    • 如果主动发起断联的一方回复的 ACK 消息丢包,对端会重传 FIN 消息,此时如果没有 TIME_WAIT 状态,会回复 RST 消息,导致对端不能优雅地断开连接

如果处于 TIME_WAIT 状态的 TCP 连接过多,会有什么问题?

HTTP 请求的 QPS 过高时,服务端可能出现很多处于 TIME_WAIT 状态的连接,把端口号打满导致没有空闲端口来建立新的 TCP 连接

如何解决 TIME_WAIT 过多的问题?

  • 开启 LINUX 的内核参数 tcp_tw_reuse,默认是关闭的
    • 该参数能使得 TIME_WAIT 状态的连接被新连接复用,注意必须是主动发起的 TCP 连接
    • 开启 tcp_tw_reuse 必须配合开启另一个内核参数 tcp_timestamps,不过它默认就是开启的
      • 开启 tcp_timestamps,即打开 TCP 时间戳,这样就能基于 TCP 报文的时间戳来判断是否为历史连接中的延迟报文。时间戳是根据 CPU 时钟生成的

注意,不要开启内核参数 tcp_tw_recycle 来解决 TIME_WAIT 过多问题。tcp_tw_recycle 开启后,只会识别 IP 来做报文的时间戳校验。在 NAT 网络中,两个客户端的 IP 一样,如果新的客户端发送的建连 SYN 报文的时间戳小于之间客户端报文的时间戳,回收了 TIME_WAIT 连接的端口就会丢弃客户端的 SYN 消息,导致连接建立失败

既然默认开启了 tcp_timestamps,为什么不默认开启 tcp_tw_reuse?

  • 无法保证「被动关闭连接」的一方,能被正确的关闭
  • RST 报文不会做时间戳校验,如果复用了 TIME_WAIT 的新连接收到了延迟 RST 报文,且序列号恰巧也在接收窗口内,就会处理 RST 报文,导致连接“意外”断开

半连接队列和全连接队列

ref:https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247484569&idx=1&sn=1ca4daeb8043a957850ab7a8f4f1120e&scene=21#wechat_redirect

什么是半连接队列和全连接队列

TCP 建连的三次握手中,服务端收到 SYN 报文后,会把连接放到半连接队列,服务端收到客户端对自己 SYN 报文的 ACK 后,把连接从半连接队列移到全连接队列(也称 accept 队列),等待进程调用 accept 把连接取走

如果瞬时流量把全连接队列打满了怎么办?

LINUX 提供内核参数 tcp_abort_on_overflow,默认为 0,即扔掉客户端的 ACK 报文。这样客户端感知不到,就会接着发送 HTTP 请求,且携带 ACK,当服务端全连接队列有空闲后,依然会 accept 拿到这次连接并成功建连的。
除非你确定全连接队列将长期溢出,否则不要将 tcp_abort_on_overflow 改为 1,否则服务端会回复 RST 报文,导致这次连接建立失败。

如何增大全连接队列

全连接队列足最大值取决于 somaxconn 和 backlog 之间的最小值

  • somaxconn 是 Linux 内核的参数,默认值是 128
  • backlog 是内核方法 listen 的参数,由 web 服务器程序决定。Nginx 默认 511

如何增大半连接队列

需要同时增大内核参数 tcp_max_syn_backlog 和全连接队列大小

什么是 SYN 攻击?

SYN 攻击就是客户端发起 SYN 报文后,不响应服务端的 SYN 报文,导致服务端半连接队列打满无法建立新连接。也称 DDos 攻击

如何防御 SYN 攻击?

  • 增大半连接队列
  • 开启内核参数 tcp_syncookies
    • tcp_syncookies 可以在不用半连接队列的情况下建连成功
    • 服务端收到 SYN 报文后,在发送 SYN+ACK 报文里携带 cookie,如果收到客户端 ACK 报文后 cookie 验证合法就认为建联成功
  • 减少 SYN+ACK 重传次数
    • 因为 SYN 攻击的一方不会回复 ACK,所以服务端可以修改内核参数 tcp_synack_retries 的值,来减少 SYN+ACK 重传次数,以加速断开这种 TCP 连接

ref:https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247511590&idx=2&sn=faa6a4c6124eca8a79201489abfd394e&chksm=f98dea8ccefa639af93083f9d917775efc103cf239a54d672c5a9dd7808f4d77447aa6e4c6a4&scene=178&cur_album_id=1337204681134751744#rd

TCP 基于序列号的确认应答和重传机制保证了数据的可靠传输。重传机制主要是“超时重传”和“快速重传”二者的结合

报文发送端会不断抽样加权计算得到报文从发送到收到应答的平均往返时间,当报文超过平均往返时间还没收到应答,就重传报文

当发送方连续三次收到针对同一个报文的 ACK 后,就会重传报文,即使还没超时

  • 接收端需要保证数据连续性,所以当前面有报文没收到时,即使收到了后面的报文,ACK 也返回前面缺失报文的序列号

快速重传的问题是,重传时无法决定是只重传 ACK 指向的报文还是把后面的报文都重传。因此引入了 SACK( Selective Acknowledgment 选择性确认) 机制

SACK 在 TCP 头部「选项」字段里加一个 SACK 字段,它可以将缓存的地图发送给发送方,这样发送方就知道哪些数据收到了,哪些数据没收到,就可以只重传丢失的数据

  • 通过内核参数 net.ipv4.tcp_dsack 开启 SACK 机制,默认开启

ref:https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247511590&idx=2&sn=faa6a4c6124eca8a79201489abfd394e&chksm=f98dea8ccefa639af93083f9d917775efc103cf239a54d672c5a9dd7808f4d77447aa6e4c6a4&scene=178&cur_album_id=1337204681134751744#rd

滑动窗口指发送方可以连续发送 TCP 报文的一个缓冲区空间。当前面发送的报文收到 ACK 后,窗口就会向后滑动,继续发送后面的 TCP 报文

接收方每次收到报文,都会将当前 TCP 连接中还没收到的最前面的报文的序列号放到 ACK 里。
基于这种确认机制,当接收方收到了多个连续的 TCP 报文,即使中间一个 ACK 丢包了也不会影响,因为发送方只要收到最后一个报文的 ACK,就认为前面的报文也被接收了,这就叫“累积确认”

接收方将自己可接收的网络缓冲区大小放到 TCP 协议头部的 Window 字段,以此控制发送方滑动窗口的大小,实现流量控制

  • 可以认为滑动窗口的大小约等于接收方当前可接收的网络缓冲区大小。

如果接收方网络缓冲区都占满了,则 ACK 消息里 Window =0,此时窗口关闭
当窗口关闭时,发送方将不再发送后面的报文,而是启动一个定时任务,到时间则发送一个窗口探测报文查询接收方当前可用缓冲区大小,如果仍为 0,则继续这一过程,不为 0 则可以继续发送报文

糊涂窗口综合症

当接收方的应用层读取数据速度较慢时,接收方的可用缓冲区不断缩小,导致滑动窗口最后变得很小,一个 TCP 报文只能携带很少的应用数据(TCP、IP 协议字段占了大头),这就是糊涂窗口综合症,非常浪费带宽

为了避免糊涂窗口综合症,发送端和接收端有对应的策略

发送端使用 Nagle 算法延迟发送报文

  • 要等到窗口大小 >= MSS 或是 数据大小 >= MSS
    • MSS 指 TCP 包能容纳应用层数据的最大长度,在建立连接的时候通常要协商双方的 MSS 值
    • 还有一个相关的参数 MTU,指网络包的最大长度,即 MSS+TCP 头+IP 头,一般为 1500 字节
  • Nagle 算法默认是打开的,但对于小数据包强交互的程序如 ssh 或 telnet 并不合适,因此这类程序里需要设置 TCP_NODELAY 来关闭 Nagle 算法
    • 没有内核参数来全局开关 Nagle 算法

接收端在可用缓冲区较小时,直接在 ACK 里向发送方通告窗口为 0

  • 较小指可用缓冲区小于 min( MSS,缓存空间/2 )

ref:https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247511590&idx=2&sn=faa6a4c6124eca8a79201489abfd394e&chksm=f98dea8ccefa639af93083f9d917775efc103cf239a54d672c5a9dd7808f4d77447aa6e4c6a4&scene=178&cur_album_id=1337204681134751744#rd

滑动窗口本质是发送端匹配客户端消费速度的一个数据发送窗口,但如果在网络环境拥塞时一股脑将滑动窗口内的数据都发送出去,可能很多报文都需要超时重传,浪费带宽且可能继续加剧网络拥塞
因此在发送端需要拥塞控制,即控制数据发送的速率

每次发送的报文数以 2 的指数倍增加,前提条件是前面发送的报文都收到了 ACK。因此最开始发送速率最慢,一般是 1 个 MSS 长度的报文,然后快速增长,最后达到阈值结束慢启动阶段,进入拥塞避免阶段

为避免造成网络拥塞,后续速率线性增加,即前面所有报文都 ACK 了,下次才会在之前的基础上再多发一个报文

重启慢启动

如果发生超时重传,说明网络拥塞,会重新走一遍慢启动阶段,且慢启动阈值是发生拥塞时的 1/2

如果发生快速重传,说明网络并不算很拥塞,再收到前面遗漏报文的 ACK 后,会将发送速率减半,然后进入拥塞避免阶段


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK