Roxy's Library

Back

本文内容基于 2025 秋季《计算机网络》课程讲述,如有差错,欢迎指正

TCP 协议概述#

TCP 在一对通信的进程之间提供面向连接、点对点、全双工、可靠有序的字节流服务。有着连接管理、可靠传输、流量控制和拥塞控制等机制

TCP 报文格式#

TCP 头部标准长度为 20 字节,加上选项 (Options) 最长可达 60 字节。

TCP 报文格式

下面是各字段的详细说明:

  • 源端口与目的端口:各 16 bits,用于复用与分用
  • 序列号:32 bits
    • TCP 是字节流协议,序号指的是本报文段数据载荷中第一个字节在整个字节流中的偏移量,而不是报文段的计数
  • 确认序号:32 bits
    • 期望收到的下一个字节的序号(累积确认机制)
    • 只有当 ACK 标志位为 1 时有效
  • 首部长度:4 bits。以 4 字节为单位,表示头部长度(最小值 5 = 20 字节,最大值 15 = 60 字节)
  • 保留字段:6 bits,保留供将来使用,必须置 0
  • 标志位:
    • URG:紧急指针有效(较少使用)
    • ACK:确认序号有效
    • PSH:接收方应立即将数据交付给应用层(较少使用)
    • RST:复位,强制断开连接(用于异常处理)
    • SYN:用于发起连接
    • FIN:用于终止连接
  • 接收窗口:16 bits。
    • 用于流量控制,通知发送方接收缓冲区还剩多少可用空间。
  • 校验和 Checksum:16 bits。和UDP一样
  • 选项 (Options):这里介绍几种常用选项
    • MSS (Maximum Segment Size):最大报文段长度,通常为 1460 字节(以太网 MTU 1500 - IP头 20 - TCP头 20)
    • Window Scale:窗口扩大因子,解决 16 位窗口字段不够用的问题
    • SACK:选择确认,允许接收方使用选择确认

TCP 连接管理#

建立连接:三次握手#

目标是确认双方都同意建立连接,并同步初始序列号和 MSS 等参数。

  1. 第一次握手 (SYN):
    • 客户端发送 SYN=1, Seq=x
    • 客户端进入 SYN_SENT 状态。
    • 不携带数据,但消耗一个序号
  2. 第二次握手 (SYN-ACK):
    • 服务器收到 SYN,分配资源。
    • 发送 SYN=1, ACK=1, Seq=y, ACKnum=x+1
    • 服务器进入 SYN_RCVD 状态。
  3. 第三次握手 (ACK):
    • 客户端收到 SYN-ACK,分配资源。
    • 发送 ACK=1, Seq=x+1, ACKnum=y+1
    • 客户端进入 ESTABLISHED 状态。
    • 可以携带数据

三次握手示意图

为什么需要三次握手(而不是两次)?

  • 防止失效的连接请求突然到达:如果客户端发送的第一个 SYN 滞留在网络中,延误到达。若只有两次握手,服务器收到后会错误地建立连接并等待数据,浪费资源。三次握手要求客户端再次确认,客户端会发现自己并没有请求该连接,从而不会进行响应
  • 双向确认:确保双方的发送和接收能力都正常。

客户端和服务器的状态机表示如下:

TCP 状态机

右侧为客户端状态机,左侧为服务器状态机

关于起始序列号的选择:

  • 序列号的选择应尽量随机,以防止旧连接的数据被误认为是新连接的数据(序列号重叠问题)
  • 早期 TCP 实现中,序列号每 4 微秒增加 1,现代实现通常使用更复杂的算法(基于密码学)来生成初始序列号

关闭连接:四次挥手#

TCP 是全双工的,每个方向需要单独关闭。

这里假设客户端先关闭连接:

  1. 第一次挥手:客户端发送 FIN=1, Seq=u,停止发送数据。进入 FIN_WAIT_1 状态
  2. 第二次挥手:服务器收到 FIN,发送 ACK=1, ACKnum=u+1。服务器进入 CLOSE_WAIT状态,客户端进入 FIN_WAIT_2。此时连接处于半关闭状态(服务器仍可发送数据,接收端只可回复ACK)
  3. 第三次挥手:服务器数据发完,发送 FIN=1, Seq=v。服务器进入 LAST_ACK状态
  4. 第四次挥手:客户端收到 FIN,发送 ACK=1, ACKnum=v+1
    • 此时客户端需要等待两倍的报文最大生存时间后才彻底关闭。

如果客户端结束后服务器也不再发送数据,第二次和第三次挥手可以合并成一个报文(即服务器同时发送 ACK 和 FIN)

四次挥手示意图

为什么需要客户端最后等待

  • 保证最后一个 ACK 能到达服务器:如果 ACK 丢了,服务器会重传 FIN,客户端需要能响应

握手协议的安全隐患#

SYN 洪泛攻击

TCP实现中,服务器在收到SYN段后,发送SYNACK段,分配资源。若未收到ACK段,服务器超时后重发SYNACK段。 服务器等待一段时间(称SYN超时)后丢弃未完成的连接

如果攻击者采用伪造的源IP地址,向服务器发送大量的SYN段,却不发送ACK段, 那么服务器为维护一个巨大的半连接表耗尽资源,导致无法处理正常客户的连接请求,表现为服务器停止服务

TCP端口扫描

  • SYN扫描:扫描程序向目标主机的各个端口发送SYN段,如果收到SYNACK段,说明该端口有服务运行
  • FIN扫描:发送FIN段,如果收到RST段,说明端口关闭;如果没有响应,说明有服务在监听(因为开放端口不会响应FIN段)

TCP 流量控制#

TCP接收端需要将接收到的数据存入接收缓冲区,供应用层读取。如果发送端发送数据过快或接收端应用消化数据过慢,可能导致接收端缓冲区溢出,从而丢失数据。 为防止这种情况,发送端TCP通过调节发送速率,不使接收端缓存溢出

UDP由于不保证可靠性,故不进行流量控制

发送方控制#

接收缓存中的可用空间称为接收窗口,记为 RcvWindow。其计算公式为:

RcvWindow=RcvBuffer(LastByteRcvdLastByteRead)RcvWindow = RcvBuffer - (LastByteRcvd - LastByteRead)

TCP 流量控制示意图

接收方通过 TCP 头部的 Receive Window 字段将当前的 RcvWindow 通告给发送方

发送方必须保证 LastByteSentLastByteAckedRcvWindowLastByteSent - LastByteAcked \leq RcvWindow,即发送方未确认的数据量不能超过接收方通告的窗口大小

零窗口通告#

零窗口问题

当接收方的缓冲区满时,RcvWindow = 0,接收方通过 TCP 头部通告发送方零窗口(零窗口通告),此时发送方必须停止发送数据

但是此时接收方无法发送 ACK 通知发送方缓冲区状态变化,因为未接收到数据

TCP 规定发送方收到“零窗口通告”后,可以发送“零窗口探测”报文段,从而接收方可以发送包含接收窗口的响应报文段

具体实现:

  • 发送端收到零窗口通告时,启动一个坚持定时器(Persistent Timer)
  • 定时器到期时,发送一个“零窗口探测”报文段(序号为上一个段中最后一个字节的序号)
  • 接收端在响应的报文段中通告当前接收窗口的大小
  • 若发送端仍收到零窗口通告,重新启动坚持定时器

糊涂窗口综合症

接收方腾出几个字节就通告,发送方只有几个字节的数据就发送,导致头部占比巨大(如 40 字节头传 1 字节数据)

  • 接收方策略 (Clark 算法):不通告小窗口。等到缓冲区有一半空闲或足够容纳一个 MSS 时再通告(可与推迟确认结合,都是希望间隔内有更多数据)
  • 发送方策略 (Nagle 算法):积聚足够多的数据再发送。如果有数据要发:
    • 若是新连接立即发送
    • 否则,若有未确认数据,将新数据缓存,直到收到 ACK 或数据大小达到MSS且窗口大小大于等于MSS再发。

TCP 拥塞控制#

网络内部是由大量的路由器或交换机组成,每个路由器或交换机上也有一个缓冲区。 如果说我们同时往网络里注入了大量的数据,网络内部的缓冲区也会导致溢出,产生丢包、时延增大等问题, 此时网络的带宽浪费在了重传丢包的数据上,导致网络吞吐量下降,甚至崩溃

对于网络中的拥塞问题,一种方法是在网络内部反馈信息进行调控,另一种方法则是采用端到端的调控机制,依赖于端系统对网络内部的状态进行观测,并判断拥塞。 下面介绍的拥塞控制或传统TCP的拥塞控制采用的是第二种方法

进行拥塞控制本质上要解决三个问题:

  1. 发送方如何检测网络拥塞?
  2. 发送方如何限制发送速率以避免拥塞?
  3. 发送方采取什么策略来调整发送速率?

拥塞感知#

发送方利用丢包事件感知拥塞:

  • 拥塞造成丢包和分组延迟增大
  • 无论是实际丢包还是分组延迟过大,对于发送端来说都是丢包了

丢包事件包括:

  • 重传定时器超时
  • 发送端收到3个重复的ACK(说明网络还能传 ACK,只是丢了个别包)

拥塞窗口#

发送方使用拥塞窗口cwnd限制已发送未确认的数据量:

LastByteSentLastByteAckedcwndLastByteSent - LastByteAcked \leq cwnd

注意,同时还要满足流量控制的接收窗口限制:

LastByteSentLastByteAckedRcvWindowLastByteSent - LastByteAcked \leq RcvWindow

拥塞控制算法#

总体上采用 AIMD(Additive Increase Multiplicative Decrease,加性增乘性减)策略来调整cwnd大小:

  • 加性增:若无丢包,每经过一个RTT,将cwnd增大一个MSS,直到检测到丢包。缓慢增大发送速率,避免振荡
  • 乘性减:发生丢包时,窗口减半。迅速减小发送速率,缓解拥塞

实际实现上拥塞策略由慢启动、拥塞避免、快速恢复3部分组成,近似实现AIMD

慢启动

对于新建连接,初始cwnd较小,如果使用加性增,达到可用带宽太慢

因此,前期可以采用一些激进的策略快速探测可用带宽,即慢启动算法

思想:

  • 初始时,cwnd 较小(比如1 MSS)
  • 每经过一个 RTT,cwnd 翻倍(指数增长)
  • cwnd达到慢启动阈值 ssthresh,进入拥塞避免阶段

实际实现:

  • 每收到一个 ACK,cwnd 增加 1个 MSS
  • 只要发送窗口允许,发送端可以立即发送下一个报文段

慢启动与拥塞避免示意图

拥塞避免

当cwnd增大到一定程度时,此时距离拥塞可能并不遥远,继续指数增长,容易导致拥塞。故在拥塞避免阶段,cwnd采用线性增长

实现:每当收到ACK,更新cwnd

cwnd=cwnd+MSSMSScwndcwnd=cwnd + MSS\cdot \frac{MSS}{cwnd}

此时小心翼翼地探寻带宽上限,但仍然不可避免会发生拥塞,产生丢包。但对于丢包后的处理,TCP有不同的版本

TCP Tahoe 与 TCP Reno#

TCP Tahoe 不区分丢包的类型(超时或3个重复ACK),统一处理,直接进入慢启动阶段

TCP Reno 在 TCP Tahoe 的基础上增加了快速恢复机制,区别对待不同类型的丢包

  • 发生超时:进入慢启动阶段
    • 将ssthresh降低至cwnd/2
    • 设置cwnd=1MSS
    • 使用慢启动增大cwnd至ssthresh
  • 收到3个重复ACK:进入快速恢复阶段

快速恢复

  • 将ssthresh降低至cwnd/2
  • 将cwnd降至当前cwnd/2+3
  • 采用新机制调节cwnd,直到再次进入慢启动或拥塞避免阶段

继续监听ACK:

  • 如果继续收到该重复ACK,每次将cwnd增加1个MSS
  • 如果收到新ACK,降低cwnd至ssthresh,进入拥塞避免阶段
  • 如果发生超时,进入慢启动阶段

快速恢复阶段cwnd的增长比拥塞避免阶段更快,有利于发送新数据

TCP发送端的事件与动作#

TCP 发送端状态机

TCP 的公平性#

如果N条 TCP 连接共享瓶颈带宽 R,每条连接获得 R/N 的带宽,才能保证公平性

TCP的公平性基于AIMD机制,我们下面以两个TCP连接为例进行分析:

图中横纵轴分别表示两个连接分配的带宽,蓝色线表示总带宽 R 的约束,红色45度线表示公平分配。 这里假设两个连接的一些塞控制相关的参数相同

对于加性增:其增长方向与45度公平线一致

加性增示意图

对于乘性减:其减小方向指向原点

乘性减示意图

对于AIMD :每次从任何一个点上开始,经过多次加性增和乘性减的交替作用,最终都会收敛到红线和蓝线的交点,实现公平分配

AIMD示意图

  • 局限性:
    • UDP 不受控,会压制 TCP 流量
    • 单个应用开启多个TCP连接会抢占更多带宽
TCP连接管理、流量与拥塞控制
https://astro-pure.js.org/blog/csnet_trans_chap3
Author GreyRat
Published at December 10, 2025
Comment seems to stuck. Try to refresh?✨