TCP是一种面向连接的单播协议。在数据交换之前,双方必须先建立连接。TCP的服务模型是一个字节流。TCP会修复下层(如IP层,链路层等)产生的数据传输问题,对于数据交换来说,大致有三个方面的问题:丢包、重复和错误。为了解决这些问题,TCP引入了连接的概念。***可以说,UDP和TCP本质上最大的区别在于TCP需要维护连接。TCP由于要维护连接所以TCP是有状态的,相比于无状态的UDP,TCP在妥善处理多种TCP状态问题时需要面对大量的细节问题,比如连接的建立、终止和重启。这一章我们就来一起讨论关于TCP的连接管理。
TCP连接的建立与终止一个TCP连接是有一个4元组构成的,简单来说就是一组IP和一组端口号。一个TCP连接通常分为三个阶段:启动、数据传输和退出。下面我们就对这三个阶段一步步讨论。
图 1
在图1中的时间轴描绘了一个连接建立过程中的相关事宜。
连接建立 正常的连接建立流程(三次握手)首先我们先讨论一个TCP连接建立的过程:
-
客户端发送一个SYN报文段,并指明自己想要连接的端口和它的客户端初始序列号(记为ISN©)。通常客户端还会借此发送一个或多个选项。客户端发送的这个SYN报文段称作段1。这个时候客户端进入SYN-SENT状态
-
服务端收到SYN报文段后,进入LISTEN状态。服务端也发送自己的SYN报文段作为相应,并包含了它的 初始序列号(ISN(s))。该段称为段2。为了确认客户的的SYN,服务器将其包含的ISN©数值加1后作为返回的ACK数值,然后进入SYN-RCVD状态。因此,每送一个SYN,序列号就会自动加1。这样如果出现丢失的情况,该SYN段将会重传。
-
客户端收到报文后,客户端进入ESTABLISHED状态,这个时候对于客户端来说已经是可以交换数据的状态了。为了确认服务器的SYN,客户的将ISN(s)的数值加1后最为返回的ACK值,这称为段3。
- 服务端收到后,进入ESTABLISHED状态,这个时候双方就进入正式的数据交换过程。
TCP A TCP B 1. CLOSED LISTEN 2. SYN-SENT -->--> SYN-RECEIVED 3. ESTABLISHED <-- <-- SYN-RECEIVED 4. ESTABLISHED --> --> ESTABLISHED 5. ESTABLISHED --> --> ESTABLISHED Basic 3-Way Handshake for Connection Synchronization 在[rfc793]中对于三次握手的图示
通过上面这三个报文段就能够正常建立一个TCP连接,也称为三次握手。三次握手的目的不仅在于让通信双方连接一个连接正在建立,还在于利用数据报的选项来交换特殊信息,交换初始序列号ISN。一般来说,三次握手是首个发送SYN的一方被认为主动打开一个连接,我们一般称之为客户端,而被动打开连接的一方我们称之为服务端。但是有一种特殊情况是两个端点同时发起连接,下面会讨论这种特殊的情况。
同时打开 Simultaneous initiation两个端点同时给对方发送主动建立连接的情况不多见,但是在特定的场景下是有机会出现的。前提是两个端点都有对方的ip和端口,就有可能发生这种情况,对于同时打开,需要四个报文段。
TCP A TCP B 1. CLOSED CLOSED 2. SYN-SENT -->... 3. SYN-RECEIVED <-- <-- SYN-SENT 4. ... --> SYN-RECEIVED 5. SYN-RECEIVED --> ... 6. ESTABLISHED <-- <-- SYN-RECEIVED 7. ... --> ESTABLISHED Simultaneous Connection Synchronization
从上面的图可以看出,同时打开每个TCP都是从CLOSED 到 SYN-SENT 再到 SYN-RECEIVED 到已确立的。
这里说一下我个人关于三次握手的一些想法。大家对这个流程想必都不陌生,但是我感觉很多人在描述这个过程的时候并没有说出关键的信息,就是三次握手每次是为了什么。首先握手的目的就是双方确认对方的收发能力,对于两端来说要确认的东西是一样的,确认对方能接受自己的信息,能给自己发送信息。
客户端给服务端发消息的时候,这时候服务端知道客户端有发送消息的能力。
服务端给客户端发送消息的时候,客户端就知道服务端有接受消息和发送信息的能力。
客户端再次给服务端发送消息,这时候服务端就知道客户端有接受信息的能力(通过ACK知道消息是应答而不是重复发)。
不管是哪一端,收到对方的消息就是代表对面有正常发送消息的能力,收到对方的ACK = 己方发送的SEQ + 1就证明对方是有正常的接收能力。
这也很好解释为什么不是四次握手,因为三次握手就已经足够让两端都确认对方有正常的收发消息的能力。
当然在三次握手的过程中还处理了很多别的细节问题,不过在我看来,三次握手本质上要做的事情只有一件,就是确认对方的收发能力。
再来看同时打开为什么是四次,其实就是接收到对方正确的ACK回复就证明对方有正常的收发能力。
TCP 快启TCP Fast Open这里举一个例子,如果没有三次握手有可能发生什么情况:
TCP A发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达TCP B。本来这是一个早已失效的报文段。但TCP B收到此失效的连接请求报文段后,就误认为是TCP A再次发出的一个新的连接请求。于是就向TCP A发出确认报文段,同意建立连接。
假设不采用“三次握手”,那么只要TCP B发出确认,新的连接就建立了。由于是TCP A发送的请求报文段滞留后才到达服务端,而现在TCP A并没有发出新的建立连接的请求,因此不会理睬TCP B的确认,也不会向TCP B发送数据(此时的 TCP A 对于 TCP B 其实是没有接收能力的,因为接收到什么都是扔掉的**)。但TCP B却以为新的连接已经建立,并一直等待TCP A发来数据。这样,TCP B的很多资源就白白浪费掉了。
TCP 快启策略使用存储在客户端的 TFO cookie 与服务端快速建立连接。
TCP 连接的客户端向服务端发送 SYN 消息时会携带快启选项,服务端会生成一个 cookie 并将其发送至客户端,客户端会缓存该 cookie,当其与服务端重新建立连接时,它会使用存储的 cookie 直接建立 TCP 连接,服务端验证 cookie 后会向客户端发送 SYN 和 ACK 并开始传输数据,这也就能减少通信的次数。
-
客户端发送SYN包,包尾加一个FOC请求,只有4个字节。
-
服务端受到FOC请求,验证后根据来源ip地址声称cookie(8个字节),将这个cookie加载SYN+ACK包的末尾发送回去。
-
客户端缓存住获取到的cookie 可以给下一次使用。
-
下一次请求开始,客户端发送SYN包,这时候后面带上缓存的cookie,然后就是正式发送的数据。
-
服务器端验证cookie正确,将数据交给上层应用处理得到相应结果,然后在发送SYN+ACK时,不再等待客户端的ACK确认,即开始发送相应数据。
由于TFO存在诸如TFOcookie多长时间后删除,谁来维护和删除和一些安全隐患等问题所以没有大面积推广,这个不做过多讨论。之后有机会再单独和大家聊一下这个东西。
关闭连接 正常的关闭连接流程我们还是先来看正常的关闭连接流程,双方不管哪一方都可以主动发起关闭操作。TCP协议规定通过发送FIN段来发起关闭操作。我们先来看一下完整的流程:
-
TCP A发送一个FIN段指明接受者希望看到的自己当前的序列号K,FIN段还包含一个ACK段用于确认对方最近一次发来的数据,并且停止发送数据,此时TCP A进入FIN-WAIT-1状态。(FIN报文不管是否携带数据,都需要消耗一个序列号)
-
TCP B将序列号K的数值加1作为ACK的值,表明已经接受到FIN,进入了CLOSE-WAIT状态。这个时候上层的应用程序会被告知连接的另一端已经提出了关闭的请求,接下来就是应用程序自己的操作了。(这个时候TCP A向TCP B的方向就释放了,但是TCP B向TCP A发送数据还是可以的,这个状态会持续整个CLOSE-WAIT状态)
1.这时候TCP A接收到到来自TCP B的应答报文后,进入FIN-WAIT-2状态,等待TCP B发送连接释放报文,在这个过程中是要处理来自TCP B发过来的消息的。
-
TCP B发送完最后的数据之后,就发送自己的FIN报文,该报文的序列号为L,然后进入进入了LAST-ACK状态。
- 这时候TCP A接收到到来自TCP B的连接释放报文后,状态进入TIME-WAIT,并不是马上释放,等待2MSL之后才会进入CLOSED状态。
-
TCB A为了完成关闭连接,最后发送的报文段还包含一个ACK = L + 1用于确认上一个FIN。
- TCP B只要收到了TCB A发出的确认,立即进入CLOSED状态
值得注意的是,如果出现FIN丢失,那么发送方将重新传输知道接收到一个ACK确认为止。
TCP A TCP B
1. ESTABLISHED ESTABLISHED
2. (Close)
FIN-WAIT-1 --> --> CLOSE-WAIT
3. FIN-WAIT-2 <-- <-- CLOSE-WAIT
4. (Close)
TIME-WAIT <-- <-- LAST-ACK
5. TIME-WAIT --> --> CLOSED
6. (2 MSL)
CLOSED
Normal Close Sequence
和启动不同,关闭一个TCP连接需要4个报文段。关于TCP连接关闭,有两个问题值得拿出来讨论的。
-
为什么客户端最后要等待一个2MSL的时间才进入CLOSED?
- 原因是保证客户端能处理“服务端没有正常接收到客户端最后一个ACK报文”这种情况,如果客户端最后发送给服务端的应答报文丢失,那么服务端就是重新发送FIN+ACK报文请求断开连接,在正常情况客户端会在2MSL内收到这条请求,如果没收到就当服务端已经正常关闭。如果在2MSL内收到了服务器的重新请求,客户端就会重新应答,然后重新启动一个2MSL的计时器。
-
为什么挥手要四次?
- 我们仔细观察第二三次挥手,其实就是把第二次的握手一分为二了。我们回忆下建立连接的时候,服务器是在LISTEN的状态下,收到请求连接的SYN后,将ACK和SYN放在第二次握手的报文中发送给客户端的。但是在关闭连接的时候,服务器确认的ACK和服务器要断开连接的FIN是在两个不同的报文中处理的,所以就多了一次连接。那么为什么第二三次要拆开,原因是服务端收到关闭连接请求后,是要先处理完在等待发送的数据,然后才关闭SOCKET,中间这个时间可长可短,对于客户端来说可能会误以为报文丢失,引起不必要的重传,而且这样客户端本可以切换状态告诉上层应用,也变得要一直等待服务端响应。
和开启一样,关闭也有可能出现同时关闭的情况。下面先来看一下同时关闭的时序图。
同时关闭就相对比价简单,同时关闭和正常关闭需要交换的报文段数量是相同的,两者的却别在于报文段序列是交叉还是顺序的。
半关闭TCP半关闭其实是一些应用需要此项功能,本身这个功能并不是特别常见。这个需求顾名思义就是通信双方一方完成了数据发送的工作,然后发送一个FIN给对方,但是仍然希望对方在发送FIN给我之前我能收到来自对方的数据。在伯克利套接字API里面,正常关闭的API是close(),半关闭就是shutdown()。下面这里有瓣关闭的流程图,可以看到,客户端发起关闭的流程和正常关闭是一样的,中间的数据段传输过程是可以传输任意数量的数据段,知道另一方发送完数据,发送FIN给客户端之然后收到客户端的确认之后,整个连接才完全关闭。
初始序列号根据之前的内容,我们知道任何拥有合适的IP地址、端口号、符合逻辑的序列号以及正确的校验和的报文段都将被对方接收,这个时候就有一个问题,TCP报文段在金国网络略有后可能会存在延迟抵达和排序混乱的情况。为了解决这种问题,初始序列号就不能是简单的0或1,初始序列号的选择就变得非常关键了。
首先如果初始序列号是1,我们来看下会发生什么情况。C端和S端建立了连接,这个时候C端给S端发送了10个包,但是由于网络问题,这10个包在网络中滞留了。这个时候C端用相同的端口号和S端发起连接,然后发了5个包。接着,之前在网络中滞留的10个包到达了S端,由于IP地址,端口号,序列号这些统统都一样,所以服务端会当作合法包接收,然后回一个回应包给C端,这个包的确认号是10,但是对于C端只发了5个包,所以一切就乱套了。、
[rfc0793]给出的建议是The generator is bound to a (possibly fictitious) 32 bit clock whose low order bit is incremented roughly every 4 microseconds.ISN的生成器绑定到一个32位的虚拟时钟上,每4微妙递增一次。对于同一个连接的两个实例而言,新的序列号不能出现重叠的情况。其实从这里就可以看出,TCP还是挺脆弱的,只要知道IP地址,端口号,序列号就可以伪造出合法的报文段。对于这种潜在的安全问题,要么是然序列号难以猜测,要么就是加密,加密我们之后会另行讨论。
TCP选项前面我们说到,TCP头部包含了多个选项,下面我们就来讨论一下几个比较重要的选项。
MSS 最大段大小选项最大段大小指的是TCP协议锁允许从对方接收到的最大报文段。MSS指的是TCP数据报的长度儿不包括TCP头部和其他TCP选项。MSS是在SYN报文中指定的,如果没有指定,那么默认大小是536字节。这里要明确一点,MSS不是双方协商的结果,是一个单方面的限定数值。
SACK前面在聊滑动窗口的时候,提到过累计ACK确认,由于接收到数据是无序的,所以序列号也是无序的,这就会导致数据队列中***空洞***的出现。按照目前TCP的确认系统不是特别的好处理这种不连续确认的状况,目前是只有低于ACK number的包都被接收才进行ACK,out-of-order的片段只能是等待,这个时间窗口是无法向右移动的。为了解决这个问题,就引入了SACK(Selective Acknowledgment 选择性确认)。说简单点就是用于描述乱序数据,一般是接收方告诉发送方***空洞***是哪些。SACK的标准描述文件是[rfc2018]。
SACK作为一个TCP选项,想要使用必须是通信双方都同意使用,Sack-Permitted Option选项在SYN中携带。
TCP Sack-Permitted Option:
+---------+---------+
| Kind=4 | Length=2|
+---------+---------+
在两个端点都支持SACK时,检测到数据流中丢失数据包的对等方可以将此信息通知发送方。
TCP SACK Option:
Length: Variable
+--------+--------+
| Kind=5 | Length |
+--------+--------+--------+--------+
| Left Edge of 1st Block |
+--------+--------+--------+--------+
| Right Edge of 1st Block |
+--------+--------+--------+--------+
| |
/ . . . /
| |
+--------+--------+--------+--------+
| Left Edge of nth Block |
+--------+--------+--------+--------+
| Right Edge of nth Block |
+--------+--------+--------+--------+
时间戳 防回绕序号
TCP最早是在[RFC1323]中引入的timestamp选项,并在[RFC7323]中进行更新。时间戳的作用一个有两个:
- 估算一条TCP连接的往返时间(round-trip-time,RTT),用于设置重传超时。
- 防止过期报文干扰正常通信 PAWS。
Timestamp作为一个TCP选项,在TCP首部占10个字节(8个字节用于保存两个时间戳,另外两个数值用于指明选项的数值与长度),具体如下图。
TCP Timestamps Option (TSopt):
Kind: 8
Length: 10 bytes
+-------+-------+---------------------+---------------------+
|Kind=8 | 10 | TS Value (TSval) |TS Echo Reply (TSecr)|
+-------+-------+---------------------+---------------------+
1 1 4 4
选项的核心数据是两个 32-bit 的时间戳字段.TSval 表示发送端发出该报文时的本地时间戳, 而 TSecr 则负责回放(Echo) 最近一次收到的对端报文中的 TSval 的值。**TSval **和 TSecr 在数值上并没有绝对的大小关系。TSval 是以本地的时钟为基准的, 而 TSecr 则是以对端的时钟为基准的。
以下是一个组典型的时间戳交互过程:
TCP A TCP B
----->
<----
----->
<----
启用 Timestamp 选项需要经过双方的协商,协商在三次握手时完成,如果协商成功,则在后续的报文中, 除了 RST 之外的所有报文均必须包含 Timestamp 选项。
TSval是以本地时间为基准,但是不一定要与真实时间相同,我们只需要保证TSval不要太快或者太慢就行。
- 不能太慢时因为如果tick的时间过长会影响RTT的计算结果,例如10s tick一定下,对于往返时间小于10s的连接来说,算出来的RTT甚至有可能是0。
- 不能太快是因为PAWS,因为TCP协议规定最大的MSL只有255s,那么一次时钟的循环必须大于这个值,32-bit的时钟tick必须大于59ns。否则就无法区分两个时间戳释放经历了时间戳回绕(这个下面会提到)。RFC 规定虚拟时钟的频率为每 1ms 到 1s 一个 tick。 按照 1ms 计算,32-bit 的时间戳回绕一次需要 24.8 天。
TSecr一般来说填入对端上一个报文的 TSval 就行,但是下面三种特殊情景需要注意一下:
-
Delay ACK,这个选项的本质是为了减少网络中的pure ACK,其实就是前面说道的延时应答机制,等报文积累到一定程度再进行应答(上一章提到过这个设计)。开启了Delay ACK后,当启用了Delay ACK后接收端收到多个报文的时候,就会出现多个时间,这个时候我们用最早的报文的 TSval,只有这样算出来的RTT才准确。
-
空洞 发送端发送了多个报文,但是由于网络原因出现了空洞,意味着可能发生了网络阻塞,这个时候我们最好是可以让接收方echo早一点的TSval,而不是序号最大的报文的TSval,这样的发送方计算出的RTT会偏大,发送报文就更保守,有利于减小阻塞。
-
空洞补上 这里有两种可能:
- 乱序的报文来迟了
- 收到的是重传的报文
不管是哪种,都要Echo当前报文的TSval,这样才能反映真实的网络情况。
算法
针对上面说的情景,在RFC[1323]中有设计出一套解决的算法可以处理上面问题的同时也能兼容正常场景。
- 连接状态增加两个32位的变量 TS.Recent 和 Last.ACK.sent。TS.Recnet保存下一个填入TSecr的时间戳。Last.ACK.sent保存上一个有序报文的ACK。在不启用delay ACK的时候,Last.ACK.sent 和 RCV.NXT是保持一致的。当启用delay ACK的时候,每收到一个有序报文,RCV.NXT就会向后推进,Last.ACK.sent 只会在真正 ACK 报文发送过后才更新。
- 如果收到的报文时间戳大于TS.Recent并且报文没有造成空洞,则立刻更新TS.Recent的值
if SEG.TSval >= TS.Recent and SEG.SEQ <= Last.ACK.sent then TS.Recnet = SEG.TSval
来看一下RFC[1323]中,针对不同情况给出的官方例子。
- 接收端启用了delay-ACK, 报文顺序按照A -> B -> C 到
TS.Recent Last.ACK.sent RCV.NXT
------------------->
1 A A->B
------------------->
1 A B->C
------------------->
1 A C->D
<----
(etc.) 1 A->D D
这个例子可以看到启用了delay-ACK之后,TS.Recent的值一直是收到的一个包的TSval = 1,Last.ACK.sent真正 ACK 报文发送过后才更新,RCV.NXT却是马上更新的。
- 未启用delay-ACK,报文乱序
TS.Recent Last.ACK.sent RCV.NXT
------------------->
1 A A->B
<----
1 A->B B
------------------->
1 B B
<----
1 B B
------------------->
1->2 B B->D
<----
2 B->D D
------------------->
2 D D
<----
2 D D
------------------->
2->4 D D->F
<----
4 D->F F
(etc.)
这个例子看C, D就行了,报文 C 到达后, 由于造成了接收端序号空洞,因此不会更新 TS.Recent, 报文 B 到达, 填上了空洞,因此 TS.Recent 更新,根据TS.Recent更新原则,这个时候需要用B的TSval更新到TS.Resent,后面D包到的时候也是同理。Last.ACK.sent 和 RCV.NXT都是在没有空洞之后才更新。
PAWS Protection Against Wrapped Sequences 防止序号回绕
先把PAWS的原理说给大家,PAWS本质就是利用***时间戳是单调递增***的这一数学特性来判断报文是否过期。
TSval是发送端发送报文的时间戳,因此如果SEG.TSval < TS.Recent,则说明这个报文时老旧的过期报文,可以丢弃。因为前面说过,TS.Recent必须是没有空洞的时候才会更新。假设报文SEG1在网络中滞留,然后发送端重传了SEG2。这个时候接收端先收到了SEG2,更新TS.Recent = SEG2.TSval,这个时候SEG1到了,因为SEG1.TSval < SEG2.TSval,所以SEG1.TSval < TS.Resent,因此根据规则SEG1被丢弃。
TCP高速连接的场景中,32位的序号有可能在短时间内用完,这时候如果没有时间戳就有可能会混淆两个序号相同的包。TCP Timestamp本质就是序列号的扩充。
有一个非常特殊的情况是,由于32位的时间戳回绕一次的时间是24.8天,所以如果一条TCP连接在24.8天之内都没有收到另一端的数据,这个时候TS.Recnet就会视作失效,接下来的第一个报文是不能用PAWS校验的,必须从第二个开始使用。但是这种情况比较罕见。
TCP Timestamp虽然有很多好处,但是有一个很明显的弊端就是,太长了。所以并不是所有的TCP都会使用Timestamp选项,例如windows就是默认关闭的,我们熟悉的Linux就是默认打开的。
用户超时选项用户超时(User TimeOut, UTO)指明了TCP发送者在确认对方未能成功接收数据之前愿意等待该数据ACK确认的时间。[RFC0793]中UTO是TCP协议本地配置的一个参数。用户超时选项允许TCP通信方将自己的UTP告诉对方,方便TCP接收方做出调整。但是UTO时建议性的,因为接收方不是一定要遵从发送方发过来的数值。
TCP状态转换 TCP状态转换图前面我们提到TCP是有状态的,下面我们就来看看TCP在各种状态之间的切换,首先来看看TCP的状态图。这里直接放出[rfc0793]里面给出的状态图。
Transmission Control Protocol
Functional Specification
+---------+ --------- active OPEN
| CLOSED | -----------
+---------+<--------- create TCB
| ^ snd SYN
passive OPEN | | CLOSE
------------ | | ----------
create TCB | | delete TCB
V |
+---------+ CLOSE |
| LISTEN | ---------- | |
+---------+ delete TCB | |
rcv SYN | | SEND | |
----------- | | ------- | V
+---------+ snd SYN,ACK / snd SYN +---------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd ACK | |
| |------------------ -------------------| |
+---------+ rcv ACK of SYN / rcv SYN,ACK +---------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / snd ACK +---------+
| FIN |<----------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
snd ACK +---------+delete TCB +---------+
------------------------>|TIME WAIT|------------------>| CLOSED |
+---------+ +---------+
TCP Connection State Diagram
Figure 6.
这个状态转换图画出了完整的TCP有限状态机的所有的状态转换。这个图将CLOSED状态用作开始状态点和终止状态点(这个其实并不能算作一个“官方”的状态)。
我们来大致拆解一下这个图。
- 建立连接部分
- CLOSED → SYN_SEND → ESTAB 这个客户端正常发起连接的流程。
- CLOSED → SYN_SEND → SYN_RCVD → ESTAB 这个是同时打开的流程。和上面的区别就在于 SYN_SEND后在收到自己的SYN + ACK之前先收到了对方的SYN报文。
- CLOSED → LISTEN → SYN_RCVD → ESTAB 这个是服务器正常监听的流程。
这里有两点是值得注意的。
一、 从LISTEN到SYN_SENT的状态转换是合法的,但是却不被伯克利套接字支持,因为很少有这种情况出现。FTP中是会出现服务端主动对客户端发连接。
二、从SYN_RCVD回到LISTEN的状态只有在SYN_RCVD状态是由LISTEN状态转换而来(并非从SYN_SEND而来)才是正确的。可能的场景是:我们执行一个被动打开操作,接收一个SYN,发送一个带有ACK的SYN进入SYN_RCVD,然后收到一个重置消息,这个时候就返回LISTEN状态。
- 断开连接部分
- CLOSE_WAIT → LACK_ACK → CLOSE 是比较典型的“服务端”关闭状态流程。
- FIN_WAIT_1 → FIN_WAIT2 → TIME_WAIT → CLOSE 这个是典型的“客户端”主动关闭状态流程。
- FIN_WAIT_1 → CLOSING → TIME_WAIT → CLOSE 这个是同步关闭的状态流程。
time_wait状态只有主动关闭的一方才有,time_wait状态是主动发起关闭一方收到对方的FIN包之后进入的状态,持续时间为2MSL,如果期间重新收到了对方的FIN包,则重发ACK包并重置计时器重新计时,直到2MSL后收不到对方的消息,则进入CLOSE状态。
关于time_wait我们一个个来说,首先是MSL是什么?MSL是Maximum Segment Lifetime,报文最大生存时间,这个时间是报文在网络中可存活的最长时间,一旦报文在网络中存在的时间超过了这个时间,则会被丢弃。IP头中的TTL也是限制报文存活的条件,TTL是IP数据报可以经过的最大路由数量。MSL和TTL一个是时间长度另一个数字。这里讲一个规则:MSL应该要大于等于TTL消耗为0的时间,因为这样才可以保证报文自然消亡。
说完了MSL的概念,我们来说一下TIME_WAIT状态为什么要出现,为什么不是主动关闭方收到对方的FIN包后发一个ACK包就直接关闭,还需要等待一个2MSL?为什么是2MSL,不是3MSL, 4MSL,8MSL?
首先我们来说一下,TCP四次挥手的时候,假设没有TIME_WAIT可能会遇到什么问题。
- 第一个原因在[rfc793]中有明确的描述,这里我直接上原文了。
TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.
简单来说就是要保证被动关闭方能收到最后的ACK包。
- 第二个原因是,当客户端发起关闭连接流程,在收到服务端的FIN包后发出对应的ACK包然后直接关闭,但是ACK包丢失了服务端在一定时间内没有收到客户端的ACK包重发了FIN包,这个时候客户端马上又发起连接,就会收到上一次服务端发过来的FIN包。虽然组成socket的四元组一样,但是却不是上一次的连接了。或者说有一个网络包在网络中堵住了,然后这个时候客户端和服务端在MSL内完成了关闭,然后又发起连接,这个时候就会有存在网络中的旧包干扰到新的连接。
总的来说TIME_WAIT最大的作用就是:1、最大程度保证正常关闭。2、防止旧包干扰。
好了,当我们确定需要TIME_WAIT之后,TIME_WAIT的时间要怎么定,又是一个值得探讨的问题。上面有两个原因,正常来说,就是看哪个需要消耗的时间长,我们取哪个就可以了。
ACK报文被B接收到。我们假设A发送了ACK报文后过了一段时间t之后B才收到该ACK,则有 0 < t <= MSL。因为A并不知道它发送出去的ACK要多久对方才能收到,所以A至少要维持MSL时长的TIME_WAIT状态才能保证它的ACK从网络中消失。同时处于LAST_ACK状态的B因为收到了ACK,所以它直接就进入了CLOSED状态,而不会向网络发送任何报文。所以晃眼一看,A只需要等待1个MSL就够了,但仔细想一下其实1个MSL是不行的,因为在B收到ACK前的一刹那,B可能因为没收到ACK而重传了一个FIN报文,这个FIN报文要从网络中消失最多还需要一个MSL时长,所以A还需要多等一个MSL。
TIME_WAIT至少需要持续2MSL时长,这2个MSL中的第一个MSL是为了等自己发出去的最后一个ACK从网络中消失,而第二MSL是为了等在对端收到ACK之前的一刹那可能重传的FIN报文从网络中消失。
重置报文段这里个有两个点需要注意的:
- 上面的例子中的server重发FIN包并不是等待到超时才重发的,server 会根据 net.ipv4.tcp_orphan_retries 的配置定时重发,直至收到 ACK 或重试次数到达上限,再关闭该连接。至于多久之内发送,是根据RTO算出来的,这个结果是会远小于MSL的。
- 其实2MSL并不能完全避免所有极端的情况,但是可以避免大多数情况的问题,2MSL可以算是一个权衡过后的值。
前面介绍TCP头部的时候有提到过RST位字段。该字段如果被启用,那么这个报文就叫做“重置报文段”。通常只有收到严重错误的报文才会产生重置报文,重置报文通常会导致TCP连接快速拆卸。
不存在接口先说一个比较简单的情况,如果TCP收到一个连接请求但是并没有相关进程对对应的端口进行监听时,就会产生重置报文。以前在讲UDP的时候其实也有类似的情况,UDP遇到这种情况就会产生一个ICMP目的不可达的消息。TCP就会用重置报文来代替。
终止一条连接前面我们提到,如果想终止一条连接,我们可以发送一个FIN包来走四次挥手的流程。我们称这种情况较有序释放。因为FIN是在之前所有排队的数据都发送出去后才被发送出去的,通常是不会出现丢失数据的情况。我们也可以通过发送一个重置报文段替代FIN来终止一条连接。通过重置报文段终止的方式我们称为终止释放。
终止一条连接可以为应用程序提供两大特性:
- 任何排队的数据都将被抛弃,一个重置报文段会被马上发出。
- 重置报文段的接收方会说明通信的另一端采用了终止的方式而非正常关闭。
API是会提供一种实现上述终止行为的的方式来取代正常的关闭操作。
时间等待错误我们回到刚刚的四次挥手,假设前面当客户端进入TIME_WAIT状态的时候, 客户端的下一个序列号为 K, 而服务器的下一个序列号为 L, 最近到达的报文段是由服务器发送至客户端, 它使用的序列号为 L-100, 包含的序列号为 K-200, 客户端收到后认为的收到了旧报文段发送一个 ACK, 包含了最新的序列号 K, L, 服务器收到这个报文段后, 没有关于这条连接的任何信息, 因此发送重置报文段作为响应。这并不是服务器的问题,但是却会造成客户端过早进入CLOSED状态,所以有些系统规定当处于TIME_WAIT状态时不处理重置报文段。
小结本章从简单的讨论了TCP从建立连接到交互数据到断开连接的基本过程,还有中间一些异常状态的产生和对应处理。简单讨论了一些TCP选项以为TCP为什么需要这些选项。下一章和大家聊一下TCP的超时与重传。



