TCP 协议


# TCP 协议

# 基本认识

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

面向连接可靠的字节流

(TCP 三大特点)

  • 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的。
  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端。
  • 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。

# TCP 包头

TCP 包头比 UDP 复杂得多。

源端口号(16 位)目的端口号(16 位)数据序号(32 位)确认序号(32 位)首部长度(4 位)保留(6 位)URGACKPSHRSTSYNFIN窗口大小(16 位)校验和(16 位)紧急指针(16 位)选项

(TCP 包头示意图)

  • 源端口号目标端口号:可以知道把数据发给哪个应用。
  • 包的序号:用来解决网络包乱序问题。在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。
  • 确认序号:用来解决丢包的问题。指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收,如果没有收到就重新发送,直到送达。
  • 一些状态位:TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。
    • ACK(acknowledgement)是确认:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1
    • RST(reset)是重新连接:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
    • SYN(synchronous)是发起一个连接:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
    • FIN(finish)是结束连接:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
    • 其它还有 PSH(push)是传送,URG(urgent)是紧急等,跟本文接下来的内容关联较小,不做详细阐述。
  • 窗口大小:TCP 要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力,防止发送的太快或太慢。

总的来说,TCP 是尽可能地在自己的层面上保证可靠性。因为如果更底层的 IP 层面网络状况就很差,那么是没有任何可靠性保证的。对于 TCP 来说,就是 IP 层你丢不丢包,我管不着,但在我的层面上,可以通过不断重传,通过各种算法,努力保证可靠性。

# TCP 的三次握手

# 状态变化时序图

TCP 的连接建立,常常称为三次握手。在连接建立的过程中,双方的状态变化时序图就像这样:

CLOSEDSYN_SENTESTABLISHEDCLOSEDLISTENSYN_RCVDESTABLISHED客户端服务端SYN, seq=xSYN, ACK, seq=y, ack=x+1ACK, seq=x+1, ack=y+1数据传输

(三次握手)

一开始,客户端和服务端都处于 CLOSED 状态。

先是服务端主动监听某个端口,处于 LISTEN 状态。

  • 第一次握手:客户端主动发起连接 SYN,之后处于 SYN_SENT(同步已发送)状态。
  • 第二次握手:服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN_RCVD(同步收到)状态。
  • 第三次握手:客户端收到服务端发送的 SYNACK 之后,发送 ACKACK,之后处于 ESTABLISHED(已建立连接)状态,因为它一发一收成功了。

最后,服务端收到 ACKACK 之后,处于 ESTABLISHED(已建立连接)状态,因为它也一发一收了。

小贴士

在三次握手过程中:

  • seq 是数据包本身的序列号,这是为了连接以后传送数据用的。
  • ack 是对收到的数据包的确认,值是等待接收的数据包的序列号(也就是期望对方继续发送的那个数据包的序列号)。

在传递过程中它们是这样用的:

  • 第一次消息:客户端会随机选取一个序列号(x)作为自己的初始序号发送给服务端。
  • 第二次消息:服务端使用 ACK 对客户端的数据包进行确认,准备接收序列号为 x+1 的包,所以 ack=x+1。同时告诉客户端自己的初始序列号,就是 seq=y
  • 第三次消息:客户端告诉服务端收到了它的确认消息并准备建立连接,客户端自己此条消息的序列号是 x+1,所以 seq=x+1,而 ack=y+1 是表示客户端正准备接收服务端序列号为 y+1 的数据包。

# 保活机制

在大部分情况下,客户端和服务端建立了连接之后,客户端就会马上发送数据。

但如果客户端就是不发数据,建立连接后空着。我们在程序设计的时候,可以要求开启 keepalive 机制,即使没有真实的数据包,也有探活包。

另外,作为服务端的程序设计者,对于这种长时间不发包的客户端,可以主动关闭,从而空出资源来给其他客户端使用。

# 包的序号

三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是 TCP 包的序号的问题。客户端要告诉服务端,我这边发起的包的序号起始是从哪个号开始的,反之同理。

但是序号不能都从 1 开始,因为这样往往会出现冲突。比如建立连接后:

  • 客户端发送了 123 三个包,但是发送 3 的时候,中间丢了,或者绕路了,于是重新发送。
  • 后来客户端掉线了,重新连上服务端后,序号又从 1 开始,然后发送 2,此时压根没想发送 3
  • 但是上次绕路的那个 3 又回来了,发给了服务端。服务端自然认为,这就是下一个包,于是发生了错误。

因而,每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4ms 加一,如果计算一下,到重复需要 4 个多小时,那个绕路的包早就死翘翘了,因为我们都知道 IP 包头里面有个 TTL,也即生存时间。

小贴士

这里引申一个问题,客户端因为某种原因掉线后(比如拔掉网线几秒再插回去),原本的 TCP 连接还存在吗?

参考本系列的这篇文章:拔掉网线几秒再插回去,原本的 TCP 连接还在吗?

# 为什么是三次

这是一个常见的问题,既然 2 次,3 次,4 次都没法彻底解决问题,那为什么是 3 次呢?

因为三次握手恰好可以保证客户端和服务端对自己的发送、接收能力均做了一次确认:

  • 第一次,客户端给服务端发了 seq=x,但无法获知对方是否收到。这一次握手,让服务端知道客户端的发送能力没问题
  • 第二次,对方回复了 seq=y, ACK=x+1这一次握手,让客户端知道自己的发送和接收能力没有问题,但服务端只能知道自己的接收能力没问题
  • 第三次,客户端发送了 ACK=y+1,服务端收到后知道自己第二次发的 ACK 对方收到了。这一次握手,让服务端知道自己的发送能力也没问题

# TCP 的四次挥手

四次挥手的目的是为了确保数据能够完成传输,即客户端和服务端之间都完成了该接收和该发送的内容,防止有一方自顾自地中断「合作」。

# 状态变化时序图

TCP 协议专门设计了几个状态来确保断开连接时的稳定性,这个过程的状态时序图如下:

ESTABLISHEDFIN_WAIT_1FIN_WAIT_2TIME_WAIT(等待 2MSL)CLOSED客户端服务端ESTABLISHEDCLOSE_WAITLAST_ACKCLOSED数据传输FIN, seq=pACK, ack=p+1FIN, ACK, seq=q, ack=p+1ACK, ack=q+1

(四次挥手)

一开始,客户端和服务端都处于 ESTABLISHED 状态,双方正常进行数据传输。

  • 第一次挥手:客户端主动发送一个 FIN(结束)的报文,告诉服务端我已经没有数据要发送了,随后进入 FIN_WAIT_1(终止等待1)状态。
  • 第二次挥手
    • 服务端收到这个 FIN,返回确认报文 ACK,随后进入 CLOSE_WAIT(关闭等待)状态。
    • 客户端收到确认后,进入 FIN_WAIT_2(终止等待2)状态,等待服务器发送释放连接的报文。因为在释放连接请求发送前,客户端需要接收服务端发送的最后的数据。
    • 异常情况:接下来如果服务端直接断了,则客户端将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。
  • 第三次挥手:服务端发送最后的数据后,发送释放连接的报文(ACKFIN),然后进入 LAST_ACK(最后确认)状态,等待客户端确认。
  • 第四次挥手
    • 客户端收到释放连接的报文后,发出确认报文 ACK,表示知道你东西发完了准备结束了,然后进入 TIME-WAIT(时间等待)状态。
    • 这个时候客户端不确定最后的这个 ACK 有没有被服务端收到,所以 TCP 连接没有立即释放,等待 2MSL 时间过后,客户端才会撤销相应的 TCP 连接,进入 CLOSED 状态。
    • 异常情况:服务端没有收到客户端最后发的那个 ACK,则服务端会重发一次释放连接的报文,让客户端再发一次 ACK。中间这个 2MSL 时间要足够长。
    • 服务端在接收到确认后,立即撤销 TCP 连接,进入 CLOSED 状态。

小贴士

MSLMaximum Segment Lifetime,报文最大生存时间。它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文是基于 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

还有一个异常情况就是,服务端超过了 2MSL 的时间,依然没有收到它发的 FINACK,怎么办呢?按照 TCP 的原理,服务端当然还会重发 FIN,这个时候客户端再收到这个包之后,干脆就直接发送 RST,服务端就知道客户端早就断开了。

# 客户端延迟释放

上面说了 TCP 协议要求客户端最后等待一段时间 TIME_WAIT 后再释放,是为了确保最后一个 ACK 能够被服务端收到,因为如果服务端没有收到的话,会重新向客户端发一个 FIN,随后客户端也重新发一个 ACK

其实这个 TIME_WAIT 这个延时存在还有一个原因,就是如果客户端直接释放了,那它的端口就直接空出来了。但是服务端不知道,服务端原来发过的很多包很可能还在路上。如果客户端的端口被一个新的应用占用了,这个新的应用会收到上个连接中服务端发过来的包。虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来服务端发送的所有的包都死翘翘,再空出端口来。

# 为什么是四次

为什么是四次挥手呢?因为 TCP 是全双工的,全双工通信需要两方都确认关闭 —— 四次。

对于全双工来说,可以处于 Half-Close 状态:

  • 第一次:客户端对服务端说:「Hi,服务端,我要关了。」
  • 第二次:服务端回复:「好的,你的信息我收到了。」
  • 第三次:服务端回复:「Hi,客户端,我也要关了。」
  • 第四次:客户端回复:「好的,你的信息我也收到了。」

如果只发生了第一次和第二次,就意味着该连接处于 Half-Close 状态,此时客户端处于 FIN_WAIT_2 状态,服务端处于 CLOSE_WAIT 状态,客户端通往服务端的通道关闭了,但服务端通往客户端的通道还未关闭,仍然可以传输数据。

小贴士

  • 全双工:允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合。
  • 单工:就是在只允许甲方向乙方传送信息,而乙方不能向甲方传送。

# TCP 应用场景

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP 文件传输
  • HTTP / HTTPS

(完)