参考内容:

Transmission Control Protocol (TCP)

TCP - 12 simple ideas to explain the Transmission Control Protocol

TCP (Transmission Control Protocol) – What is it, and how does it work?

The Internet's Layered Network Architecture

《计算机网络-自顶向下方法(第7版)》

TCP (Transmission Control Protocol - 传输控制协议) 在过去 40 多年的时间里,一直是互联网通信的核心。在大学学习计算机网络时,大部分同学都只是简单机械的背诵 TCP 是面向连接的,是可靠的传输协议。本文借助 Wireshark 抓包工具分析 TCP 报文头部,来详细的介绍它的工作原理。

TCP 报文格式如下图所示,我们将重点关注 Sequence NumberAcknowledgment NumberWindow 几个字段,以及 TCP Flags 中的 ACKPSHRSTSYNFIN。后续的内容除了建立连接过程外,其它均与服务端客户端无关,即任何一方均可发起相关操作。

建立连接

在 TCP 建立连接时进行抓包,可以发现客户端首先向服务端发送了 SYN 包,服务端向客户端响应了 SYN, ACK 包,最后客户端向服务端发送了 SYN 包,这就是大名鼎鼎的三次握手

TCP 建立连接的三次握手过程,有四个事件发生:

  • 客户端→服务端:SYNchronize(同步)我的初始序列号 X
  • 服务端→客户端:我收到了你的 SYN,我 ACKnowledge(确认)我已经准备好接收 [X+1]
  • 服务端→客户端:SYNchronize(同步)我的初始序列号 Y
  • 客户端→服务端:我收到了你的 SYN,我 ACKnowledge(确认)我已经准备好接收 [Y+1]

为什么是三次握手而不是两次握手?

我们用打电话的场景来类比两次握手和三次握手的区别,可以发现在场景一中,B 以为通话已经建立,但其实可能 A 没有听到 B 的回复,而在场景二中双方都确认对方能够听到自己的声音,所以三次握手要更加可靠。

场景一:两次握手
A:喂,听得到吗?(SYN)
B:听得到,你听得到吗?(ACK)

场景二:三次握手
A:喂,听得到吗?(SYN)
B:听得到,你听得到吗?(SYN + ACK)
A:我也听得到!(ACK)

发送数据

TCP 连接建立后就可以发送数据了,数据发送过程主要关注 Sequence NumberAcknowledgment NumberWindow 几个字段。

Sequence Number 用于跟踪已经发送的数据,Acknowledgment Number 则用于跟踪已经接收的数据。比如客户端本次发送的数据包为 seq=1001 且数据长度为 200 字节,当服务端接收到这 200 个字节后,将回复 ack=1201 表示已经成功接收数据,下一次客户端发送数据的序列号将为 1201

如果接收方为接收到的每段报文都发送确认信息,那就意味着在线路上传输的报文数量翻了一倍,为了减轻数据传输的负担,TCP 加入了延迟确认机制。延迟确认机制规定每收到两段报文,或者距离最后一段报文的时间不超过 500ms,至少要回应一次 ACK。

超时重传

TCP 是如何处理丢包情况的呢?实际上每次数据发送时,客户端都会保留一份已经发送的数据副本到缓存,并启动重传超时。如果客户端收到服务端发送的数据包确认,则将缓存中的数据删除即可,因为可以确认服务端已经正确接收了数据包。

如果客户端一直没有收到已发送数据包的确认,重传超时时间一定会在某个时刻到期,此时客户端即会意识到服务端可能没有接收到数据,所以客户端会重新发送丢失的数据包,确保数据可以被服务端接收到。

超时重传机制的巧妙之处在于不管在哪个方向丢包,它都能正常工作。比如客户端正常发送了数据包,但是服务端的 ACK 包丢失了,当达到重传超时时间后,客户端会再次发送对应数据包,而对服务端来说即可猜出是 ACK 数据包丢了,服务端不会重复存储该数据包,只需要再次回应对应的 ack 即可。

流量控制

如果发送方一味的发送数据,而接收方可能正忙于其它事务,可能会读取数据相对缓慢,如此就会很容易地使接收缓存溢出。TCP 为应用程序提供了流量控制服务以消除发送方使接收方缓存溢出的可能性。流量控制因此是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。

从上图可以看出 TCP 通过 Window 字段来进行流量控制,通过该字段告知发送方在必须等待接收确认之前可以发送多少数据。即通过控制窗口大小来达到流量控制的目的。

数据包解析

将 Wireshark 抓取的 TCP 数据包原始字节数据复制出来如下所示,会发现里面不止有 TCP 报文的头部,还有以太网和 IPv4 的头部。

0000   74 5a 01 b5 29 e4 90 65 84 0b 69 8b 08 00 45 00
0010   00 32 1b bf 40 00 80 06 00 00 c0 a8 00 0a 08 9c
0020   53 29 6a b8 1a 0a 06 6e 20 43 47 9e 92 37 50 18
0030   00 ff 1c 9c 00 00 31 32 33 34 35 36 37 38 39 30



数据链路层:以太网头部(14 Bytes)
74 5a 01 b5 29 e4 90 65 84 0b 69 8b 08 00

目标 MAC:74:5a:01:b5:29:e4
源 MAC:90:65:84:0b:69:8b
类型:08 00 = IPv4



网络层:IPv4 头部 (20 Bytes)
08 00 45 00 00 32 1b bf 40 00 80 06 00 00 c0 a8 00 0a 08 9c 53 29

版本 + IHL:45 = IPv4,头长 20 字节
差分服务字段 (DSCP/ECN),通常为 0:00
总长度:00 32 = 50 字节(IP 头 20 + TCP 头 20 + 数据 10 字节)
标识符:1b bf,用于数据包分片重组
标志 + 分片偏移:40 00 = DF (表示不分片)
生存时间 TTL:80 = 128,表示数据包最多能经过 128 跳
协议:06 = TCP,即上层协议是 TCP
头部校验和:00 00,这里显示为 0,可能是抓包时未计算,或者故意置零
源 IP:c0 a8 00 0a = 192.168.0.10
目标 IP:08 9c 53 29 = 8.156.83.41



传输层:TCP 头部 (20 Bytes)
6a b8 1a 0a 06 6e 20 43 47 9e 92 37 50 18 00 ff 1c 9c 00 00

源端口:6a b8 = 27320
目标端口:1a 0a = 6666
序列号:06 6e 20 43 = 107,661,379, wireshark 显示的是相对序列号,所以不一样
确认号:47 9e 92 37 = 1,201,353,271,与序列号同理
数据偏移 (4位) + 保留 (3位) + 标志 (9位) (2字节):  50 18 -> 分解:
    数据偏移: 5 -> TCP 头部长度 5 个“32位字” (5 * 4 = 20 字节)。
    标志位: 0x018(二进制 0000 0001 1000):
        ACK = 1 (确认有效)
        PSH = 1 (推送数据,提示接收端应立即交给上层应用)
        RST, SYN, FIN = 0
窗口大小:00 ff = 65535
校验和:1c 9c
紧急指针:00 00 = 0,未使用



应用层:TCP 载荷数据(10 Bytes)
31 32 33 34 35 36 37 38 39 30

都是 ASCII 码,数据内容为:"1234567890"

从数据包的封装情况即可看出来对网络协议分层的好处,比如工作在链路层的交换机只需要解析出 MAC 地址即可进行数据转发,而不需要再解析网络层中的 IP 地址等信息。同理对工作在网络层的路由器来说,解析出 IP 地址即可转发数据,不需要对数据进行深度解析。

断开连接

TCP 有两种关闭连接的方式,一种是通过 FIN 来优雅的关闭连接,另一种是通过 RST 来暴力的关闭连接。我们首先来看优雅的关闭方式。

如果一切都进展的很顺利,客户端和服务端建立连接并成功的交换了彼此的数据,它们就可以关闭连接并继续进行各自的工作。与建立连接的过程类似,关闭连接的过程也会发生四个事件,即大家所说的四次挥手

  • 客户端→服务端:我已经FINished(完成)了数据发送,我最后的序列号是 X
  • 服务端→客户端:我 ACKnowledge(确认)接收到了你的 FIN 且 ack=[X+1]
  • 服务端→客户端:我已经FINished(完成)了数据发送,我最后的序列号是 Y
  • 客户端→服务端:我 ACKnowledge(确认)接收到了你的 FIN 且 ack=[Y+1]

和三次握手一样,中间两个事件可能在同一个数据包中发生,比如下面使用 Wireshark 工具抓到的包。

实际情况不会每次都将两个事件合并,这是因为四个事件并不是严格要求必须按照上述顺序进行的,很有可能客户端已经发送完数据并断开连接,但是服务端仍然有数据需要向客户端发送,所以可以只关闭客户端向服务端发送数据的通道,而不关闭服务端向客户端发送数据的通道。请记住 TCP 是全双工通信,一个 FIN-ACK 组合事件只会关闭一个方向的连接。

非优雅的暴力关闭连接方式采用 RST Flag 实现,任何一方都可以发送该连接关闭数据包,使用该方式关闭连接意味着 TCP 连接出现了问题,发送方向接收方发送了 RST 数据包后,即将该 TCP 连接相关信息清空,接收方接收到该数据包后也即将该 TCP 连接相关信息清空。