谈一谈 TCP 解决了什么问题,以及三次握手、四次挥手的细节,包括传输无误的流程以及每个环节出错的情况

如何保证可靠连接

在 TCP/IP 四层模型里,TCP 接收来自应用层的数据流,将其分割并封装为适当长度的 TCP 「报文」,通过 IP 层(网际层)传输数据。

TCP 是可以可靠传输数据的,也就是说,建立 TCP 连接的双方,能保证发出的信息一定被接收到。如何实现这一点呢?

  • 首先,TCP 的报文是按顺序发送的,且 TCP 会对报文中包含的数据的每个 Byte做编号。假设报文的数据序号从 1 开始(实际上起始序号可变,原因见下文 ),第 ① 段报文包含 1460 Byte 字节,其中每个 Byte 的编号就是 1,2,3,…,1460。
  • 发送给接收端,接收端收到报文后,用一段不携带数据的 TCP 报文(相当于只有报文头)确认,同时用确认号 1461(1460 + 1)来表示,自己受到了前 1460 Byte 的数据。
  • 接着,发送端发送第 ② 段报文,数据长度也为 1460 Byte,这样数据编号就变成 1461,1462,1463,…,2920.
  • 接收端收到报文后,确认的报文确认号就为 2921(2920 + 1)。
  • 如此往复…显然,接收端无需每次都回复,比如他收到 ①~⑤ 条报文段,由于报文段按顺序发送的缘故,只需要确认第 ⑤ 段即可。
  • 如果一共发送了五段报文,第 ③ 段接收端还没有收到,就收到了 ④、⑤ 段,说明第 ③ 段传输失败了。因此接收端只能确认收到第 ② 段。
  • 发送端得知最后一个未被确认的包是第 ③ 段,重传它。成功接收后,接收端就可以直接确认 ⑤ 段,因为 ④ ⑤ 已经收到。

img

TCP 标识了报文数据的顺序,从而接收端接收数据时可以重建顺序。关于上文说到的,在接收到一定量的连续字节流后才发送确认,这是一种 TCP 的扩展,被称为选择确认(Selective Acknowledgement)。选择确认使得接收端可以对乱序到达的数据块进行确认。乱序到达可能是因为包的乱序交付(由于网络延迟,第 ② 段报文比第 ① 段先送到),或者丢包。

可见,序号是保证可靠性的核心

然而上面的流程中,用 1 作为起始序号有安全隐患:第三方如果猜到序号,可以伪造一个 RST 报文,具体危害见这个回答
因此,序号需要动态随机生成(毕竟,从 0 开始猜 SN 和从一个随机数开始猜难度不一样),实际上它是由操作系统随机生成的 32 位长的序号。那么自然的,TCP 通信双方就需要在建立连接时,创建好初始序号,并同步给对方。

现在概念比较多了,我们用一些缩写来代指专有名词,并看看 TCP 报文的数据包接口是否符合需求:
SN(sequence number):序号,序列号,指 TCP 报文携带的数据中每个 Byte 的编号
ISN(Initial Sequence Number):初始的 SN,SN 的起点,在三次握手中同步
ACK(Acknowledge):确认,指接收端收到报文段后根据 SN 回复的行为。ACK 并不意味着数据已经交付了上层应用程序。
SYN(synchronization  /ˌsɪŋkrənaɪˈzeɪʃən/):同步,指一端告诉另一端自己的 ISN
FIN(finish):指断开连接

img

可见:

  • TCP 所在的传输层是建立、维护端口到端口的连接的,每个报文头中都有来源和目的端的端口号。
  • 每个报文段都带有序列号码,也就是 SN,用于标识报文的序号。注意,虽然说 SN 标识的是报文数据中连续的 Byte(1000,1001,1002…),但连个连续报文段的 SN 是不连续的,前一段可能是 1000,下一段可能就是 1500,也就是说前一段有 500 Byte 的数据。
  • 每个报文段还会带上 ACK number,除了第一次握手的 SYN 报文。
  • 报文头中有 9 个标志符,每个占 1 bit。
    • 上述的 ACK、SYN、FIN 三种操作都是通过对应的标识符置 1 来实现的。
    • RST = reset,当该位置 1 时,说明有严重差错,需要重新创建 TCP 连接。还可以用于拒绝非法的报文段和拒绝连接请求。
    • 另外 5 种不太懂…

三次握手

建立 TCP 连接,首先要做的是客户端和服务端让对方知道自己的 ISN。

下文表述中的发送端和接收端变成客户端和服务端。实际上客户端不一定是发送端,比如建立连接后服务端也可以向客户端发送 TCP 报文。

  1. 涉及到两个过程:a.客户端向服务端同步,b.服务端向客户端同步。
  2. 理论上两端同时初始化它们之间的连接是可能的,不过大多数情况下都是有先后顺序的:服务端先打开一个 socket 来监听另一端的连接(此时处于LISTEN状态)。服务端被被动打开后,客户端就能创建主动打开。
  3. 客户端生成 ISN(不妨记为 x),放在报文头的 SN 位置。先向服务端发送自己的 ISN,同时在报文头中把 SYN 置 1,向服务端表明这段报文是连接请求。
  4. 服务端正确收到报文,在本地保存客户端的 ISN。同时为了保证可靠传输,要向客户端发送 ACK 报文,其 ACK number 为 x + 1,以表明自己收到了客户端的 ISN,且值为 x
  5. 这样过程 a 就完工了,还有过程 b。
  6. 服务端向客户端发送自己的 ISN(不妨记为 y),同时在报文头中把 SYN 置 1。
  7. 客户端正确收到报文,在本地保存服务端的 ISN。同样是保证可靠传输的原因,向服务端发送 ACK 报文,其 ACK number 为 y + 1注意因为第一次 SYN 报文是带了 1 bit 数据的,所以这段报文的 SN 值为 ISN + 1,也就是 x + 1

每次发送报文的过程就成为一次「握手」。可见,服务端向客户端连续发送了两次报文,这是没有必要的,降低了传输效率。
上述过程称为「四次握手」,将服务端连续两次的握手合并,就得到了三次握手:

  1. 客户端发送 SYN 报文,SN = ISN,由CLOSED状态转为SYN-SENT状态。
  2. 服务端收到客户端的 SYN 后,向客户端发送 SYN/ACK 报文,带上 ACK number,SN = ISN,由LISTEN状态转为SYN-RCVD(RCVD = received)状态
  3. 客户端收到服务端的 SYN/ACK 后,向服务端发送 ACK 报文,带上 ACK number, SN = (ISN + 1),由SYN-SENT转为ESTABLISHED状态

img

之后,当服务端接收到 ACK,转为ESTABLISHED状态,TCP 连接建立成功

容错机制

三次握手如何确保双方稳定获取了彼此的 ISN 呢?考虑发送端握手失败的情况:

客户端没收到自己 ISN 的 ACK,得知第一次握手失败

两种可能

  • 服务端收到 SYN 了,但 ACK 报文发送失败了:信息似乎同步成功了,只是客户端不知情。
  • 服务端根本没收到 SYN:很严重,可靠性受到威胁。

客户端没法判断究竟是哪种情况,必须处理最坏的情况,也就是服务端没收到 ISN。没收到咋办呢?周期性超时重传 SYN。

服务端没收到自己 ISN 的 ACK,得知第二次握手失败

两种可能

  • 客户端收到 SYN,ACK 报文发送失败
  • 客户端没收到 SYN

同样的,服务端必须周期性超时重传 SYN/ACK 报文。

但是考虑此时的客户端状态:
客户端发送完 ACK 报文后,就转为 ESTABLISHED 状态,单方面认为三次握手成功,准备收发数据了。

第三次握手成功与否,或者说服务端是否接收到这次 ACK 报文,客户端是无感知的。

此时:

  • 服务端会周期性超时重传 SYN/ACK(默认五次),直到正确收到客户端的 ACK,或者超过最大时限,转入CLOSED状态
  • 客户端如果有数据发送,并成功送达服务端:
    • 服务端已经进入CLOSED状态,则会以 RST 报文回应。
    • 服务端还在SYN_RCVD状态,服务端会正常收到数据 + 期望中的 ACK number,相当于还是成功接收到 ACK 了,第三次握手成功,服务端也转为 ESTABLISHED 状态。

服务端在SYN_RCVDCLOSED状态下的行为
img
img
图片源自《TCP/IP 协议族》

可见,第三次握手中客户端发出的 ACK 是“不可靠的”。这次客户端没能保证自己的信息被服务端成功接收。不过由于第一次 SYN 之外的全部报文中 ACK 都置 1 这个设计,规避了不稳定的隐患。

那么需要对第三次握手的 ACK 再做 ACK 吗?

不需要。如果说需要对无数据的报文进行 ACK,则会进入互相 ACK 的死循环:
服务端收到 ACK,如果为了让客户端知道自己收到了 ACK,再次 ACK,客户端又收到 ACK,需要再次 ACK,服务端又 ACK…就没个头了。
所有 ACK 的发送方都不保证 ACK 的可靠性,由对方保证超时重传。
由此引入所谓的两军问题

TCP 对有数据的报文必须确认;不会为没有数据的 ACK 超时重传。
第三次握手的 ACK 报文就没有携带数据。发送出去之后不要求接收方返回 ACK。
此时有个问题,第二次握手的 SYN/ACK 被确认了,它带了什么数据?
TCP 设计者将 SYN 报文设计成占用一个字节的编号(可以理解为“消耗”了一个 SN)(FIN 标志位也是),也就是说 SYN 报文会携带一个 bit 的数据。因此,客户端会对第二次握手 ACK(服务端也会对第一次握手 ACK,也同理)。
体现这个原则的地方还有:

  • 在第三次握手中,SN = ISN + 1,这里的 1 就是第一次握手时 SYN 占用的 bit
  • 在四次挥手的第二次,服务端发送 ACK,这个 ACK 不带数据,也就不需再被客户端 ACK,也不要超时重传

如果没有第三次握手就建立连接,会怎么样?

  • 从交换 ISN 的角度,服务端无法确定客户端有没有成功接收到自己的 ISN,可靠传输无法保证。
  • 考虑这种场景:客户端发出第一次握手的报文(称为 A),这段报文在网络节点中滞留时间过长。
    服务端因为没有收到报文而不作反应;而客户端会在超时重传机制下重发 SYN 报文。
    一段时间后,滞留的报文终于送达服务端(假设服务端处于LINTEN状态)。如果服务端发送 ACK/SYN 后,不等第三次握手就直接进入ESTABLISH状态,将导致不必要的错误和资源浪费,毕竟此时客户端没有建立连接的意图,不会进行数据传输。

四次挥手

通过发送FIN报文,A 端可以向 B 端发出断开连接的请求。

img

  1. A 端发送 FIN,其中 SN = x。从ESTABLISHED状态转为FIN-WAIT-1状态
  2. B 端接收到报文,回复 ACK,其中 ACK number = x + 1。从ESTABLISHED状态转为CLOSE-WAIT状态

A 端接收到 ACK,会将状态从FIN-WAIT-1转为FIN-WAIT-2
由于是 A 端主动发起的请求,A 端本身肯定是没有数据需要再发送到 B 端了。但 B 端可能是在传输数据的途中接收的 FIN。
因此 B 端在回复 ACK 之后,还得等自身数据发送完毕,再发送 FIN 给 A 端,意为告诉 A 端可以断开连接了。

  1. B 端发送 FIN,其中 SN = y,从CLOSE-WAIT状态转为LAST-ACK状态
  2. A 端收到报文,回复 ACK,其中 ACK number = y + 1,由于刚才的 FIN 消耗一个 SN,这次 SN = x + 1
    这样就可以关闭连接了吗?注意,第四次挥手是个不带数据的 ACK 报文
    我们遇到了和第三次握手一样的问题,这个 ACK 报文如果发送失败怎么办?
  • 基于 TCP 可靠连接的原则,B 需要知道自己的 FIN 被 ACK 了,从而得知「A 已经知道所有数据都已送达」这件事。正确收到 ACK 可以保证 B 端正常进入CLOSED状态,避免(多次超时重发等)浪费资源,尤其是 B 端通常为服务端。
  • 传输层是维护端口到端口的连接的,一个 TCP 连接对应两端的端口号(从报文头中也能体现)。
    假设 A 端发送 ACK 之后立即进入CLOSED状态,本地 socket 连接的四元组(A IP、A 端口号(不妨设为x)、B IP、B 端口号)被释放,这时端口x立刻进入可用状态。考虑这种情况:A 端 ACK 后立刻创建了一个新的 TCP 连接,A 操作系统随机分配的端口恰巧(无巧不成书嘛)也为 x,这时旧的 TCP 连接的 B 端可能会发来一些报文(比如没有收到 ACK 而超时重传的 FIN),由于端口相同,新的 TCP 连接会成功收到这个报文,造成无法预期的混乱。
    可见,在 A 端第四次挥手之后,本次 TCP 连接占用的端口还需要一段时间不能使用,避免新旧 TCP 连接在同一个端口中产生混淆。这段时间要长到能保证本次连接产生的所有报文段都从网络中消失
    TCP 报文有一个最大生存时间(MSL = Maximum Segment Lifetime)的概念,即任何报文段被丢弃前在网络内的最长时间。根据一些推算,在 2MSL 的时长内,本次连接产生的所有报文段都会从网络中消失。
  1. A 端转为TIME-WAIT状态,等待 2MSL 的时间都没有收到其他报文后,可以转为CLOSED状态。而对 B 端,收到 ACK 就可以直接断开连接了,状态从LAST-ACK转为CLOESD

    可以看到,服务端(B 端)结束 TCP 连接的时间要比客户端(A 端)早一些。

最后一次握手/挥手都面临接收到预期之外的报文的问题:

  • 第三次握手可以避免预期之外的 SYN 报文送达服务端,导致服务端直接建立连接的情况。
  • 第四次挥手之后的 2MSL 等待可以避免旧连接中的报文出现在建立于同一端口的新连接中。

Q & A

三次握手中,为什么除了第一个握手报文 SYN 除外,其它所有报文必须将 ACK = 1

RFC793 明确规定,除了第一个握手报文 SYN 除外,其它所有报文必须将 ACK = 1。
TCP 作为一个可靠传输协议,其可靠性就是依赖于收到对方的数据,ACK 对方,这样对方就可以释放缓存的数据,因为对方确信数据已经被接收到了。
但网络传输中丢包是家常便饭,每次报文要尽可能的传输信息。比如每次报文中都“捎”上 ACK number。
这样应用的好处之一是,在第三次握手丢包时,客户端(ESTABLISHED)给服务端(假设是第二次握手后的SYN-RCVD状态)发送数据,其中的报文里带上了 ACK numebr,虽然服务端没收到第三次握手的报文,但还是正确收到了 ACK number,可以自然地进入ESTABLISHED状态。

第一、第三次握手可以携带数据吗

第一次不能,因为第一次握手时连接还没建立。如果服务端在第一次握手中就开辟缓存来容纳数据,会放大 SYN FLOOD 攻击,即攻击者伪造成千上万个携带大量数据的 SYN 报文,服务端就得开辟大量缓存来容纳巨额数据,内存很快耗尽导致拒绝服务。

第三次可以,因为能够发出第三次握手的主机一定不是伪造 IP,伪造 IP 主机是不会接收到第二次报文的。
因此第三次握手的主机应该是合法用户。

第三次报文携带数据发到服务端,处于SYN-RCVD的服务端会自然转为ESTABLISHED状态,走正常流程去接收就好。

为什么第四次挥手 A 端的等待时长为 2MSL?

时间分两段,第一段是第四次挥手的 ACK 报文的 MSL;第二段是服务端没收到 ACK 而重传的 FIN 报文的 MSL。
极端情况是如果在 ACK 到达的前一普朗克时间,服务端等不及就重发了 FIN,可以认为两端报文的传输时间是没有重叠的,加起来为 2SML
img

2MSL 能保证接收到服务端超时重发的 FIN 报文的最长期限,这段时间里都收不到,就可以认为当前网络里不存在这段报文了,即使存在也会失效,就避免了新 TCP 连接被这段旧报文干扰。

成功建立连接后客户端故障怎么办?

每次服务端收到客户端的数据后都会复位一个保活计时器,时长默认是 2 小时。直到计时器超时,都没有收到客户端的数据,服务端将会发送探测报文给客户端(默认发 10 次,间隔 75s)。如果探测报文也没有被回应,服务端得知客户端故障,主动关闭连接。

参考资料

TCP 握手的核心是 ISN
关于三次握手和四次挥手,面试官想听到怎样的回答?
附带 gif 的资料
维基百科
为什么 ISN 要动态随机
为什么 tcp 的 TIME_WAIT 状态要维持 2MSL