TCP (Transmission Control Protocol, 传输控制协议) 是应用广泛的传输层协议, 它是 TCP / IP 体系两个重要的协议之一, TCP 协议的标准文档为 1981 年发布的 RFC 793, TCP 位于 OSI 模型体系的第四层, 其向应用层提供面向连接、可靠传输的服务, TCP 保证交付给应用层的数据不丢失, 不重复, 并按序交付, 是诸如 HTTP / SSH / FTP / SMTP 等应用层协议的基础, TCP 是一个相对比较复杂的协议, 为了实现可靠传输, TCP 设计了计时器 / 超时重传 / 滑动窗口 / 序号 / 确认等机制来保证端到端的可靠传输, 本文讨论 TCP 协议的设计与工作原理

25.1 什么是可靠传输

与 TCP 相对应的另外一个传输层协议是 UDP, 其向应用层提供不可靠的服务, 数据可能丢失, 可能出现差错, 可能顺序错乱, 如果需要在 UDP 的基础上实现可靠传输需要应用层做额外的措施, UDP 在通信之前不需要建立连接, 数据以 Datagram 的形式传送, 所谓可靠传输即是要保证交付给应用层的数据不丢失, 不重复, 并且按发送的顺序交付, 为了实现可靠传输, 需要设置如下机制:

  • 对 PDU (协议数据单元) 设置序号, 因为网络协议的设计都是建立在网络本身是不可靠的假设下的, 因此无法保证数据一定按序到达, 此时接收方需要根据序号对 PDU 做重排序

  • 双方应设置对 PDU 的确认机制, 接收方在收到 PDU 后应向发送方发送确认信息, 让发送方知晓数据没有在链路上丢失

  • 发送方应设置发送缓存和计时器, 当某个已发出的 PDU 在设定的时间内没有收到对方的回复, 可以认为该数据已丢失, 此时需要发起重传

  • 应设置流量控制机制, 由于通信双方对数据的处理能力可能有差别, 当发送频率过快时, 接收方来不及对数据进行处理, 大量的数据堆积将造成缓冲区溢出, 因此需要设置流量控制协调双方的收发, 使得发送方不至于发送地过快而使接收方来不及处理, 也避免发送方发送速度过慢造成链路利用率不高

为了达到以上目标, TCP 设置了面向连接的数据传输机制, 通信双方在使用 TCP 进行数据传输之前, 首先需要建立 TCP 连接, 同时通信双方分别设置发送缓存和接收缓存用于暂存已发送或已接收的数据, 除此之外, 通信双方都设置有计时器, 当在设定的时间内没有收到对方的确认后将重传此前已发送的数据, TCP 数据传输的基本单位称为 TCP Segment, 它由 Header 和 Payload 两部分构成, TCP 在 Header 中设置诸如 段序号 / 确认号 / 校验和 等字段用来实现可靠传输

25.2 TCP Segment 的结构及其字段语义

TCP Segment 的 Header 结构如下所示:

各个字段的语义如下:

  • Source Port, 长度为 16 比特, 发送端的端口

  • Destination Port, 长度为 16 比特, 接收端的端口

  • Sequence Number, 长度为 32 比特, Segment 的序号, 在通常的 TCP 实现中 (如 4.2 BSD) 在进行 TCP 通信时, 都会随机生成一个起始序号 (Initial Sequence Number, 缩写为 ISN), 然后在此基础之上, 每发送一个 Segment, 序号便增加一

  • Acknowledgment Number, 长度为 32 位, TCP 的确认号, 确认号用于实现对 Segment 的确认机制, 确认号是接收方当前已收到并校验无误的最大序号加一, 即确认号等于 N 代表接收方已经收到了包括 N - 1 在内的所有 Segment, 期望接收序号为 N 的 Segment

  • Data Offset, 长度为四比特, 该字段以四字节为单位指示 Segment 的 Payload 部分的起始位置相对于 TCP Segment 的起始位置的偏移量, 即若 Data Offset 等于 N 表示 TCP Payload 相对于 TCP Segment 的起始位置的偏移量为 N * 32 比特, 由于该字段的长度为四比特, 因此 TCP Segment 的最大 Header 长度为 60 Byte

  • Reserved, 长度为 6 比特, 保留字段, 使用时应当置 0

  • URG, 长度为 1 的标志位, 该字段指示紧急指针, 当 URG = 1 时表示当前的 Segment 为高优先级, 应优先发送当前的 Segment

  • ACK, 长度为 1 的标志位, 当 ACK = 1 时, 确认号 (Acknowledgment Number) 字段有效

  • PSH, 长度为 1 的标志位, PSH 用来告知接收方此 Segment 应尽快交付给应用层, 因为从 TCP 及其往下的网络协议栈通常都是由内核实现的, TCP 将接收到的数据交付给应用层需要将内核空间的数据拷贝到用户空间, 这是一个比较耗时的操作, 通常 TCP 的实现都会等待数据达到一定数量再交付给应用层, PSH 标志可以通知接收方尽快将 Segment 交付给应用层

  • RST, 长度为 1 的标志位, 用于释放连接, 当 RST = 1 时通常说明网络发生了严重错误, 此时应立即断开连接并重新连接

  • SYN, 长度为 1 的标志位, 用于在连接建立的握手阶段来同步序号, 当通信方发起 TCP 握手时, 应设置 SYN = 1 及 ACK = 0, 对方若同意握手请求, 则将响应 Segment 中的 SYN 设置为 1, 并将 ACK 设置为 1

  • FIN, 长度为 1 的标志位, 用于释放 TCP 连接

  • Window, 长度为 16 比特, 该字段指示发送方自身的接收窗口, 例如发送方发出的 Segment 的确认号为 N, 窗口为 M, 则表示接收方从 N 算起还可以接收 M 个字节的数据, Window 用来实现 TCP 端到端的流量控制

  • Checksum, 长度为 16 比特, TCP Segment (Header 及 Payload) 的校验和, 用于接收方校验接收到的数据是否有差错

  • Urgent Pointer, 长度为 16 比特, 紧急指针, 该字段指示紧急数据的末尾在该 Segment 中的位置, 紧急数据都放在普通的数据之前, 因此该字段也可以理解为普通数据的起始位置

  • Options, 长度可变, 用于存放 TCP 的选项信息, 由于 Data Offset 的长度为 4 位, 其以 32 比特为单位指示 Payload 相对于 Segment 起始位置的偏移量, 因此 TCP Header 的最大长度为 60 Byte, 其中前 20 Byte 为固定 Header, 因此选项字段的长度上限为 40 Byte (选项字段可以存放的信息比较多, 例如可以存放时间戳, TCP Segment 的序号长度为 32 位, TCP 发送方会随机初始化一个 ISN, 一方面这个 ISN 本身就可能很大, 从而导致序号很快到达最大值而又绕回 0, 另一方面即便 ISN 设置为 0, 由于现在的网络传输速度很快, 可能很快就会把序号用到最大值, 当序号绕回 0 之后, 若此时又收到了此前没有收到的大序号的 Segment, 可能导致无法区分二者的新旧次序, 使用时间戳选项可以实现新旧 Segment 的区分)

  • Padding, 长度可变, 用于 TCP Segment 是 4 字节对齐的, Options 字段的长度可能不是 4 字节的整数倍, 此时使用 Padding 来填充, Padding 的值应设置为全 0

25.3 停止等待与连续 ARQ

要实现可靠传输, 确认与超时重传机制是必不可少的环节, 发送方在发送 Segment 之后, 不应立即删除 Segment 而是将其存放在发送缓存中, 当收到对方的确认后再删除该数据, 等待的时长需要设置上限, 当超过设定的阈值没有收到对方的确认后便要重传此前已发送的 Segment, 这称为 ARQ 机制, 即 Automatic Repeat reQuest, ARQ 有两种机制, 分别是停止等待 ARQ 和连续 ARQ, 停止等待 ARQ 指的是发送方在发送一个 Segment 之后便停下来等待接收方传回的确认, 直到收到接收方对上一个 Segment 的确认之后再开始发送下一个 Segment, 当超过给定时间没有收到对方确认之后便重传 Segment, 这种方式虽然实现了可靠传输但是效率比较低, TCP 使用效率更高的连续 ARQ 机制, 连续 ARQ 使用流水线的方式一次性发送多个 Segment, 而不必像停止等待 ARQ 那样每次都需要等待对方的确认, 连续 ARQ 一次性发送的 Segment 也有上限, 这受对方的接收窗口以及 TCP 的拥塞窗口控制 (在下面讨论)

25.4 TCP 滑动窗口

TCP 使用以字节为单位的滑动窗口, 滑动窗口主要用于流量控制, 通过滑动窗口可以协调发送方的发送速度, 避免发送方发送过快使得接收方来不及处理, 滑动窗口是一个比较形象的比喻, 滑动窗口可以由窗口前沿和窗口后沿来确定, 发送窗口的后沿之后的部分是已发送并已收到对方的确认的 Segment 集合, 对该部分的 Segment 可以从发送缓存中删除, 发送窗口的前沿之前的部分不允许发送, 在发送窗口的前沿和后沿之间的便是在未收到对方确认的前提下, 发送方可以发送的 Segment 的集合, 当这部分 Segment 发出并收到对方的确认之后, 发送窗口的后沿便可以向前移动, 接收端可以在对 Segment 的确认 Segment 中声明自己的接收窗口大小, 若接收窗口大小不变, 则当发送方发送 Segment 并收到对方确认之后可以将发送窗口整体都向后移动, 若发送方将位于发送窗口内的 Segment 都已经发出了但是一直没有收到接收方的确认, 则发送方的发送窗口将减小到 0, 此时发送方必须等待而不能继续发送新的 Segment

接收方可以采用累积确认, 如发送方连续发送 N, N + 1, N + 2 共 3 个 TCP Segment, 接收方可以只发送对 N + 2 的确认, 当发送方收到接收方对 N + 2 的确认 (即接收方收到的确认号为 N + 3) 后, 可以将发送窗口后沿连续向前滑动 3 个单位的距离

若接收方有能力处理更高的发送频率, 则接收方可将自己的接收窗口适当调大, 此时发送方可以连续发送更多的 Segment

25.5 TCP 连接建立 (三次握手)

当一方想要与另一方通过 TCP 协议进行通信时, 首先应建立 TCP 连接, TCP 连接的建立需要经历三次握手

  • 在 TCP 连接建立之前, 双方都处于 CLOSED 状态

  • 主动发起 TCP 连接的一方向另一方发送 SYN = 1, Sequence Number = k 的握手请求, 这是第一个握手, 此 Segment 发出以后, 主动发起 TCP 连接的一方由 CLOSED 状态转变为 SYN - SENT 状态

  • 被动打开的一方在收到另一方发来的握手请求后, 若同意建立连接, 则发送 SYN = 1, ACK = 1, Acknowledgment Number = k + 1, Sequence Number = m 的 TCP Segment, 当该 Segment 发出以后, 被动打开的一方由 LISTEN 状态转变为 SYN - RCVD 状态, 这是第二个握手

  • 主动打开的一方在收到另一方的握手响应之后, 发送 ACK = 1, Acknowledgment Number = m + 1, Sequence Number = k + 1 的 TCP Segment, 当该 Segment 发出以后, 主动发起 TCP 连接的一方由 SYN - SENT 状态转变为 ESTAB - LISTEN 状态, 这是第三个握手 (第三个握手消息可以包含有数据, 如果没有数据则不消耗序号, 即若该握手消息中没有携带数据, 则下一个 TCP Segment 仍可以使用值为 k + 1 的 Sequence Number)

  • 主动发起 TCP 连接的一方在发送第三个握手消息之后便进入连接已建立的状态了, 它可以开始向接收方发送正式的数据

  • 被动打开的一方在收到主动发起的一方发送的第三个握手消息之后也进入 ESTAB - LISTEN 状态, 此时也可以正式开始收发数据

为什么需要三次握手

如果只采用两次握手 (即一次请求与确认), 则可能会发生如下情况: 由于网络是不可靠的, 主动发起 TCP 连接的一方向另一方发起握手请求, 但该数据可能会丢失, 发送方在设定的时间内没有收到对方的握手响应, 于是重传握手请求, 双方进行正常的 TCP 通信并正常结束, 而实际上第一次发送的握手请求并没有丢失, 只是在链路上传递的时间过长, 当双方的 TCP 通信结束之后接收端又收到了此前没有到达接收端的握手请求, 其认为另一方再次发起了 TCP 握手, 于是发出握手响应, 如果只采用两次握手, 则被动打开的一方在发出握手响应之后便进入了 ESTAB - LISTEN 状态, 对它来说认为连接已经建立, 而实际上另一方并没有发起 TCP 连接, 从而造成不必要的资源浪费

25.6 TCP 连接释放 (四次挥手)

TCP 是全双工通信的可靠传输, 当连接建立以后, 双方可以同时收发数据, 因此 TCP 连接的释放也分两部分, 即释放 A → B 上的连接以及释放 B → A 上的连接, 假设 A 为首先发起连接释放的一方, 整个 TCP 连接释放的过程如下:

  • A 向 B 发送 FIN = 1, Sequence Number = k 的 TCP Segment, 当该 Segment 发出以后, A 由 ESTAB - LISTEN 状态转变为 FIN - WAIT - 1 状态 (第一次挥手)

  • B 收到 A 发来的第一次挥手 Segment 后, 向 A 发送 ACK = 1, Acknowledgment Number = k + 1, Sequence Number = m 的 TCP Segment, 当该 Segment 发出以后, B 由 ESTAB - LISTEN 状态转变为 CLOSE - WAIT 状态 (第二次挥手)

  • A 收到 B 发来的第二次挥手 Segment 之后, 便由 FIN - WAIT - 1 状态转变为 FIN - WAIT - 2 状态, 此时由 A → B 上的连接可以认为已经释放, 但 B 仍然可以给 A 发送消息

  • B 如果不想关闭连接, 可以持续正常地向 A 发送 TCP Segment, 假设在某个时间点上, B 也想关闭 TCP 连接, 则 B 向 A 发送 FIN = 1, ACK = 1, Acknowledgment Number = k + 1, Sequence Number = j 的 Segment, B 发出以后, 它由 CLOSE - WAIT 状态转变为 LAST - ACK 状态 (第三次挥手)

  • A 收到 B 发来的 FIN = 1 的 Segment 后, 向 B 发送 ACK = 1, Acknowledgment Number = j + 1, Sequence Number = k + 1 的 Segment, 该 Segment 发出以后, A 由 FIN - WAIT - 2 状态转变为 TIME - WAIT 状态 (第四次挥手)

  • B 收到 A 发送的第四次挥手消息之后, 便由 LAST - ACK 状态转变为 CLOSED 状态, 对 B 来说, TCP 连接已彻底释放

  • 但 A 仍需要等待一段时间, RFC 793 建议的等待时长为 2min * 2, 其中 2 min 是 MSL (Maximum Segment Lifetime, 即估计一个 TCP Segment 从发出以后在被接收之前在网络中存活的最长时间), 这里 A 在最后一次发出 Segment 之后仍需要等待 2 * MSL 才可以彻底释放连接

为什么 A 在发出最后一个 Segment 之后仍需等待 2 * MSL

一方面, 网络是不可靠的, A 最后发送的 Segment 可能没有到达 B, 对 B 来说, 当设定的时间内没有收到 A 挥手响应, 将会重传此前发送的第三次挥手 Segment, 如果 A 在发送完毕之后直接释放连接将会收不到 B 重传的消息; 另一方面, 与 3 次握手的设置相同, 等待 2 * MSL 可以保证本次 TCP 连接过程中的所有的 Segment 都从网络中消失, 避免旧的 Segment 对之后的连接产生影响 (举例来说, A 与 B 使用 TCP 进行通信, 双方正常收发数据, 通信完成之后释放连接, 主动关闭的一方没有在最后设置额外的等待时间, 而是直接关闭了连接, 这时可能会在网络中存在没有被交付的 Segment, 而此时 A 与 B 再建立新的 TCP 连接进行新一轮的通信, 此前在网络中没有交付的 Segment 可能会在本轮通信中出现, 而实际上这是上轮已关闭的连接中的 Segment, 为了避免这种情况, 设置 2 * MSL 的等待时间可以保证上一轮连接中的 Segment 都从网络中消失)

25.7 TCP 的丢包退让

此前已在 [Blog 7 - 可靠 UDP 的实现] 中讨论过关于 TCP 的丢包退让以及根据 RTT 设置等待时间的策略, 此处不再赘述


如果文章对您有帮助, 不妨请作者喝杯咖啡