一、概述
在之前的文章 为什么 TCP 建立连接需要三次握手 中,关于下面 3 个问题给出了简单的回答:
- 第一次握手时可以携带应用数据吗?
- 第二次握手时可以携带应用数据吗?
- 第三次握手时可以携带应用数据吗?
简单来说,传统的 TCP 建立连接时需要三次握手,而且这三次握手只发送简单的 SYN 和 ACK 报文。
从网络带宽的资源利用的角度来看,传输层的 TCP 头部 + 网络层的 IP 头部,最少有 40 个字节,为了发送几个字节的报文数据包,而额外组装了 40 个字节的头部,这有点类似前面讲 TCP 滑动窗口时,所谓的 “糊涂窗口综合症”。
从应用优化的角度来看,因为要等到 TCP 经过三次握手建立连接之后才能发送应用层数据,所以会造成应用程序首次发送数据时存在一定的延迟,尤其是短连接、移动设备等场景中,这种副作用会加剧。
那么这种问题如何解决呢?使用 TFO 解决方案。
📝 PS: 因为 TCP 协议栈不同版本间存在差异,所以本文的前提是 TCP 三次握手时不传输数据,也就是传统的 TCP 三次握手,事实上,很多云计算服务商提供的 Linux 发行版本都对网络协议栈进行了优化,会在 TCP 第三次握手时直接发送数据,读者自己抓包验证时,可能会和本文结果存在一定差异。
二、TFO
TCP Fast Open (TFO) 是在传统的三次握手基础上进行优化,允许在握手过程中发送数据,从而减少首次发送数据的延迟,提升网络应用性能。
1.实现原理:
TFO 的核心原理是在发送方和接收方通信中,引入 1 个 Cookie 机制,这样使发送方在后续重连接收方时,能够简化 TCP 三次握手。
顾名思义,TFO Cookie 中的 Cookie 和 Web 应用层 中的 Cookie 机制一样,第一次访问时,需要登录验证,然后由服务端验证后,后续访问中可以直接携带,无需再次登录。
(1) 首次连接
- 当发送方第一次和接收方建立 TCP 连接时,发送 1 个 SYN 报文
- 接收方返回 SYN-ACK 报文的同时,附带一个随机生成的名为 TFO Cookie 的标识符给发送方
- 发送方收到 SYN-ACK 报文后,保存 TFO Cookie,发送 ACK 报文给接收方,完成三次握手,开始传输数据
(2) 后续连接
- 当发送方再次连接同一个接收方时,可以在 SYN 报文中携带上次保存的 TFO Cookie,同时在 SYN 报文中附带应用层数据 (也就是第一次握手时就直接发送数据)
- 接收方验证发送方的 TFO Cookie 后,将数据发送给应用层处理,并返回 SYN-ACK 报文 (同时也可以发送数据)
- 发送方收到 SYN-ACK 报文后,发送 ACK 报文给接收方,完成三次握手
2.优点
通过 TFO,发送方在发送 SYN 报文时就可以直接携带数据,接收方可以在第一次握手时直接处理数据,并且在第二次握手时直接发送数据,最终:
- 发送方第一次发送数据,减少了 1.5 个 RTT 延迟
- 接收方第一次发送数据,减少了 1 个 RTT 延迟
发送方第一次发送数据,减少了 1.5 个 RTT 延迟
接收方第一次发送数据,减少了 1 个 RTT 延迟
3.局限性
(1) 兼容性
需要通信双方都支持 TFO, 如果其中一方不支持,连接自动回退到传统的 TCP 连接建立过程,此外,通信链路中的转发设备 (NAT, 防火墙) 也会执行这个兼容性机制。
(2) 安全性
虽然 TFO 的 Cookie 是由接收方生成并发送给发送方的,并且每个 Cookie 都与发送方关联,但是增加了接收方的安全攻击面,可能引发诸如 “TCP SYN Flood” 放大攻击 等安全风险。
如果攻击者从被入侵主机获取到有效的 TFO Cookie,进而伪造了大量的携带数据报文,那么接收方就需要大量的内存来临时存储应用数据,最终导致内存耗尽。
(3) 部署环境要求
对内核版本有要求,且需要修改内核参数。
(4) 应用数据过大
如果发送方第一次要发送的数据大于 TCP 的 MSS, 依然需要拆包进行多次发送,当应用数据过大时,TCP Fast Open 带来的优势 (RTT 减少) 几乎可以忽略。
三、模拟环境
TFO 需要发送方和接收方同时支持,如果任意一方不支持 TFO,连接会自动回退到传统的三次握手方式。
为了演示效果,笔者使用了 2 个 Linux 服务器作为通信发送方和接收方,对应的发行版本和内核版本参数如下。
1.TFO 对 Linux 内核版本要求: >= 3.7。
2.内核参数调整
TFO 启用需要修改默认内核参数:
- 0:关闭 TFO
- 1:启用发送方模式 TFO
- 2:启用接收方模式 TFO
- 3:同时启用发送方和接收方模式 TFO
作为模拟实验,笔者只是临时修改了参数,可以采用如下步骤进行配置永久生效:
(1) 编辑 /etc/sysctl.conf 文件,添加配置项
(2) 运行 sysctl -p 命令生效,重启之后仍然有效
四、程序代码
如果读者使用主机的 curl 版本较高,可以直接使用如下方式直接开启 TFO 机制方式访问:
因为笔者所使用的服务器中的 curl 版本较低,所以这里编写 Python 脚本代码,核心代码其实就是 2 个套接字的参数的设置而已。
1.接收方 (服务端) 代码
将接收方作为服务端程序的方式来实现,绑定/监听指定端口,然后接收来自发送方 (客户端) 的 TCP 连接。
2.发送方 (客户端) 代码
五、运行程序实验
程序核心代码 (总共 2 行) 准备就绪,接下来开始运行程序,验证 TCP Fast Open 过程。
服务端公网 IP: 104.21.71.166
1. 启动服务端程序,并确认监听状态
2. 客户端开始抓包
打开 WireShark, 监听对应的网卡设备。
3. 运行客户端程序
4. 查看客户端 TCP 连接状态
一切运行正常,接下来就可以去看 WireShark 的抓包结果了。
六、WireShark 抓包结果
首先使用 tcp.options.tfo 过滤条件,快速筛选出和 TCP Fast Open 有关的 TCP 报文。
下面对 WireShark 抓包结果展开分析一下。
1.第一次建立连接
当发送方第一次和接收方建立 TCP 连接时,发送 1 个 SYN 报文,以及设置 TCP Options 字段 TCP Fast Open 。
此时并没有发送任何数据,所以 WireShark 抓包结果中的 Len = 0。
接收方返回 SYN-ACK 报文的同时,附带一个随机生成的名为 TFO Cookie 的标识符给发送方。
发送方收到 SYN-ACK 报文后,保存 TFO Cookie,发送 ACK 报文给接收方,完成三次握手。
其中 TFO Cookie 的值为: d82d9074a6105a13。
三次握手完成后,开始传输数据。
2.后续建立连接
通过截图可以看到,后续客户端和服务端建立 TCP 连接时,会在第一次握手时携带 FTO Cookie 并且直接发送数据,所以 WireShark 抓包结果中的 Len = 13。
那么这个 13 是什么?就是客户端发送的数据,正好是 13 个字节。
后续 TCP 连接建立 (第一次握手) 时就可以直接发送数据 (篇幅所限,这里只截图 2 个数据抓包详情):
每个数据包中的 TFO Cookie 的值都是 d82d9074a6105a13,也就是第一次建立 TCP 连接时,服务端发送 SYN-ACK 报文时携带的值。