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(Open Systems Interconnection,开放系统互联)参考模型描述了七层框架:
- 物理层;
- 数据链路层;
- 网络层;
- 传输层;
- 会话层;
- 表示层;
- 应用层。
每一层有相对独立的职责,并通过接口与相邻层配合。OSI 是 ISO 制定的通用参考模型,不是“所有公司必须实现的一套具体协议”;现实中的 TCP/IP 协议族通常把会话、表示等能力分布在应用协议、库、TLS、RPC 或序列化格式中。
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 协议的传输层
传输层为不同主机上的应用进程提供通信抽象,常见功能包括:
- 通过端口号区分同一主机上的不同应用或服务;
- 为上层提供可靠字节流或无连接数据报;
- 进行序列控制、确认、重传、流量控制和拥塞控制(具体能力取决于协议);
- 将应用数据复用到网络层,并在接收端根据端口分用给正确的 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 地址只在相应链路或 VLAN 中有意义,不用于跨互联网路由,也不能单独作为安全身份认证。
图解数据链路层:使用交换机解决 MAC 地址转发问题
集线器的问题
如果能只把帧发送到目标 MAC 所在的端口,就可以减少无关设备接收和冲突。这就是交换机(Switch)的基本思路。

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

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

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

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

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

以后 A 发给 B 时,交换机可以按表项只转发到 B 所在端口:
- 表项会随时间老化;
- MAC 移动时,交换机可能更新端口;
- 广播、组播和未知单播仍可能泛洪;
- VLAN 会把一个物理交换网络划分为多个逻辑广播域;
- 多交换机之间形成二层环路时,需要 STP/RSTP 或其他机制避免广播风暴。
多个交换机可以互联,但“直接把交换机连起来就永远没有问题”是不完整的,真实网络还要考虑 VLAN、链路聚合、生成树、环路、广播域和安全策略。

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

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

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 类地址。

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

ARP:从 IP 找到下一跳的 MAC
如果 A 只知道同一局域网中 B 的 IPv4 地址 192.168.0.2,但不知道 B 的 MAC 地址,就可以使用 ARP:
- A 查询 ARP 缓存;
- 缓存没有命中时,A 在本地链路广播 ARP Request;
- 拥有目标 IPv4 地址的主机返回 ARP Reply;
- A 缓存 IPv4 与 MAC 的映射,再封装 Ethernet 帧。

如果目标在远端子网,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 不在同一子网,选择默认网关
192.168.0.254; - A 通过 ARP 获取默认网关的 MAC;
- A 封装第一跳 Ethernet 帧:源 MAC 是 A,目标 MAC 是网关;IP 源/目标仍是 A 和 F(未发生 NAT 时);
- 交换机 1 根据目标 MAC 把帧交给路由器 1;
- 路由器 1 查找 F 的 IP 路由,选择下一跳
192.168.100.5和对应接口; - 路由器 1 递减 TTL、重新计算必要的 IPv4 首部校验和,并使用下一跳链路地址重新封装帧;
- 路由器 2 收到后继续查表,直到目标网络;
- 最后一跳路由器对目标主机 IPv4 地址执行 ARP/NDP,获得 F 的链路地址;
- 最后一台交换机根据 MAC 表转发帧;
- 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 的实际线格式。

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

传输层通常通过端口号进行复用和分用:
- 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 请求可能经过多个不同类型的物理网络。路由器在网络层进行分组转发,链路层只负责相邻下一跳。

互联网并不是一个单一的物理网络,而是由大量网络、路由器、交换机、无线链路、隧道和运营商网络互联。TCP/IP 的价值之一就是隐藏各个链路的物理差异,让应用看到统一的 IP 和传输层服务。
TCP 协议的报文格式
TCP 提供面向连接、可靠、有序的字节流服务。TCP 基本首部最小 20 字节,携带选项时最长通常为 60 字节,后面是可选的数据。

(一)源端口号
源端口号占 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 段 | 1 | 1000 | 1001 |
| 第 2 段 | 1001 | 1000 | 2001 |
| 第 3 段 | 2001 | 1000 | 3001 |

如果第二段丢失,但第三段到达,服务端通常继续发送 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 个控制位。
(七)控制标志

常见控制位:
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 可靠性主要来自哪些机制
- 序列号和确认:记录字节范围并进行累计确认;
- 重传机制:RTO、快速重传、SACK、RACK 等检测丢失并重传;
- 校验和:发现部分传输错误;
- 排序和去重:接收端按序交付字节,丢弃重复数据;
- 流量控制:接收窗口避免接收缓存被发送端压垮;
- 拥塞控制:拥塞窗口根据网络状态调整发送速率。
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 还会处理阻塞、超时、缓冲区和关闭等状态。
三次握手过程

设客户端 ISN 为 x,服务端 ISN 为 y:
- 第一次握手:客户端进入
SYN-SENT,发送SYN=1, SEQ=x,可以在 SYN 中协商 MSS、Window Scale、SACK Permitted、Timestamp 等选项。ISN 不是固定每 4ms 加一的简单随机值,现代实现会结合时钟和秘密函数生成; - 第二次握手:服务端收到 SYN 后进入
SYN-RECEIVED,发送SYN+ACK,其中ACK=x+1,自己的SEQ=y。服务端发送的 MSS 表示服务端愿意接收的最大 TCP 数据段长度; - 第三次握手:客户端收到 SYN+ACK 后确认服务端序列号,发送
ACK=y+1,客户端进入ESTABLISHED。第三个 ACK 可以与第一段应用数据合并,但 TFO 的 SYN 早期数据是另一个扩展; - 服务端收到 ACK:服务端进入
ESTABLISHED,TCP 全双工连接建立。
客户端收到 SYN+ACK 后进入 ESTABLISHED,并不代表服务端已经收到第三个 ACK;服务端只有在收到最终 ACK 后才完成自己的状态转换。
TCP 的四次挥手
业务通信完成后,连接的任一端都可以主动关闭自己的发送方向。TCP 是全双工的,两个方向独立关闭,因此常见是四个控制报文,但 ACK 和 FIN 可能合并,RST 则会异常中止连接。
四次挥手具体过程

假设客户端主动关闭:
- 第一次挥手:客户端发送 FIN,正确设置序列号和 ACK,进入
FIN-WAIT-1。这表示客户端不再发送应用数据,但仍可接收服务端数据; - 第二次挥手:服务端收到 FIN,发送 ACK,确认号为客户端 FIN 序列号加 1,进入
CLOSE-WAIT。ACK 只是确认收到 FIN,不等于“同意立即关闭”;服务端仍可以发送剩余数据; - 第三次挥手:服务端应用完成发送后,TCP 发送 FIN(通常也带 ACK),进入
LAST-ACK; - 第四次挥手:客户端收到服务端 FIN 后发送 ACK,进入
TIME-WAIT;服务端收到 ACK 后进入CLOSED。客户端等待约 2×MSL 后释放连接状态。
客户端收到服务端 ACK 后通常从 FIN-WAIT-1 进入 FIN-WAIT-2。服务端长期停留在 CLOSE-WAIT,常常说明应用没有及时关闭连接或仍在等待业务逻辑。
TIME-WAIT 与 2MSL
主动关闭方等待 2MSL,主要有两个目的:
- 如果最后 ACK 丢失,服务端会重传 FIN,主动关闭方在 TIME-WAIT 中可以再次发送 ACK;
- 防止旧连接延迟到达的报文影响同一四元组创建的新连接。
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 时要同时掌握:
- TCP 是字节流,不是消息协议;
- SYN、FIN 各占一个序列号,纯 ACK 不占序列号;
- ACK 表示 TCP 已按序接收,不等于应用已经处理;
- 接收窗口解决流量控制,拥塞窗口解决网络拥塞;
- MAC 只在本地链路有意义,IP 负责跨网络寻址,路由器每跳重新封装链路帧;
- ARP 解析的是本地下一跳,远端目标通常解析默认网关;
- 三次握手和四次挥手是典型状态流程,但可能出现合并、重传和同时关闭;
- TIME-WAIT 用于最后 ACK 重传和旧报文隔离;
- RTO、拥塞控制、SACK、时间戳和 TFO 都有标准和实现差异,不能死记某个操作系统的默认值;
- HTTP/3 使用 QUIC/UDP,现代 HTTP 不应全部等同于 TCP。
参考资料
- RFC 9293:Transmission Control Protocol
- RFC 6298:Computing TCP's Retransmission Timer
- RFC 2018:TCP Selective Acknowledgment Options
- RFC 7323:TCP Extensions for High Performance
- RFC 7413:TCP Fast Open
- RFC 791:Internet Protocol
- RFC 826:An Ethernet Address Resolution Protocol
- RFC 1122:Requirements for Internet Hosts
- RFC 5681:TCP Congestion Control
- RFC 9438:CUBIC for Fast and Long-Distance Networks
- RFC 9110:HTTP Semantics