如果你只是在编写 Web 应用,你完全无需了解三次握手的细节——因为我真的没想到应用场景,如果你知道,可以告诉我。
故事要从我第一次编写长连接应用说起,从 HTTP 下的需求开发,到编写 TCP 长连接应用,于我而言,倘若不理解握手流程,就很难理解:
- 什么是数据包?
- 如何解析一个包?
- 握手流程里的事件用途?
- 怎样的表现是传输出错?
- 如果出现传输问题,哪个步骤出了错?该怎么解决?
- 框架有问题,想提个修复 PR,但这个代码注释说 MSS?MTU?
- 我需要支持自有包格式,该怎么写解析、封装?
- TCP/UDP/HTTP 等实际使用的协议(此处并非指某一层,譬如 HTTP 也是基于 TCP,不是说这个),性能差距差在哪里?
- etc.
如果此时你选用 Socket.IO 或类似的框架,情况还好一些,如果没有必要,你无需关注数据包问题——当数据出现异常时,理解握手有时候会成为解决问题的必须条件。
这就是我对 TCP 握手第一次产生兴趣的时刻。
TCP 握手流程
SYN:Synchronize;
ACK:Acknowledge;
FIN:Finished。
发起方 | 接收方 | 意义 |
---|---|---|
发送:SYN + A序列号;状态:SYN-SENT | ||
发送:ACK-SYN + A序列号 + B序列号 状态:SYN-RCVD | 接收方确认:接收方可收,发起方可发 | |
发送:ACK,状态:Established | 发起方确认双方可收发,我方序列号被接受 | |
状态:Established | 接收方确认双方可收发,我方序列号被接受 | |
通讯中.... | 通讯中.... | |
发送:FIN | 发起方想结束连接 | |
发送:ACK | 接收方了解发起方想结束连接 | |
发起方收到 | 发起方了解接收方已知晓 | |
发送:FIN | 接收方知晓数据传完,可关闭 | |
发送:ACK | 发起方知晓数据传完,可关闭 | |
接收方收到,不再维持连接 | 接收方知晓发起方已知晓连接可关闭 | |
等待 2 个 MSL 时间...不再维持连接 | 发起方需确认最后消息未丢包,则关闭自身 |
序列号
TCP 协议采用序列号+1的方式,来确认双方通讯的有序性。
在很老的 TCP 版本中,该起始序列号是固定的——这导致了轻松的连接劫持,只需要模拟固定的序列号即可。
不用担心,后续版本已将它升级为随机起始值。且对采用了 SSL / HTTPS 等安全协议的服务,这种问题几乎不存在。
注意安全,仍有其他劫持可能运作,譬如复合了 IP 欺骗或盲目劫持等手段后,发起的钓鱼、中间人攻击等劫持方式。
MSL 时间
数据段的最大生命时间(Maximum segment lifetime),这里有三个重点:
- 超过此时间的数据段将被丢弃(数据段或叫报文?)。
- 在 Linux,你可以通过 tcp_fin_timeout 和 tcp_keepalive_time 来调整它。
- 如果你喜欢 Windows Service,可以尝试 How Do I Reduce the Time for Canceling TCP Connections in TIME_WAIT State on Windows
- 1981 年的原始协议规定 MSL = 120s,实际上,Linux 内核将其缩短为 30s——这对于某些应用来说,仍然过长了。
SYN 泛洪攻击
指:
- 通过大量构造连接,且在最后一步不发送 ACK 数据包,从而构造半开连接;
- 服务端为连接分配资源后,长时间维持无用连接的情况(考虑 MSL 时间,这个连接会存在很久)。
- 当大量的连接被建立、分配资源、搁置后,整个服务进入停摆状态。
可以通过回收最先创建的 TCP 半开连接、 IP 鉴别并暂时拉黑,云服务商还会有握手沙盒服务等。
让我们回到 TCP 本身。
超时重传
TCP 的安全性有各种机制保证,超时重传就是其中之一。
超时重传时间,也叫 RTO 时间(Retransmission-TimeOut),它如何如何发挥作用呢?
- 当一个包传输 RTO 时间后仍然没有结果,则确认包丢失;
- 当确认包丢失后,发送端将重新发送信息给对方,此时 RTO 时间加倍;
- 重复上述过程,重试 3 次后仍无效,放弃。
RTT 时间
往返时延时间(Round-Trip-Time),这里介绍这个概念,主要是因为 RTO 时间会受到 RTT 时间的影响。
首先,RTT 计算方式:一端发送 SYN 包起,另一端回复 SYN-ACK 包结束。
接着,考虑到网络传输的时延和不稳定,TCP 会计算出 SRTT(Smoothed RTT),作为新 RTO 的计算系数之一。
(具体计算方式可以参考 RFC 的相关文档,写的很明确,附在了引用中)
ARQ 协议
自动重传请求(Automatic Repeat-reQuest),它本身只是一个协议,各种安全通讯协议都有对应的实现。
当数据包超时后,会触发 ARQ 机制进行重传。
TCP 包细节
标头
TCP 包中,标头是指 IP Header、TCP Header 这部分,常规而言,两者均为 20 字节(可以扩展,但几乎没有需要)。
标头用于指定各种地址、协议、包相关的信息:
需要注意的是:源/目标端口号、序列号、控制标记(ACK 等),它们都在标头中。
载荷
跟随在标头后,是数据包的载荷部分,这里面则包含了数据包的具体数据。
多数应用层开发者都更关注载荷,而非标头。
MTU 尺寸
包头 + 载荷,构成了一个数据包的尺寸,那我们用 MTU(Maximum transmission unit,最大传输单位) 来描述它。
若一次数据传输超过这个单位,将进行分段传输。
MSS 尺寸
MSS(最大分段大小)用于标识数据块的尺寸,也就是:MTU - 标头尺寸 = MSS。
此处有 MSS 协商的概念,连接双方在握手的 SYN 阶段确认彼此的 MSS 尺寸,同时以最小值作为本次连接的信息交换尺寸。
这个协商并非强制进行的,可能一端并不会传输自己的 MSS 尺寸,导致:
如标头后加入了其他信息(IPsec、GRE 隧道传输等信息),可能会导致 MTU 超过 1500 的常见尺寸,造成隐蔽的包体内容不全。
引用
本文参考或引用以下资料,在此致谢:
The TCP / IP Guide
淘宝二面,面试官居然把TCP三次握手问的这么详细
Understanding RTT Impact on TCP Retransmissions
TCP 的特性 - # 笔试面试知识整理
Why is TCP MSL so long? - ServerFault
RFC - 6298
引申1:找资料时看到有博主表示,抓包时了解 TCP 很有用,这一点对 tcpdump
、fiddler
是有意义的,可如果你是用 Charles
、Chrome Console 的 Network
抓包,它已经把 懒人化 做到极致了,其实你需要的数据,98% 都只是传输信息,而非包数据。
引申2:TCP 的相关知识点很充足,导致写的有点久。很多有趣的内容也没有说到,譬如:
- TCP 的流量控制、拥塞控制,这是 TCP 重要的设计概念,对 KCP 也有不小的影响;
- TCP 的攻防机制,与 UDP 攻防的不同点。
- TCP 的配置,在 Linux 环境的性能优化,知名的网游 WOW(魔兽世界)就采用了 TCP 协议。
可能后续会写一些跟进文章来解释。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。