技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTNET

TCP 协议详解(史上最全)

所属馆藏
NET Protocol
文件格式
Markdown
原始路径
NET Protocol/09-TCP协议详解 (史上最全)
本文目录40 个章节
  1. OSI 模型的七层框架
  2. 通信的原始时代
  3. 集线器的诞生
  4. MAC 地址
  5. 集线器的问题
  6. 交换机的诞生
  7. 二层交换机的问题
  8. IP 地址的诞生
  9. 子网和子网掩码
  10. 默认网关
  11. 路由表的由来
  12. ARP:从 IP 找到下一跳的 MAC
  13. 整个传输过程
  14. 参考网络拓扑图
  15. 数据封装和分用
  16. 不同物理网络之间的传输
  17. (一)源端口号
  18. (二)目的端口号
  19. (三)序号(Sequence Number)
  20. (四)确认序号(Acknowledgment Number)
  21. (五)首部长度(Data Offset)
  22. (六)保留位
  23. (七)控制标志
  24. (八)窗口大小
  25. (九)校验和
  26. (十)紧急指针
  27. (十一)可选项和填充
  28. Java 服务端监听示例
  29. Java 客户端主动连接示例
  30. 三次握手过程
  31. 四次挥手具体过程
  32. TIME-WAIT 与 2MSL
  33. 问题(1):为什么关闭连接通常需要四次,而建立连接只要三次?
  34. 问题(2):三次握手可以改成两次吗?
  35. 问题(3):为什么 TIME-WAIT 等待 2MSL?
  36. 问题(4):如果连接建立后客户端突然故障怎么办?
  37. 问题(5):为什么 ACK 和 FIN 有时能合并?
  38. 问题(6):TCP 是不是一次 send 对应一次 receive?
  39. 问题(7):TCP 连接建立和关闭是不是一定至少 7 个报文?
  40. 参考资料

TCP 协议详解(史上最全)

Category(分类): NET Protocol Status: 优

本文尽量保留原文从 OSI/TCP-IP 分层、物理层、交换机、路由器、IP、ARP、数据封装到 TCP 握手挥手的完整结构和插图,并依据 RFC 9293、RFC 6298、RFC 2018、RFC 7323、RFC 7413、RFC 791 等资料修正错误、补充现代说法。

TCP/IP 协议包含一系列协议,也叫 TCP/IP 协议族(TCP/IP Protocol Suite)。它不是一个单独的协议,而是一组围绕 IP、TCP、UDP 和应用层协议组织起来的协议集合。协议族约定了数据封装、寻址、传输、路由和接收等规则。

需要特别注意:TCP 是传输层协议,IP 是网际层协议;HTTP/1.1 和 HTTP/2 常运行在 TCP 上,但 HTTP/3 运行在 QUIC 之上,QUIC 再运行于 UDP 之上。不能把所有现代网络通信都简单等同为“TCP/IP 中的一条 TCP 连接”。

TCP/IP 协议的分层模型

在展开介绍 TCP/IP 协议之前,先介绍七层 OSI 参考模型。ISO 的 OSI 模型用于描述网络通信所需要的功能和层次,是一个参考模型,不代表现实中的所有协议栈都会严格按七层分别实现。

OSI 模型各层协议示例

OSI 模型的七层框架

OSI(Open Systems Interconnection,开放系统互联)参考模型描述了七层框架:

  1. 物理层;
  2. 数据链路层;
  3. 网络层;
  4. 传输层;
  5. 会话层;
  6. 表示层;
  7. 应用层。

每一层有相对独立的职责,并通过接口与相邻层配合。OSI 是 ISO 制定的通用参考模型,不是“所有公司必须实现的一套具体协议”;现实中的 TCP/IP 协议族通常把会话、表示等能力分布在应用协议、库、TLS、RPC 或序列化格式中。

TCP/IP 协议与七层 OSI 模型的对应关系

常见 TCP/IP 四层模型与 OSI 模型的关系如下:

TCP/IP 协议与 OSI 模型对应关系

TCP/IP 常见层次常见功能协议举例
应用层应用语义、消息格式和服务HTTP、DNS、SSH、SMTP、FTP
传输层进程到进程传输、端口、可靠性或数据报TCP、UDP、SCTP、QUIC
网际层/网络层IP 寻址、分组转发、路由IPv4、IPv6、ICMP、IGMP
网络接口层/链路层帧、链路地址、介质访问Ethernet、Wi-Fi、ARP、PPP

有些教材使用五层模型,把物理层从网络接口层中单独列出;这两种模型都可以帮助理解,关键是不要把模型当成协议必须遵守的边界。

(一)TCP/IP 协议的应用层

应用层包括直接为应用程序提供网络服务和数据格式的协议,例如:

  • HTTP:Web 和 API;
  • DNS:域名解析;
  • FTP:文件传输;
  • SMTP、IMAP、POP3:电子邮件;
  • SSH:安全远程登录和隧道;
  • DHCP:自动配置网络参数;
  • WebSocket、SSE、RTP 等:实时或流式应用能力。

HTTPS 通常可以理解为 HTTP 与 TLS 的组合。HTTP/3 的 HTTP 语义仍属于应用层,但其传输映射是 QUIC,而不是 TCP。

(二)TCP/IP 协议的传输层

传输层为不同主机上的应用进程提供通信抽象,常见功能包括:

  1. 通过端口号区分同一主机上的不同应用或服务;
  2. 为上层提供可靠字节流或无连接数据报;
  3. 进行序列控制、确认、重传、流量控制和拥塞控制(具体能力取决于协议);
  4. 将应用数据复用到网络层,并在接收端根据端口分用给正确的 Socket。

TCP 是面向连接、可靠、有序的字节流协议。它尽量将数据按序交付给应用,但连接断开、进程崩溃或超过重传限制时,未确认数据仍可能丢失。

UDP 是无连接的数据报协议,不保证送达、顺序和不重复。UDP 有校验和,但校验和不是可靠传输机制。QUIC 可以在 UDP 之上提供可靠流、拥塞控制、TLS 保护和连接迁移。

“TCP 效率低、UDP 效率高”只能作为粗略经验。实际效率还取决于消息大小、丢包、RTT、拥塞控制、系统调用、TLS、应用协议和网络路径。早期资料中把 QQ 聊天数据作为 UDP 小数据示例,也不应理解成现代即时通信必然使用 UDP。

(三)TCP/IP 协议的网络层

网络层在复杂网络环境中根据目标 IP 地址和路由表转发分组。它负责跨越多个链路和路由器,但不负责像 TCP 那样保证应用数据可靠到达。

常见协议包括:

  • IPv4、IPv6:寻址和分组转发;
  • ICMP、ICMPv6:差错报告、诊断和控制信息;
  • IGMP、MLD:组播成员管理。

路由器会根据最长前缀匹配选择下一跳或出口接口,并在 IPv4 转发中递减 TTL;如果没有可用路由,可能返回 ICMP 不可达,也可能由防火墙丢弃。网络层没有“把整个数据包直接从一个主机搬到另一个主机”的能力,而是依靠每一跳的链路层传输逐段转发。

(四)TCP/IP 协议的链路层

链路层处理相邻节点之间的帧、链路地址和介质访问,例如以太网、Wi-Fi、PPP。物理层负责电信号、光信号或无线信号的实际传输,常见 TCP/IP 四层模型会把链路层和物理层合在网络接口层中。

ARP 用于 IPv4 在局域网中解析下一跳的链路地址;IPv6 使用 ICMPv6 Neighbor Discovery,而不是 ARP。RARP 已经是历史协议,不应作为现代常见网络配置方式。

链路层的传输单位通常称为帧,物理层才更适合用比特、符号或信号来描述。原文把链路层和所有“电缆、光纤、连接器”完全等同,属于入门层面的简化。

图解物理层:使用链路地址解决设备识别问题

通信的原始时代

很久很久以前,两台电脑没有连接。后来它们各开了一个网口,用一根网线连接起来。

两台设备的物理连接

设备直接连接示意

网卡、驱动程序、收发器和物理介质共同完成信号发送和接收。应用程序通常不需要直接关心电信号如何被网卡转换;它通过 Socket API 交给操作系统协议栈处理。

物理信号传输动画

当第三台设备加入时,如果每台设备都与其他设备直接连接,网线数量会快速增长,维护成本很高。

三台设备直接互联

多设备网状连接

集线器的诞生

于是出现了集线器(Hub):所有设备把网线插到集线器,由集线器把收到的电信号重复到其他端口。

集线器示意

集线器工作在物理层附近,它不理解 Ethernet 帧中的目标 MAC 地址,收到信号后通常向其他端口重复。所有端口共享冲突域,可能发生碰撞,因此现代局域网通常使用交换机替代集线器。

“广播”在这里是入门类比,严格说集线器是重复信号;以太网广播帧则是目标 MAC 为 ff:ff:ff:ff:ff:ff 的链路层帧。

集线器重复信号

MAC 地址

为了让链路上的设备判断帧是否发给自己,以太网帧会包含源 MAC 地址和目标 MAC 地址。传统 EUI-48 MAC 地址通常为 48 位,例如:

AA-AA-AA-AA-AA-AA
BB-BB-BB-BB-BB-BB

以太网帧中的 MAC 地址

目标设备检查目标 MAC:匹配自己、组播或广播规则时接收,否则通常丢弃该帧。

目标 MAC 过滤

MAC 地址并不等于不可修改的全球身份证:设备可以使用本地管理地址、随机 MAC,虚拟网卡和容器也可以拥有不同的 MAC。MAC 地址只在相应链路或 VLAN 中有意义,不用于跨互联网路由,也不能单独作为安全身份认证。

图解数据链路层:使用交换机解决 MAC 地址转发问题

集线器的问题

如果能只把帧发送到目标 MAC 所在的端口,就可以减少无关设备接收和冲突。这就是交换机(Switch)的基本思路。

交换机只向目标端口转发

交换机的诞生

交换机通常工作在数据链路层,内部维护 MAC 地址表,把 MAC 地址映射到端口或端口组。

交换机工作示意

交换机通过观察进入帧的源 MAC 地址学习映射,而不是通过收到一个目标地址就直接知道来源:

交换机 MAC 地址表

假设 MAC 地址表为空,设备 A 发一个目标为 B 的帧:

未知目标 MAC 的帧

交换机从端口 4 收到源 MAC 为 A 的帧,于是学习:

MAC A → 端口 4

由于目标 MAC B 尚未在表中,交换机会进行未知单播泛洪,把帧发送到同一 VLAN 的其他相关端口。只有 B 会按目标 MAC 接收,其他设备通常丢弃。

交换机根据源 MAC 学习

B 回复后,交换机从 B 所在端口学习:

MAC B → 端口 1

MAC 地址学习过程

以后 A 发给 B 时,交换机可以按表项只转发到 B 所在端口:

  • 表项会随时间老化;
  • MAC 移动时,交换机可能更新端口;
  • 广播、组播和未知单播仍可能泛洪;
  • VLAN 会把一个物理交换网络划分为多个逻辑广播域;
  • 多交换机之间形成二层环路时,需要 STP/RSTP 或其他机制避免广播风暴。

多个交换机可以互联,但“直接把交换机连起来就永远没有问题”是不完整的,真实网络还要考虑 VLAN、链路聚合、生成树、环路、广播域和安全策略。

多个交换机互联

MAC 地址和端口映射记录

在简单的两个交换机拓扑中,交换机可以逐步学习到各个设备的 MAC 位置:

左侧交换机 MAC 表

右侧交换机 MAC 表

实际设备表项通常按 VLAN 区分,并不保证一直记录网络中所有主机。未知单播泛洪也不是“发送给整个互联网”,只发生在相应的二层广播域内。

图解网络层:IP 地址和路由器

二层交换机的问题

交换机依靠 MAC 地址学习和转发,适合局域网内的二层通信。但当网络规模增大、广播域扩大或需要连接不同网络时,仅依靠一个巨大的 MAC 表并不合适。

路由器在不同网络之间转发 IP 分组。路由器的每个接口通常有自己的链路层地址,但并非所有接口都使用 MAC,例如 PPP、隧道或其他链路类型可能有不同的寻址方式。

路由器连接不同网络

路由器不是简单“拥有一个 MAC 地址的转发设备”:它会查看 IP 首部、执行路由查找、递减 TTL、重新封装出接口链路帧,可能执行 ACL、NAT、QoS、分片或隧道等处理。

IP 地址的诞生

MAC 地址主要在链路内有意义,不能直接表达“某一组网络位于哪个方向”。因此需要网络层地址和前缀。

IPv4 地址为 32 位,常用点分十进制表示:

11000000.10101000.00000000.00000001
192.168.0.1

IP 地址结构示意

IPv6 地址为 128 位,采用十六进制分组表示。IP 地址通常表示接口或可达性,不是不可变的“主机身份证”:一台主机可以有多个地址,多个主机可以共享公网地址,私有 IPv4 地址也不能在公网直接路由。

主机配置 IP 地址

IP 与 MAC 的区别

  • MAC/链路地址:解决同一链路或 VLAN 中“下一跳帧发给谁”;
  • IP 地址:解决跨多个网络的分组寻址和路由;
  • TCP/UDP 端口:解决一台主机上“交给哪个传输端点或应用”。

在同一个局域网内,A 发给 B 时,Ethernet 帧的源/目标 MAC 是 A/B;IP 首部的源/目标 IP 也是 A/B。

同一局域网内的直接帧

如果 A 发给远端网络中的 C,A 会把目标 IP 保持为 C,但把 Ethernet 帧的目标 MAC 设置为默认网关的 MAC:

发送到路由器的第一段帧

路由器转发到目标网络的帧

在没有 NAT 的普通转发中,端到端 IP 源地址和目标地址通常保持不变;每经过一个路由器,二层帧会被拆掉并根据下一跳重新封装。NAT、隧道、代理和防火墙可能修改或重新建立不同的地址关系。

子网和子网掩码

主机需要判断目的 IP 是否在本地直连网络中。传统 IPv4 可以使用子网掩码;现代网络也常用 CIDR 前缀长度表示。

例如:

192.168.0.1/24

等价于子网掩码:

255.255.255.0

主机将自己的地址和目标地址分别与本地接口的前缀/掩码比较:

  • 如果网络前缀相同,通常认为目标在本地链路,解析目标的链路地址后直接发送;
  • 如果网络前缀不同,查路由表,选择默认网关或更具体的下一跳。

下面的 192.168.0.xxx 判断只适用于 /24 这个示例,不是所有以 192.168.0 开头的地址都天然在同一子网。

子网掩码计算示意

例如:

A:192.168.0.1 & 255.255.255.0 = 192.168.0.0
B:192.168.0.2 & 255.255.255.0 = 192.168.0.0
C:192.168.1.1 & 255.255.255.0 = 192.168.1.0
D:192.168.1.2 & 255.255.255.0 = 192.168.1.0

A/B 属于同一 /24 示例网络,C/D 属于另一个 /24 示例网络。实际是否同链路还受到 VLAN、接口配置、策略路由和地址类型影响。

子网划分示意

默认网关

如果目标不在本地子网,主机不能直接把 Ethernet 帧发给远端目标的 MAC,而是把 IP 分组交给本地链路上可达的下一跳,通常是默认网关。

默认网关是主机路由表中的一个下一跳地址。主机先通过 ARP(IPv4)或邻居发现(IPv6)获取网关的链路地址,然后:

  • Ethernet 目标 MAC:默认网关的 MAC;
  • IP 目标地址:最终目标主机的 IP(未经过 NAT 的普通路由场景);
  • 路由器收到后重新生成下一跳链路帧。

原文中“发给路由器”容易让人误解为把目标 IP 改成路由器 IP;正确的是链路层目标是网关,网络层目标通常仍是最终目标。

路由表的由来

路由器根据路由表决定数据包从哪个接口、经哪个下一跳发送。路由表可以来自:

  • 直连路由;
  • 静态路由;
  • OSPF、IS-IS、BGP、RIP 等动态路由协议;
  • 策略路由、控制器或云平台下发。

路由查找通常采用最长前缀匹配,同样前缀还可能比较管理距离、度量、策略等,不是简单地找一个完全相等的 IP。

路由表结构

CIDR 把网络地址和前缀长度写在一起:

192.168.0.0/24

表示前 24 位是网络前缀,剩余位用于主机或其他用途。前缀长度不一定是 8、16、24,也不一定使用传统 A/B/C 类地址。

CIDR 前缀表示

路由器选定下一跳后,还要通过该出口链路的 ARP/NDP 或其他链路机制得到下一跳链路地址。

路由查找动画

ARP:从 IP 找到下一跳的 MAC

如果 A 只知道同一局域网中 B 的 IPv4 地址 192.168.0.2,但不知道 B 的 MAC 地址,就可以使用 ARP:

  1. A 查询 ARP 缓存;
  2. 缓存没有命中时,A 在本地链路广播 ARP Request;
  3. 拥有目标 IPv4 地址的主机返回 ARP Reply;
  4. A 缓存 IPv4 与 MAC 的映射,再封装 Ethernet 帧。

ARP 缓存示意

如果目标在远端子网,A 不会 ARP 查询远端主机的 MAC,而是查询默认网关的 MAC。ARP 只服务 IPv4 局域网,IPv6 使用邻居发现协议。ARP 缺乏强认证,存在 ARP 欺骗风险,不能把 ARP 缓存当作可信身份数据库。

整个传输过程

电脑视角

  • 确定本机地址、目标地址和路由;
  • 通过前缀判断目标是否在本地链路;
  • 本地目标:解析目标链路地址并直接发送;
  • 远端目标:解析默认网关/下一跳链路地址并发送;
  • 发送过程中,应用和 TCP 通常不需要知道每一跳的 MAC 地址。

交换机视角

  • 收到 Ethernet 帧;
  • 学习源 MAC;
  • 查找目标 MAC 和 VLAN 的映射;
  • 找到已知单播时从对应端口发送;
  • 未知单播、广播或组播按 VLAN 和交换策略泛洪;
  • 表项老化、端口移动和环路控制都会影响转发。

路由器视角

  • 收到链路帧并进行链路层校验;
  • 查看目标 IP、TTL、协议和策略;
  • 进行最长前缀匹配,选择出口和下一跳;
  • IPv4 转发时递减 TTL 并更新首部校验和;
  • 重新封装下一跳链路帧;
  • 没有路由或被策略拒绝时,可能发送 ICMP 错误或直接丢弃。

涉及的三类表可以这样理解:

  • 交换机:MAC 地址表,映射 MAC/VLAN 与端口;
  • 路由器:路由表,映射 IP 前缀与出口/下一跳;
  • 主机和路由器:ARP/NDP 缓存,映射本地下一跳 IP 与链路地址。

MAC 表通过源地址学习,路由表通过直连、静态配置或路由协议形成,ARP/NDP 缓存通过解析、应答和邻居发现形成。它们都可能过期、变化或被策略影响,不是永久不变的全网地图。

参考网络拓扑图

多路由器网络拓扑

当路由器 1 连接路由器 2 后,路由表可以包含下一跳地址。匹配到下一跳的路由后,路由器还要在相应出口网络中解析下一跳的链路地址。

路由器之间的下一跳表

如果 A 给 F 发送数据:

A 到 F 的转发路径

A 到 F 的转发动画

详细过程可以概括为:

  1. A 根据本地前缀判断 F 不在同一子网,选择默认网关 192.168.0.254
  2. A 通过 ARP 获取默认网关的 MAC;
  3. A 封装第一跳 Ethernet 帧:源 MAC 是 A,目标 MAC 是网关;IP 源/目标仍是 A 和 F(未发生 NAT 时);
  4. 交换机 1 根据目标 MAC 把帧交给路由器 1;
  5. 路由器 1 查找 F 的 IP 路由,选择下一跳 192.168.100.5 和对应接口;
  6. 路由器 1 递减 TTL、重新计算必要的 IPv4 首部校验和,并使用下一跳链路地址重新封装帧;
  7. 路由器 2 收到后继续查表,直到目标网络;
  8. 最后一跳路由器对目标主机 IPv4 地址执行 ARP/NDP,获得 F 的链路地址;
  9. 最后一台交换机根据 MAC 表转发帧;
  10. F 校验目标 MAC、IP 和上层协议后接收数据。

在每个路由器之间,链路层源/目标地址通常会改变;端到端 IP 地址通常不改变,但 NAT、隧道、代理或防火墙可能改变路径上的地址和报文。

HTTP 报文传输原理

利用 TCP/IP 进行网络通信时,发送端通常从应用层向下封装,接收端从链路层向上解封装。传统 HTTP/1.1 over TCP 的一条请求可以抽象为:

应用层:HTTP 请求
传输层:TCP 段
网络层:IP 包
链路层:Ethernet/Wi-Fi 帧
物理层:比特、符号或无线信号

到达路由器后,路由器通常去掉收到的链路层帧,查看 IP 分组,再为下一条链路重新封装帧。HTTP/3 则使用 QUIC/UDP,不能把下面所有示意图理解为 HTTP/3 的实际线格式。

HTTP 请求的分层传输

数据封装和分用

数据在网络中传输时需要携带各层的标识。发送时,各层增加自己的首部,这叫封装;接收时,各层依据标识把数据交给对应上层并去掉本层首部,这叫分用。

TCP/IP 数据封装和分用

传输层通常通过端口号进行复用和分用:

  • TCP 和 UDP 的端口字段都是 16 位;
  • TCP、UDP、SCTP 等使用不同的传输协议标识;
  • 同一个端口号可以在不同地址、协议、命名空间或监听状态下分别使用。

IPv4 首部的 Protocol 字段标识上层协议,例如:

1  = ICMP
2  = IGMP
6  = TCP
17 = UDP

IPv6 使用 Next Header 字段,语义上不能直接把所有协议都称作 IPv4 的 Protocol 字段。IP 首部还包含源 IP、目标 IP、TTL/Hop Limit 等信息。

链路层帧也需要标识承载内容的类型,例如以太网 EtherType 常见值:

0x0800 = IPv4
0x0806 = ARP
0x86DD = IPv6

网络分层封装示意

数据封装的好处是:每一层只处理自己的职责,底层链路变化时,上层协议可以继续使用统一的 IP/TCP/HTTP 接口。

不同物理网络之间的传输

HTTP 请求可能经过多个不同类型的物理网络。路由器在网络层进行分组转发,链路层只负责相邻下一跳。

HTTP 请求跨不同网络传输

互联网并不是一个单一的物理网络,而是由大量网络、路由器、交换机、无线链路、隧道和运营商网络互联。TCP/IP 的价值之一就是隐藏各个链路的物理差异,让应用看到统一的 IP 和传输层服务。

TCP 协议的报文格式

TCP 提供面向连接、可靠、有序的字节流服务。TCP 基本首部最小 20 字节,携带选项时最长通常为 60 字节,后面是可选的数据。

TCP 报文格式

(一)源端口号

源端口号占 16 位,表示发送端的传输层端口。它需要和源 IP、目标 IP、目标端口以及 TCP 协议一起参与连接识别。

(二)目的端口号

目的端口号占 16 位,表示接收端的传输层端口。服务器监听的通常是本地端口,已建立的连接还会记录远端地址和端口。

TCP 连接常用以下四元组描述:

源 IP、源端口、目标 IP、目标端口

再加上 TCP 这个传输协议,才能与 UDP 或其他协议区分。NAT、IPv6、容器网络和多网卡环境下,实际连接还要结合命名空间和路径状态理解。

(三)序号(Sequence Number)

TCP 传输的是字节流,数据部分的每个字节都有序号。Sequence Number 占 32 位,会在 2^32 处回绕。

  • SYN=1 时,序号是初始序列号 ISN,SYN 本身占用一个序列号;
  • 建连后的第一个数据字节通常从 ISN+1 开始;
  • 普通数据段的序号是该段第一个数据字节的序号;
  • 下一个数据段序号通常等于前一个数据段序号加上数据长度;
  • FIN 也占用一个序列号,纯 ACK 不占用序列号。

例如某段从序号 5 开始,TCP 数据长度为 12 字节,则下一段数据序号通常从 17 开始。序列号用于排序、检测重复、确认范围和判断报文是否属于当前连接,但不是“第几个 TCP 包”的编号。

(四)确认序号(Acknowledgment Number)

如果 ACK 控制位为 1,确认号表示接收端按序期待的下一个序列号。它是累计确认:ACK=1001 通常表示序号小于 1001 的数据已经按序被 TCP 接收。

举例:客户端发送三个各 1000 字节的数据段,第一段从序号 1 开始,那么理想情况下:

客户端数据段起始序号数据长度服务端累计 ACK
第 1 段110001001
第 2 段100110002001
第 3 段200110003001

TCP ACK 确认号示例

如果第二段丢失,但第三段到达,服务端通常继续发送 ACK=1001,表示仍然缺少从 1001 开始的按序数据;如果协商了 SACK,还可以在 ACK 中报告已经收到的后续区间。

建立连接后,TCP 报文通常都带 ACK;SYN 报文的 ACK 位可能为 0,因为此时还没有确认对端的序列号。对 SYN+ACK 来说 ACK 位为 1,确认号确认客户端的 SYN。

ACK 只反映 TCP 层接收和确认,不代表应用已经读取、解析或持久化数据。

(五)首部长度(Data Offset)

Data Offset 占 4 位,单位不是 4 bit,而是 32 位字(4 字节)。

  • 没有选项时 TCP 首部为 20 字节,Data Offset=5;
  • 最大 Data Offset=15,因此首部最多 60 字节;
  • 选项不足 32 位整数倍时,用填充字节补齐。

(六)保留位

传统 TCP 头部示意图常把这里写成 6 位保留字段并只列 6 个标志位。现代 TCP 头部还包括 ECN 相关的 CWR/ECE 和 NS 位,当前 RFC 9293 的头部布局应理解为保留位与控制位共同组成完整字段。旧图仍可用于认识 URG、ACK、PSH、RST、SYN、FIN,但不能据此认为 TCP 永远只有 6 个控制位。

(七)控制标志

TCP 控制标志

常见控制位:

  • URG:紧急指针有效,现代新应用通常不依赖 TCP urgent 机制;
  • ACK:确认号字段有效;
  • PSH:历史上的“尽快交付缓存数据”提示,不是消息边界,Socket 应用通常不可见;
  • RST:重置或中止连接;
  • SYN:同步初始序列号、建立连接;
  • FIN:发送方在该方向没有更多数据;
  • ECE/CWR:ECN 拥塞通知相关;
  • NS:扩展的 ECN 相关控制位,较少直接在应用讨论。

(八)窗口大小

Window 字段长度为 16 位,用于公布接收窗口。它表示从当前确认号开始接收方愿意接收的范围,实际有效窗口可能通过 SYN 阶段协商的 Window Scale 扩大。

窗口是字节数,不是数据包数量。发送方的实际发送限制还受到拥塞窗口、已发送未确认数据、MSS、PMTU 和系统缓存限制。

(九)校验和

TCP 校验和占 16 位,覆盖 TCP 首部、TCP 数据以及由 IP 地址等组成的伪首部。接收端用于发现部分传输错误。校验失败的报文通常被丢弃,发送端因无法收到有效 ACK 而可能超时重传。

(十)紧急指针

Urgent Pointer 占 16 位,仅在 URG 置位时有效。它与序列号共同描述 urgent data 的位置,历史实现对 urgent data 的处理存在差异,现代新应用通常使用普通应用层消息或带外协议语义替代。

(十一)可选项和填充

选项和填充部分长度是 32 位的整数倍。最常见选项包括:

  • MSS(Maximum Segment Size):声明本端愿意接收的最大 TCP 数据长度,不包含 TCP/IP 首部;
  • Window Scale:扩大接收窗口,必须在 SYN 阶段协商;
  • SACK Permitted/SACK:选择确认能力和非连续数据区间;
  • Timestamp:RTT 测量和 PAWS;
  • TFO:TCP Fast Open Cookie。

MSS 通常在 SYN 报文中声明,表示本端能够接收的最大 TCP 数据段,而不是“对端一定必须使用的最大数据包总长度”。如果没有收到 MSS 选项,RFC 9293 为 IPv4 和 IPv6 分别规定了历史默认发送 MSS(IPv4 为 536 字节、IPv6 为 1220 字节);现代实现通常根据接口 MTU、路径 MTU 发现或 PLPMTUD 发送更合适的 MSS。实际 TCP 分段还受路径 MTU、隧道、拥塞控制和实现策略影响。MSS 越大也不一定越好,超过路径能力会导致分片、丢包或 PMTU 问题。

TCP 可靠性主要来自哪些机制

  1. 序列号和确认:记录字节范围并进行累计确认;
  2. 重传机制:RTO、快速重传、SACK、RACK 等检测丢失并重传;
  3. 校验和:发现部分传输错误;
  4. 排序和去重:接收端按序交付字节,丢弃重复数据;
  5. 流量控制:接收窗口避免接收缓存被发送端压垮;
  6. 拥塞控制:拥塞窗口根据网络状态调整发送速率。

TCP 并不保证应用业务必然完成;如果连接在应用确认前失败,应用仍需要重试、幂等和业务确认。

TCP 的三次握手

TCP 连接建立时,双方需要经过三次握手;断开时通常经过四个控制报文,但实际可能合并、重传或同时关闭。

Java 服务端监听示例

服务端通常依次执行 socket()bind()listen(),然后调用 accept() 获取已建立连接。Java 中 ServerSocket 封装了这些操作。accept() 返回的是一个新的连接 Socket,监听 Socket 继续监听后续连接。

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("服务器开始监听 8080");
            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println("收到连接:" + socket.getRemoteSocketAddress());
                Thread thread = new Thread(() -> handle(socket));
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handle(Socket socket) {
        try (socket) {
            // 此处按应用协议读取和写入数据
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

原文代码中的注释、换行和 ...... 是文章排版残留,已整理。实际服务端应使用线程池、NIO 或异步模型,并设置读取、写入和空闲超时。

Java 客户端主动连接示例

客户端通过 connect() 主动打开连接:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class SocketClient {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 8080)) {
            OutputStream out = socket.getOutputStream();
            InputStream in = socket.getInputStream();

            // 按应用层协议发送和读取数据
            out.write("hello\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));
            out.flush();
            // int value = in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCP 连接的识别通常依靠双方地址和端口,Socket API 还会处理阻塞、超时、缓冲区和关闭等状态。

三次握手过程

TCP 三次握手示意图

设客户端 ISN 为 x,服务端 ISN 为 y

  1. 第一次握手:客户端进入 SYN-SENT,发送 SYN=1, SEQ=x,可以在 SYN 中协商 MSS、Window Scale、SACK Permitted、Timestamp 等选项。ISN 不是固定每 4ms 加一的简单随机值,现代实现会结合时钟和秘密函数生成;
  2. 第二次握手:服务端收到 SYN 后进入 SYN-RECEIVED,发送 SYN+ACK,其中 ACK=x+1,自己的 SEQ=y。服务端发送的 MSS 表示服务端愿意接收的最大 TCP 数据段长度;
  3. 第三次握手:客户端收到 SYN+ACK 后确认服务端序列号,发送 ACK=y+1,客户端进入 ESTABLISHED。第三个 ACK 可以与第一段应用数据合并,但 TFO 的 SYN 早期数据是另一个扩展;
  4. 服务端收到 ACK:服务端进入 ESTABLISHED,TCP 全双工连接建立。

客户端收到 SYN+ACK 后进入 ESTABLISHED,并不代表服务端已经收到第三个 ACK;服务端只有在收到最终 ACK 后才完成自己的状态转换。

TCP 的四次挥手

业务通信完成后,连接的任一端都可以主动关闭自己的发送方向。TCP 是全双工的,两个方向独立关闭,因此常见是四个控制报文,但 ACK 和 FIN 可能合并,RST 则会异常中止连接。

四次挥手具体过程

TCP 四次挥手示意图

假设客户端主动关闭:

  1. 第一次挥手:客户端发送 FIN,正确设置序列号和 ACK,进入 FIN-WAIT-1。这表示客户端不再发送应用数据,但仍可接收服务端数据;
  2. 第二次挥手:服务端收到 FIN,发送 ACK,确认号为客户端 FIN 序列号加 1,进入 CLOSE-WAIT。ACK 只是确认收到 FIN,不等于“同意立即关闭”;服务端仍可以发送剩余数据;
  3. 第三次挥手:服务端应用完成发送后,TCP 发送 FIN(通常也带 ACK),进入 LAST-ACK
  4. 第四次挥手:客户端收到服务端 FIN 后发送 ACK,进入 TIME-WAIT;服务端收到 ACK 后进入 CLOSED。客户端等待约 2×MSL 后释放连接状态。

客户端收到服务端 ACK 后通常从 FIN-WAIT-1 进入 FIN-WAIT-2。服务端长期停留在 CLOSE-WAIT,常常说明应用没有及时关闭连接或仍在等待业务逻辑。

TIME-WAIT 与 2MSL

主动关闭方等待 2MSL,主要有两个目的:

  1. 如果最后 ACK 丢失,服务端会重传 FIN,主动关闭方在 TIME-WAIT 中可以再次发送 ACK;
  2. 防止旧连接延迟到达的报文影响同一四元组创建的新连接。

MSL(Maximum Segment Lifetime)是报文段在网络中允许存在的最大寿命。2MSL 是 TCP 状态机中的规范概念,但具体实现的计时器、端口复用策略和时间戳优化可能不同。不能把 RFC 中的历史推荐值当成所有系统固定的“1~4 分钟”;也不要仅凭 TIME-WAIT 多就判断网络异常。

主动关闭方在等待期间如果收到对方重传的 FIN,通常再次 ACK 并重新等待。TIME-WAIT 既保护对端正常进入 CLOSED,也保护新连接不被旧报文污染。

为什么关闭常见四次,而建立可以三次?

建立时,服务端可以把确认客户端 SYN 的 ACK 与服务端自己的 SYN 合并成一个 SYN+ACK,因为服务端没有“等待发送完剩余应用数据”这一前置问题。

关闭时,被动关闭方收到 FIN 后可能仍有数据要发送,所以先 ACK,再等本地应用完成后单独发送 FIN。若服务端恰好没有剩余数据,ACK 和 FIN 可以合并,抓包中可能只看到三段。

同时关闭

若双方几乎同时发送 FIN,双方可能经历 FIN-WAIT-1 → CLOSING → TIME-WAIT,这和一方先进入 CLOSE-WAIT 的正常路径不同。

三次握手、四次挥手的常见面试题

问题(1):为什么关闭连接通常需要四次,而建立连接只要三次?

关闭时两个方向独立:一方 FIN 只关闭自己的发送方向,对方可能还有数据要发送,因此 ACK 与 FIN 通常分开。建立时服务端的 SYN 和对客户端 SYN 的 ACK 可以合并成 SYN+ACK,节省一个报文。

这不是“建立必然三段、关闭必然四段”的硬性报文数量规则。重传、同时打开、同时关闭、ACK/FIN 合并和 RST 都会改变抓包表现。

问题(2):三次握手可以改成两次吗?

正常 TCP 不能改成两次。三次握手的必要性包括:

  • 双方交换并确认各自的初始序列号;
  • 服务端确认客户端收到了自己的 SYN+ACK;
  • 避免旧的延迟 SYN 使服务端错误建立半开连接;
  • 让双方进入一致的连接状态。

假设只有客户端 SYN 和服务端 SYN+ACK,若 SYN+ACK 丢失,客户端不知道服务端的 ISN 和状态,而服务端若开始发送数据也无法得到客户端对第二次握手的确认,可能形成状态不一致。

假想的两次握手问题

问题(3):为什么 TIME-WAIT 等待 2MSL?

如果最后 ACK 丢失,服务端会重传 FIN;主动关闭方需要保留连接状态来再次确认。另一方面,旧连接的重复报文可能在网络中延迟,等待 2MSL 可以降低它们干扰同一四元组新连接的概率。

不同系统可能使用时间戳、连接复用策略和实现优化,但不能因此把 TIME-WAIT 当作可以随意删除的无用状态。

问题(4):如果连接建立后客户端突然故障怎么办?

TCP keep-alive 是可选机制。Linux 常见默认值为:空闲 7200 秒后开始探测、探测间隔 75 秒、探测次数 9 次,但这不是 TCP 统一固定值,Windows、BSD、容器和云平台可能不同,应用也可以通过 Socket 选项覆盖。

TCP keep-alive 只能探测传输路径或对端 TCP 栈是否响应,不能证明应用仍然健康。生产系统通常结合:

  • 连接和读取超时;
  • 应用层心跳;
  • 负载均衡探活;
  • 连接池健康检查;
  • 断线重连和指数退避。

问题(5):为什么 ACK 和 FIN 有时能合并?

TCP 控制位可以在一个报文中同时设置。服务端如果收到客户端 FIN 时没有剩余数据,可能直接发送 FIN+ACK;如果还有数据,则先发送 ACK,之后再发送 FIN。因此“第三次一定是单独 FIN”只是典型图示,不是协议强制。

问题(6):TCP 是不是一次 send 对应一次 receive?

不是。TCP 是字节流:

  • 一次 send 可能被拆成多个 TCP 段;
  • 多次 send 可能合并为一次 receive;
  • receive 可能只返回部分应用消息;
  • 需要应用层使用长度前缀、固定头、分隔符或自描述格式解决粘包和拆包。

问题(7):TCP 连接建立和关闭是不是一定至少 7 个报文?

三次握手加典型四次挥手可以画成 7 个控制报文,但这不是所有连接的固定下限或实际数量。连接可能复用、主动关闭方向不同、ACK 与 FIN 合并、发生重传、同时关闭或通过 RST 中止。

总结

TCP/IP 是分层协议族:

  • 链路层负责相邻节点帧传输和链路地址;
  • IP 层负责跨网络寻址和逐跳转发;
  • TCP 负责可靠有序字节流、连接状态、流量控制和拥塞控制;
  • 应用层定义 HTTP、DNS、SSH 等具体语义。

理解 TCP 时要同时掌握:

  1. TCP 是字节流,不是消息协议;
  2. SYN、FIN 各占一个序列号,纯 ACK 不占序列号;
  3. ACK 表示 TCP 已按序接收,不等于应用已经处理;
  4. 接收窗口解决流量控制,拥塞窗口解决网络拥塞;
  5. MAC 只在本地链路有意义,IP 负责跨网络寻址,路由器每跳重新封装链路帧;
  6. ARP 解析的是本地下一跳,远端目标通常解析默认网关;
  7. 三次握手和四次挥手是典型状态流程,但可能出现合并、重传和同时关闭;
  8. TIME-WAIT 用于最后 ACK 重传和旧报文隔离;
  9. RTO、拥塞控制、SACK、时间戳和 TFO 都有标准和实现差异,不能死记某个操作系统的默认值;
  10. HTTP/3 使用 QUIC/UDP,现代 HTTP 不应全部等同于 TCP。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS