通常在介绍TCP的时候,都会介绍说TCP是一个面向连接的、可靠的、基于字节流的传输协议,这三个特性其实都和TCP的可靠性机制相关。这篇文章来深入了解一下TCP的可靠性机制。

TCP的序列号机制

TCP为上层提供的服务是基于字节流的传输服务,但是TCP是如何保证这些字节流传输的可靠性的呢?答案很简单,让接收端确认接收到的每一个字节,当发送端知道发送的字节被确认之后,就认为发送的字节已经可靠到达对端。

所以,为了能够识别出哪些字节没有被确认,TCP对每一个字节都施加了一个序列号,重复一遍,是针对每一个字节哦。下面的例子A端发送(LEN)99个字节给B端,这99个字节的第一个字节编号(SEQ)是101;然后B确认了99个字节都被收到,应答的序列号(ACK)是200(因为序列号从101到199的都收到了,B通过ACK告诉A说下回从200开始发):

A --> SEQ=101, LEN=99  --> B
A <-- ACK=200          <-- B

每个字节一个序列号的好处是TCP可以字节流中随意切出一段,放到IP报文中发送。跟那些序列号作用于数据包的协议相比,TCP的协议栈实现会简单些,不需要记住那些字节放进了哪些包。因此,RFC793给TCP的数据包起了一个非常形象的称呼,叫做Segment(分段)。

但是有一个负面因素,那就是TCP的序列号是32位,空间有限,当达到最大值的时候要重新从零开始循环使用。这会带来一些潜在的危险,当序列号翻转变小后,万一这时那些迟到的、装有旧数据、但序列号比当前序列号大的分段到来了怎么办?这个现象是有可能发生的,一则网络可能发生故障导致这种情况;二则使用该TCP连接的应用程序崩溃了,然后重新运行,重新建立相同连接,这时候之前使用的序列号信息可能已经消失了,需要重新挑选一个序列号,很有可能就和之前的重了。

对于上述的第二种情况,RFC793介绍了两种应对措施:

  1. 初始序列号Initial Sequence Number(ISN)根据时间信息来计算,在不发生翻转的情况下可以保证,在这一秒选择的ISN一定比上一秒来得大。
  2. 崩溃后需要等待一个2分钟的MSL(Maximum Segment Life)值才能去重新建立TCP连接,也就是说假设发送的分段在中间网络不可能存在超过2分钟。

上面的做法不是完全靠谱,于是RFC7323的章节PAWS - Protection Against Wrapped Sequences介绍了通个一个TCP Timestamp Option来识别旧分段的算法。

关于SYN和FIN

从上面章节可以看出,TCP的可靠性机制就是给需要保护的字节加上序列号。然而,TCP协议中有两个控制位也需要可靠性保护,就是SYN和FIN。可是这两个控制位并不是以数据的方式存在,而是TCP协议头的字段,怎么保护呢?唯一的办法就是把SYN和FIN也当成字节来看待。比如SYN标志置位的时候,占一个序列号,并且计算TCP负载长度的时候还要把SYN当成一个字节算上。对于FIN的处理和SYN是一样的。

序列号的安全性的

现在来谈谈安全性问题。TCP的序列号在传输数据的时候至关重要,万一有第三方伪造TCP分段并发送给接收端怎么处理呢?按常理说,网络和防火墙应该把这些不实的IP报文给过滤掉。如果网络过滤不掉,那么正常的TCP分段只能和这些伪造的TCP分段进行赛跑了,谁先到达接收端,谁就赢了。到晚的就会被当作过时的包给丢掉。不过正常的TCP分段胜算还是蛮大的,一般情况下发送端在发送的时候都是以TCP窗口允许的最大速率来发送数据,先到的可能性比较大。关于这个话题,也可以参考Wikipedia的描述