TCP是面向连接的协议,因此TCP运输连接的建立和释放是每一次面向连接的通信中必不可少的过程。所以,一次完整的TCP通信就包括三个过程:连接建立数据传送连接释放

1. TCP的连接建立

TCP建立连接的过程叫做握手,握手需要在连接发起者(客户)和连接接收者(服务器)之间交换三个 TCP 报文段。因此人们常将这一过程称为三次握手

图中两个主机下面的方块表示它们分别处在什么状态。

最开始,客户和服务器都处于 CLOSED 状态,之后服务器为了接收外部的请求,于是处于 LISTEN 状态,等待客户的连接。如有,立刻做出反应。

之后客户发出请求连接报文,该报文首部中 SYN 为1,同时选择一个初始序号 seq=x。TCP 规定,SYN 报文段(即SYN = 1的报文段)不能携带数据,但要消耗掉一个序号。这时,客户进入 SYN-SENT(同步已发送) 状态。

服务器收到请求连接报文后,如果同意连接,则向 A 发送确认。在确认报文段中,SYN 和 ACK都置为1,确认号 ack=x+1,同时也为自己选择一个初始序号 seq=y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。这时TCP服务器进入 SYN_RCVD(同步收到)状态。

TCP客户收到服务器的确认后,还要再给服务器一次确认。确认报文段的 ACK 置1,确认号 ack=y+1,而自己的序号 seq=x+1。TCP标准规定,这个 ACK 报文段可以携带数据。但如果不携带数据则不消耗序号,在这种情况下,客户发送的下一个数据报文的序号仍是 seq=x+1。这之后,TCP客户进入 ESTABLISHED(连接已建立)状态。

当服务器接收到上一个确认的时候,也进入 ESTABLISHED 状态。

这是一个 SSH 的例子,SSH 是应用层协议,要先建立 TCP 链接,可以看到前三个数据包就是三次握手。前面提到的头部的各种控制字也能对应上。

为什么客户也要发送一次确认呢

这主要是为了处理过期的 TCP 连接请求。试想这样一种情况,客户端对服务器发送了一个 SYN 请求建立连接,但这个数据段在网络的某个节点滞留了,导致定时器超时,客户端重传 SYN 。这个 SYN 被服务端成功接收并建立起一条 TCP 链路,在完成数据传输后 TCP 链路断开。但这时,第一个 SYN 终于姗姗来迟,而服务器并不知道这个 SYN 其实已经被客户端认定为过期了,假设没有第三次握手,服务端会给客户端发送 SYN-ACK,然后建立对应的结构体分配资源,自以为建立了一条 TCP 链路。而客户端会忽略这个莫名其妙的 SYN-ACK ,所以这就会导致服务端一直单方面保持着这条 TCP 链路所需的各种资源,白白浪费。

而有了第三次握手就可以避免这种情况,例如服务端发送了 SYN-ACK 后如果半天没有收到第三次握手,就会释放对应的资源。

2. 三次握手的异常情况

客户端发送 ACK 后一直没有收到 SYN-ACK 的回应

这有可能是客户端的 ACK 在网络中丢失了,自然不会收到 SYN-ACK 的回应。于是客户端会在计时器过期后重传 ACK ,这样即可以解决异常。

但同样还有一种可能。我们说服务端的 TCP 为每一个处在监听状态的套接字维护两个队列,未完成连接队列和已完成连接队列。未完成连接队列中存储每个到达的 SYN 分节,表示已接收到 SYN 请求,正在等待三次握手完成,即等待客户端返回的 ACK 。而已完成连接队列中的每一项代表已经收到客户端返回的 ACK ,三次握手完成,TCP 连接已建立。客户端程序通过系统调用 accept() 取出已完成连接队列的一项。但这两个队列的长度是有限制的,即通过系统调用 listen() 的第二个参数控制。假如两个队列满了,则此时再次收到一个 SYN 后,服务端会直接将之忽略,这样客户端同样会半天收不到 SYN-ACK ,导致超时重传 SYN 。之所以服务端设计成这样忽略 SYN ,是因为队列满这个情况很可能是暂时的,等下一次客户端重传 SYN 来很可能队列就有位置了,所以直接忽略比较合理。

客户端发送 ACK 后收到 RST

这表示服务端的对应端口没有正在监听的套接字。操作系统收到 SYN 且对应端口并无进程在监听的话,就会返回一个 RST 。

客户端的第三次握手 ACK 丢失

客户端收到服务端的 SYN-ACK 后会进入 ESTABLISHED 状态,假如第三次握手的 ACK 丢失,会导致服务端的计时器超时,从而重传 SYN-ACK。而已经进入 ESTABLISHED 状态的客户端会忽略这个 SYN-ACK ,导致服务端不断重传,在一定次数的重传后,服务端会主动将这个连接关闭。但客户端认为这个连接已经建立,假如其向服务端发送数据,会得到 RST 的回应,从而感知到 TCP 连接出现了错误。

TCP 连接的释放

TCP 连接的释放也被称为四次挥手,下面就来详细看看:

如图,假设客户现在想要主动关闭 TCP 连接,则其主动发送一个 FIN 报文,其中 seq=u ,即等于之前使用的最后一个序号加一。此时 A 进入 FIN-WAIT-1 状态。TCP 规定,即使 FIN 报文不携带数据,也要消耗一个序号。

然后服务器收到 FIN 后,要确认这个报文,即发送一个 ACK 给客户,这个 ACK 的 ack=u+1,seq=v,即之前传输的最后一个序号加一。客户收到这个 ACK 后,进入 FIN-WAIT-2 状态,此时相当于客户到服务器的连接已经关闭。而服务器发送完 ACK 后进入 CLOSE-WAIT 状态,这个状态被称为半关闭状态,因为此时只是客户到服务器的连接关闭,而服务器到客户的连接并未关闭。也就是说此时服务器仍然可以给客户发送数据,且客户也必须得接收数据。注意这些报文段的 ack 一直等于 u+1 。

而等到服务器把自己的数据也发完了,就再给客户发送一个 FIN,表示我的数据也传送完了。此时服务器进入 LAST-ACK 状态,等待客户的确认。

客户收到服务器最后的 ACK 后,要再次发送确认。然后进入 TIME-WAIT 状态,注意此时 TCP 仍未释放,客户必须等待 2MSL 时间后才能释放 TCP 连接。MSL 被称为最长报文段寿命,RFC-793 建议 MSL 为 2 分钟,现实中可能更短一些。过了 2MSL 时间后客户才进入 CLOSED 状态。而服务器收到最终确认后也进入 CLOSED 状态。

那么问题就来了,这个 2MSL 是干嘛的呢?两个目的,第一,这其实是为了处理客户的最后一个 ACK 在网络中丢失这种情况的,假如客户的最终 ACK 在网络中丢失,而客户不等待直接进入 CLOSED 状态。那服务器只会发现没有收到客户的最终 ACK ,从而重传 FIN ,但客户已经 CLOSED 了,就会导致服务器永远处于 LAST-ACK 状态,浪费系统资源。而加上了 2MSL 时间后,客户可以等待是否还会受到重传的 FIN ,从而阻止了这种情况的出现。第二,记得前面说的第三次握手的作用吗,为了防止已失效的 SYN 再次传输到服务器。而这里挥手时等待 2MSL 也可以延长 TCP 连接时间,使得网络中的失效 SYN 尽快消失。

最后修改:2020 年 10 月 26 日
如果觉得我的文章对你有用,请随意赞赏