当前 web 和 web-like 应用中一般都是在三次握手后开始数据传输,相比于 UDP,多了一个 RTT 的时延,即使当前很多应用使用 长连接 来处理这种情况,但是仍然有一定比例的 短连接 ,这额外多出的一个 RTT 仍然对应用的时延有非常大的影响。T/TCP 和 TFO 就是在这种背景下面提出来的。
点击此处:什么是短链接与长连接?

什么是 T/TCP?

T/TCP 是基于事务的 TCP,“事务” 是指类似与 http 和 DNS 这类连接持续很短的请求-回复型应用。对于这类应用,往往客户端只会发送一次数据请求,然后服务器回复一次请求,然后连接就结束了。而建立连接的过程就需要三次握手,由此看来,一般 TCP 对于这类事务的数据传送效率是很低的。T/TCP 就是为了解决这个问题而被提出——直接在 SYN 报文中传递数据。T/TCP 能够把 SYN 、FIN、SYN 合并到一个报文中(前提是数据量必须小于 MSS),从而完成一次对话只需要三次传递:

不过,由于将 SYN 和数据合并发送存在极大的安全隐患,所以这种方式并未得到推广,大多数操作系统也并不支持。因此本文就不对 T/TCP 进行展开了,详细内容可参考《UNP》P321 。

什么是 TFO?

TFO 的 cookie 机制修补了 T/TCP 的安全漏洞,但必须先进行一次完整的 TCP 三次握手后续一定时间内的握手则能够合并 SYN 和数据 ,如下:

对比图

说明:TCP 第三次握手发送的 ACK 报文可以携带数据,所以当客户端收到 HTTP Response 前需要两个来回(RTT),而在 TFP 第二次及以后的请求中,只需要一个来回(RTT)就可收到数据。

尽管 RFC793 并没有禁止 SYN 报文携带数据,但所有的 TCP 实现默认都不会使用 。原因是这不太安全,站在 Server 的角度,收到这样一个 SYN 报文,但这个时候 TCP 握手还没完成呢,对端真的可信吗?说不定是一个伪造源端的 TCP 报文,稳妥起见,这个数据报文还是等握手完成之后再上送给应用吧。这就是 TCP Fast Open 的来源,它允许在第一个握手的 SYN 报文中携带数据,如此以来,短连接便可以节省一次来回的 RTT。 即使 Web 浏览器之类的应用程序尝试使用HTTP 长连接来缓解此问题,即浏览器保持与 Web 服务器的连接,并将该连接重用于以后的 HTTP 请求。但是,这种技术的有效性会降低,因为空闲连接可能会在它们被重用之前关闭。例如,为了限制资源使用,繁忙的 Web 服务器通常会主动关闭空闲的 HTTP 连接。

TFO过程

  1. 在使用 TFO 之前,client 首先需要通过一个普通的三次握手连接获取 FOC (Fast Open Cookie)
    • 1.client 发送一个带有 Fast Open 选项的SYN包,同时携带一个空的 cookie 域来请求一个 cookie
    • 2.server 产生一个 cookie,然后通过 SYN-ACK 包的 Fast Open 选项来返回给 client
    • 3.client 缓存这个 cookie 以备将来使用 TFO 连接的时候使用
  2. 执行 TFO
    • 1.client 发送一个带有数据的 SYN 包,同时在 Fast Open 选项中携带之前通过正常连接获取的 cookie
    • 2.server 验证这个 cookie。如果这个 cookie 是有效的,server 会返回 SYN-ACK 报文,然后这个 server 把接收到的数据传递给应用层。如果这个 cookie是无效的,server 会丢掉 SYN 包中的数据,同时返回一个 SYN-ACK 包来确认 SYN 包中的系列号。
    • 4.client 发送ACK包来确认 server 的 SYN 和数据,如果 client 端 SYN 包中的数据没有被服务器确认,client 会在这个 ACK 包中重传对应的数据。
    • 4.剩下的连接处理就类似正常的 TCP 连接了,client 一旦获取到 FOC,可以重复 Fast Open 直到 cookie 过期。

代码示范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//server
#include <netinet/tcp.h>
int main()
{
int sock_listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
int qlen = 0;
setsockopt(sock_listen, SOL_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(12345);
addr.sin_addr.s_addr = inet_addr("192.168.248.128");
bind(sock_listen, (struct sockaddr*)&addr, sizeof(addr));
listen(sock_listen, 30);
int sock_conn = accept(sock_listen, NULL, NULL);
//处理数据
close(sock_conn);
close(sock_listen);
return 0;
}
//=========================
//client
int main()
{
int sock;
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in addr_serv;
bzero(&addr_serv, sizeof(addr_serv));
addr_serv.sin_addr.s_addr = inet_addr("192.168.248.128");
addr_serv.sin_port = htons(12345);
addr_serv.sin_family = AF_INET;
char buf[1000];
sendto(sock, buf, sizeof(buf), MSG_FASTOPEN, (struct sockaddr*)&addr_serv, sizeof(addr_serv));
//other things
close(sock);
return 0;
}
  • 注意包含的头文件 <netinet/tcp.h>

  • 第 6 行的 qlen 用来指定 TFO 的半连接队列大小,这是为了避免 TFO 请求过多而资源耗尽,原因和普通三次握手的半连接队列相同。

  • 第 7 行,设置 qlen 和 TCP_FASTOPEN。

  • 第 33 行,如果客户端想将数据和 SYN 合并,就必须使用 sendto 函数,且指定 MSG_FASTOPEN。

    sendto() 一般用于 UDP。

注意,TFO 功能需要在 TCP 通信的双方都启用时才会生效 ,除了上面代码层面上实现 TFO,还需要在操作系统层面上开启此功能 ,方法如下:

修改系统变量:/proc/sys/net/ipv4/tcp_fastopen ,该变量有三个值:

  • tcp_fastopen = 1

    允许客户请求 cookie

  • tcp_fastopen = 2

    允许服务器生成 cookie

  • tcp_fastopen = 3

    运行本机请求和生成 cookie

如果服务器和客户不在一台主机,那么对于服务器主机而言,开启 2 即可;对于客户端,开启 1 即可。下面的实验中,服务器和客户在一台主机,所以需要开启 3 。

wireshark抓包实验

看,第一次连接时是正常的 TCP 三次握手;然后客户断开连接(红色),重连,可看到第二次连接时,SYN 报文中携带有 1000 字节的数据。

另外在 TFO 场景下,关闭连接时第三次挥手直接发送 RST,读者可以试试。

注意事项:

Cookie 的格式:

Cookie 通过 TCP 的选项(Kind = 34)在 TCP 双方之间交互,其格式如下。它的值由 Server 根据 <ClinetIP、ServerIP> 生成。注意,Cookie 与 TCP 端口号无关 ,即使应用程序不同,只要 Client 和 Server 使用的 IP 不变,两台主机上的 TCP 程序就可以复用一个 Cookie ,换句话说,这个 Cookie 是主机粒度的。

1
2
3
4
5
6
7
8
9
10
11
12
13
                                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Kind | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
~ Cookie ~
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Kind 1 byte: value = 34
Length 1 byte: range 6 to 18 (bytes); limited by
remaining space in the options field.
The number MUST be even.
Cookie 0, or 4 to 16 bytes (Length - 2)

TFO 不保证幂等性

什么是幂等性?

幂等 是一个数学与计算机学概念,常见于抽象代数中。 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。在 TCP 中,幂等性可以指对重复包的丢弃。

虽然 TCP 保证重复数据包(重复经常发生)会被接受者忽略,但是 这个保证并不适用于连接的握手过程 。在 TFO 下随着 SYN 发送的数据有可能重复递交到应用层。例如在 IP 层不可靠传输的情况下,发送端的一个 SYN 包被传输成了两个 SYN 包,而在接收端,接收到第一个SYN包后,接收端把随SYN的数据传递到应用层,然后继续收到第二个重复包则可能再次将随 SYN 传输的数据再次传向应用层。因此如果应用层不能忍受这种包重复,则不能开启TFO特性。

换句话说,如果开启了 TFO,应用层就必须自己处理重复 SYN 带来重复数据。

参考文章:TCP Fast Open移动网络性能解密TFO详解TFO