Skip to content

TCP

约 2287 字大约 8 分钟

2025-12-20

格式

image

端口信息

系统可以通过一个四元组唯一标识一条 TCP 连接。即(local_ip, local_prot, remote_id, remote_port) 在 TCP 的头部中就包含了源端口号和目标端口号两个信息 在 Linux 中,每一个 TCP 连接都是一个文件

序列号

^ddc131 在建立连接中由计算机随机生成初始值,每次数据发送都会累加一次序列号,保证唯一与有序

确认应答号

接收端发送该信号,表示下一次期望接收的数据的序列号,发送端接收到该信号后即可认为该序号之前的所有数据都已被成功接收

首部长度

长度 4 位,表示 TCP 报文头部的长度,其表示的长度单位并不是位,而是 32 位字(在格式中可以看到 TCP 数据大小为 32 位的整数)

字指的是由一些列比特组成的数据单位,例如 8 位字、16 位字和 32 位 字等

^3549ed 首部长度为 4 位,即最大能够表示的长度为 241=152^4 - 1=15,结合单位为 32 位字,那么可以推断出 TCP 头部的最大为 60 字节

TCP 字段中并没有数据长度的表示,如何得到数据的范围?

在 IP 包的头部中有[[IP#全长|IP 包全长]]字段以及其自身的[[IP#头部长度|头部长度]],因此可以简单通过一个公式得到 TCP 数据的长度 TCP数据长度=IP全长IP头部长度TCP头部长度TCP数据长度 = IP全长 - IP头部长度 - TCP头部长度

控制位

  • URG ,紧急数据指令,表示紧急指针有效,报文段包含高优先级数据
  • ACK:该位为 1 时,表明确认应答字段有效,TCP 规定除了最初建立连接时的SYN包之外该位必须设置为 1
  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接
  • SYN:该位为 1 时,表示希望建立连接,并在[[#^ddc131|序列号]]字段进行序列号初始值的设定
  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接,双方交换FIN为 1 的消息后断开连接
  • RST:该位为 1 时,表示用于重置一个混乱的连接,或拒绝一个无效的请求

窗口大小

表示接收窗口的大小,用于实现[[TCP机制#滑动窗口|滑动窗口]]流量控制

校验和

保存报文段的校验和,用于纠错。TCP 整个报文段都会参与校验和计算

紧急指针

仅在 URG 标志位开启时有效 在校验和之后的 16 位表示紧急指针,占用如此大资源的紧急指针有什么用?

紧急模式设计初衷

允许发送方通知接收方「某些数据比其他数据更重要」,正如邮件中携带「紧急」字样,邮局将会优先处理

紧急指针的计算

紧急指针表示从当前报文开始,向后为紧急数据的偏移量

  • 当前报文段序列号 = 1000
  • 紧急指针值 = 50
  • 紧急数据范围 = (1000, 1000 + 50 - 1)

应用场景

  • Telnet 或 Rlogin:用户按下中断键时,用户端发送紧急数据,使服务器立刻结束当前进程
  • FTP:用户中止文件传输时,使用紧急模式通知对方 实际应用中,紧急数据通常只有极少字节,用于一个「通知」

其他说明

紧急数据即便有了 URG 标志,但仍然嵌入在正常的数据流中,并不是出于「带外」(out-of-band) 通道中,并不能享受网络优先级。当接受方从数据流中正常读取紧急数据后,仅作为通知使接收方应尽快将此数据交付至应用层中 并不是所有的 TCP 实现和网络中间件对 URG 标志和紧急指针做处理,行为不一致且不可靠

选项

包含一些可选记录,总长度最多可达 40 字节 image

三次握手

假设客户端为请求连接的一方,服务端为响应请求的一方

  • 客户端发送 SYN,并进入 SYN_SENT 状态
  • 服务端收到 SYN,响应 SYN + ACK,并进入 SYN_RCVD 状态
  • 客户端收到服务端的 ACK,向服务端响应 ACK,并进入 ESTABLISHED 状态
  • 服务端收到客户端的请求后进入 ESTABLISHED 状态并响应数据,至此耗费 3 个 RTT 完成握手 对于多次建立连接的双方,在 Linux 中允许使用[[网络相关#绕过 TCP 三次连接|快速连接]]

四次挥手

假设客户端为主动发出结束请求的一方,即主动方

  • 客户端发送 FIN,进入 FIN_WAIT_1 状态
  • 服务端收到 FIN,响应 ACK,表示已知晓对方不再发送数据,并通知服务端的应用程序
  • 客户端收到 ACK,进入 FIN_WAIT_2 状态
  • 服务端的 read 调用返回 0,表明应用层已不再发送数据,向客户端发送 FIN,服务端进入 LAST_ACK 状态
  • 客户端收到 FIN 后,响应 ACK,并进入 TIME_WAIT 状态
  • 服务端收到 ACK,关闭连接
  • 客户端在 TIME_WAIT 状态下等待超过 2MSL 后关闭连接 image

三次挥手

四次挥手中,被动方会对主动方的第一次 FIN 响应一个 ACK,并在自身完成传输后向主动方发送一次 FIN。如果在主动方的 FIN 报文到达时,自身已经完成了传输,完全可以向主动方发送 FIN+ACK 报文,以此节约一次报文的开销。

  • A -> B FIN
  • B -> A FIN+ACK
  • A -> B ACK

为什么主动方需要保持 TIME_WAIT 状态?

  1. 防止历史连接中的数据在新的连接中被接收。
    • 假设在连接关闭前因为网络延迟导致数据的重发,而先行的数据已经到达,这时如果关闭连接且 TIME_WAIT 极短或不存在,而新的连接又马上建立,还是相同的 ip 和端口,这时那个迟到的重发数据到达了,并且将会被错误的处理。
    • 因此主动方在收到 FIN 后需要等待 2MSL 的时间,这一时长足以让两个方向上的数据包被丢弃,确保新的数据包一定是属于新连接的。(保证所有还在飞的子弹都落地了)
  2. 保证被动方能够正常的关闭连接,即主动方最后的 ACK 能够到达
    • 若被动方长时间没能收到自己发出 FIN 的 ACK,那么将会重发 FIN,若主动方提前的关闭连接,被动方将会收到 RST 并将其解释为一个错误,随后异常终止连接,而对于一个可靠协议而言这一行为并不优雅。(理解为作为可靠协议,通信双方必须知晓或者能够推断出对方的状态) 对 2MSL 进行拆解,第一个 MSL确保这个 ACK 有足够的时间抵达被动方,第二个 MSL确保最坏情况,即 ACK 到达的前一刻被动方重发了 FIN,这个重发的 FIN 需要至多 1MSL 到达主动方 那么设想一个极端场景,被动方持续没有收到 ACK,因此不断重发 FIN 直至重试上限,而在 2MSL 后仍有在途的 FIN,且此时相同的 socket 再次建立了连接,旧连接的 FIN 落入了新连接中。基于此情况,TCP 的序列号是随机的,且现代 TCP 默认开启时间戳选项,重发的 FIN 难以同时满足序列号窗口与时间戳

MSL 是什么?

全称 Maximum Segment Lifetime,即一个报文在网络中最长的生存时间,而挥手中的 2MSL 指的是允许报文丢失一次,即考虑最坏的情况:发送的 ACK 经过一个 MSL 后对方才重发 FIN,而重发的 FIN 也允许等待一个 MSL

Option

Window Scale

在 TCP 发明时,带宽都很小,因此窗口大小仅保留了 16 位,随着网络带宽的扩展,21612^{16} - 1的长度肯定是无法满足的 在三次握手时,将 Window Scale 放在 TCP 头中的 Options 中,可以在不修改 TCP 头的设计前提下向对方声明一个 Shift count,若 Shift count 为 n,那么意味着真实窗口大小的值为: TCP头中的窗口大小2nTCP头中的窗口大小 * 2^n

特性

TCP 是面向字节流的,它并不在乎负载的消息是一个完整的消息段,它只在乎数据被装入负载。就像卡车并不在乎货仓内是否只有一类货物,只在乎装满了就发车。因此会出现[[相关问题#TCP 沾包问题|沾包问题]],而需要在应用层中进行解决

参考

TCP 报文段格式

#review