网络通信基础与TCP协议栈
翻大厂的岗位 JD 会发现一个很有意思的现象:对网络通信、TCP/IP 的要求几乎是明写的,对 Netty 反而很少单独列出。但 Java 生态里的 Dubbo、Zookeeper、RocketMQ、gRPC-Java、Spring WebFlux 底层都站着 Netty。也就是说,Netty 更像是「网络通信能力」在 Java 生态里的一个落点,而不是一个孤立的框架。
1. 网络是什么:从定义到分层
为什么需要计算机网络
早期的计算机之间交换数据,靠的是软盘一类的第三方存储介质转存,本质是「人肉搬运」。人们希望数据能直接通过通信线路传输,缩短传输时间,计算机网络由此诞生,并逐步发展为今天的 Internet。
标准定义是:利用通信线路将地理上分散的、具有独立功能的计算机系统和通信设备按不同的形式连接起来,以功能完善的网络软件及协议实现资源共享和信息传递的系统。
定义里有三个要素值得注意:分散(设备物理上不在一起)、独立(各自有自己的操作系统和体系结构)、按形式连接(连接方式不唯一,这正是分层和协议存在的理由)。
从覆盖范围划分,网络分为三类:
| 类型 | 全称 | 覆盖范围 |
|---|---|---|
| 局域网 | LAN(Local Area Network) | 一般为几米到几十公里 |
| 城域网 | MAN(Metropolitan Area Network) | 介于 WAN 与 LAN 之间 |
| 广域网 | WAN(Wide Area Network) | 一般为几十到几千公里 |
当然划分方式不止这一种:按拓扑结构可以分总线型、环型、星型、网状;按信息交换方式可以分电路交换、报文交换、报文分组交换。其中「报文分组交换」是关键,它就是今天 IP 分组转发的思想来源,也是理解网络层为什么可以做到「无连接」的前提。
发展简史:一条「抗毁」的需求主线
- 诞生阶段。20 世纪 60 年代中期之前的第一代计算机网络,是以单个计算机为中心的远程联机系统,本质是「一台主机 + 多个终端」。
- ARPANET。60 年代初,美国国防部为了保证本土和海外防御力量在遭受第一次核打击后仍具备生存和反击能力,需要设计一种分散的指挥系统:它由一个个分散的指挥点组成,部分指挥点被摧毁后,其余点仍能正常工作,并且能绕过已被摧毁的节点继续保持联系。
- 开放性的标准化体系结构,OSI 诞生。ARPANET 兴起后各家厂商各自推出自己的网络体系结构,互联极其困难,于是 ISO 和 IEEE 相继提出了 OSI 参考模型和 TCP/IP 模型。
- Internet。20 世纪 90 年代至今的第四代计算机网络。
第 2 条值得多想一层:「部分节点被摧毁,整体仍能工作」这个需求,决定了 IP 层必须无连接、必须把可靠性交给两端。如果网络层要为每条通信维护连接状态,核心节点就成了单点,一旦被打掉,整条链路就断了,这个设计目标根本无法达成。理解了这一点,就能理解为什么后面要学的 TCP 要设计成现在这样。
另外,TCP/IP 之所以打败 OSI 成为事实标准,并不是因为它在理论上更优雅,而是因为它更早拿出了一个可行性强的实现,并且允许后续持续打补丁。这在工程上是一条反复应验的规律。
协议:网络世界里的「普通话」
网络把来自不同公司、不同体系结构的计算设备连接在一起,它们之间如何通信?这就好比中国地广人多、方言差异巨大,A 地区的方言 B 地区的人可能完全听不懂,所以要有一套全国通用的标准,也就是普通话。计算机网络协议就是计算机之间的普通话。
需要强调的是,协议不等于「格式」,它是一个约定,至少包含三部分:
- 语法:报文的格式,哪几个字节是什么字段;
- 语义:每个字段的含义,收到之后该做什么;
- 时序:先发什么、后发什么,什么状态下能发什么。
所以 TCP 协议不只是「TCP 首部长什么样」,还包括三次握手、超时重传、滑动窗口这些行为约定。判断自己是否真的掌握了一个协议,标准是:能不能说清楚收发双方在各个状态下会做什么。
分层:一种隔离变化的手段
既然网络是分层的,那就要回答一个更根本的问题:为什么要分层?
我的理解是:分层不是为了「把相关的概念放在一起」,而是纵向分工——每层只依赖下一层提供的接口,只解决一类问题。这样做的收益是隔离变化:
- 上层换了应用(HTTP 换成 Redis 协议),下面的链路层、网络层完全不用动;
- 下层换了介质(网线换成 Wi-Fi 换成光纤),上面的 TCP、HTTP 也完全不用动。
代价也很明确:每一层都要加自己的首部,同一份数据被层层包装,产生额外开销。这就是为什么一个短消息最终在网络上跑的时候,头部可能比正文还长,也是应用层要做小包合并、Netty 要提供零拷贝的原因之一。
一句话总结:分层就是拿「性能开销」换「可演进性」。
2. OSI 七层模型与 TCP/IP 模型
2.1 OSI 七层模型
OSI(Open System Interconnect,开放系统互连参考模型)是 ISO 和 CCITT 联合制定的,目的是为异种计算机互连提供一个共同的基础和标准框架,为保持相关标准的一致性和兼容性提供共同参考。这里说的「开放系统」,指的是遵循 OSI 参考模型和相关协议、能够实现互连的、具有各种应用目的的计算机系统。
OSI 采用分层的结构化技术,共分七层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。

| OSI 七层 | TCP/IP 五层 | TCP/IP 四层 | 这一层解决的核心问题 | 典型协议 |
|---|---|---|---|---|
| 应用层 / 表示层 / 会话层 | 应用层 | 应用层 | 「内容怎么解释」 | HTTP、DNS、FTP、SMTP |
| 传输层 | 传输层 | 传输层 | 「交给主机上的哪个进程」 | TCP、UDP |
| 网络层 | 网络层 | 网络层 | 「怎么跨网络找到目标主机」 | IP、ICMP、ARP |
| 数据链路层 | 数据链路层 | 网络接口层 | 「同一个网段内怎么把帧送出去」 | Ethernet、PPP |
| 物理层 | 物理层 | 网络接口层 | 「0 和 1 怎么变成电信号/光信号」 | — |
2.2 TCP/IP 模型
OSI 模型比较复杂且学术化,实际使用的是 TCP/IP 模型:分 5 层时是物理层、数据链路层、网络层、传输层、应用层;把物理层和数据链路层合称为网络接口层,对应的就是 TCP/IP 四层协议模型。

一个很实用的类比:对 PC 机来说,
- 物理层 ≈ 网卡;
- 数据链路层 ≈ 网卡驱动程序;
- 网络层和传输层 ≈ 由操作系统负责处理;
- 应用层 ≈ 常用的网络应用程序和我们自己写的网络程序。
这个类比的价值在于建立直觉:Netty 要解决的问题,几乎全部集中在「操作系统已经帮你处理完网络层和传输层之后,余下的那部分工作」——也就是如何高效地读写这些字节、如何组织业务逻辑。这也是为什么 Netty 是应用层和传输层之间的桥梁,而不是网络协议的实现。
为什么 OSI 输给了 TCP/IP
除了「TCP/IP 更早可用、持续改良」这个历史原因,从技术角度看还有两点:
- OSI 的会话层和表示层过于细化。表示层关心的数据格式、加密压缩,实际上大多数应用自己就处理了;会话管理在 HTTP 这类无状态协议里甚至不存在。
- OSI 是「先定标准,再写实现」,TCP/IP 是「先有实现,跑通了再固化标准」。工程上验证过的标准,比设计得很漂亮的标准更容易活下来。
不过 OSI 的七层划分在描述问题时依然好用,比如分析一个报文时会说「这是二层的问题还是三层的问题」,所以在面试和日常排障中还是绕不开。
3. TCP/IP 协议族:它其实是一个「家族」
TCP/IP 是 Transmission Control Protocol/Internet Protocol 的简写,中文译为传输控制协议/因特网互联协议,是 Internet 最基本的协议、Internet 互联网络的基础,由网络层的 IP 协议和传输层的 TCP 协议组成。
但更准确的理解是:它是利用 IP 进行通信时所必须用到的协议群的统称,是个协议家族,由分布在不同的层的很多协议组成,是互联网的基础通信架构。

| 所在层 | 协议举例 | 作用 |
|---|---|---|
| 网络层 | IP | 确定网络中唯一的一台计算设备,为数据加上源地址和目的地址 |
| 网络层 | ICMP | 传递差错和控制信息,ping 就基于它 |
| 网络层 / 链路层之间 | ARP | 由目的 IP 地址反查 MAC 地址 |
| 传输层 | TCP | 面向连接的可靠传输 |
| 传输层 | UDP | 无连接的不可靠传输 |
| 应用层 | HTTP、DNS、FTP、SMTP | 具体业务语义 |
其中 IP 是 TCP/IP 中最核心的协议:它的作用好比现实生活中的电话号码或通讯地址,负责对数据加上 IP 地址(发送方的源地址和接收方的目的地址)和其他信息,以确定传输的目标。
把协议家族讲成人话:快递类比
- IP 决定包裹最终要送到哪栋楼(目标主机的逻辑地址)。
- 传输层决定包裹交给这栋楼里的哪个人(进程),TCP 相当于「打电话确认、要求对方签收、丢件重发」,UDP 相当于「扔进快递柜就不管了」。
- 链路层决定这一段路怎么走,相当于从发件网点到下一个网点的运输。
- 应用层是包裹里的东西到底是什么、用什么语言写的,只有收发双方约定好才看得懂。
TCP 和 UDP 都是传输层的协议,传输层主要为两台主机上的应用程序提供端到端的通信。
- TCP 类似于日常打电话:电话接通后通过「喂」确认对方身份,听不清会要求对方重说,说得太快会要求说慢点,讲完各说一句「再见」结束通话。它提供可靠的数据传输服务,面向连接,要经历建立连接的过程,传输过程中采用「带重传的肯定确认」技术实现可靠性,还用「滑动窗口」进行流量控制,发送完成后关闭连接。
- UDP 类似于通过不靠谱的物流系统寄东西:把数据直接发出去,不管对方是不是在接收、能不能接收,也不需要接收方确认,属于不可靠传输,可能出现丢包,实际应用中要求程序员自己编程验证。
所以 TCP 比 UDP 可靠得多。常见网络应用基本都基于这两者,它们又会使用网络层的 IP 协议。
可以绕过传输层吗
可以,而且这是理解分层边界的好例子:
- 直接使用 IP:Linux 内核中的 LVS 就可以直接基于 IP 层做负载均衡调度;
- 直接访问链路层:tcpdump 就是直接和链路层通信的程序。
这说明分层是「建议路径」而不是「强制路径」,只要有明确的收益,跨层操作是完全允许的。Netty 中的一些优化(比如 Epoll 的原生传输、堆外内存 Direct Buffer)本质上也是类似的「绕开通用路径换性能」的思路。
4. 数据是怎么在 TCP/IP 中传输的

数据每经过一层,该层就会在它前面附加一个首部,首部里放着这一层需要的控制信息,比如目标地址、协议类型。通常把协议需要的这部分信息称为包首部,把要发送的内容称为数据。每一层眼中的「数据」是逐层扩大的:应用层产生的消息交给 TCP 时,TCP 把它整体当作数据,在前面加上 TCP 首部;封装好的报文段交给 IP 时,IP 又把整个报文段当作数据,加上 IP 首部。对每一层来说,上一层交下来的包都是本层的数据,本层不关心里面具体是什么内容。
所以网络里传输的数据包都由两部分组成:本层的首部和上一层传下来的数据。首部的结构由协议规范详细定义,读首部就是在读这一层需要的控制信息,读完也就知道这个包接下来该怎么处理。数据本身是什么,本层不去解释——每一层只处理自己的首部。封装与解封装之所以能成立,靠的就是这条边界;反过来,如果某一层需要理解上层的内容,分层也就失去意义了。
封装的具体过程,以用户 A 发送数据给用户 B 为例:
| 步骤 | 所在层 | 做了什么 | 产物名称 |
|---|---|---|---|
| ① | 应用层 | 应用程序进行编码处理,产生报文/消息交给下面的 TCP 层 | message(消息) |
| ② | 传输层 | TCP 根据应用指示负责建立连接、发送数据、断开连接;把应用层数据封装为报文段并附加 TCP 首部,交给 IP 层 | segment(报文段) |
| ③ | 网络层 | IP 把 TCP 首部和 TCP 数据合起来当做自己的数据,在前端加上 IP 首部生成 IP 数据报,交给数据链路层 | datagram(数据报) |
| ④ | 数据链路层 | 对 IP 层来说是数据的部分,附加上链路层首部封装为链路层帧,通过物理层传输给接收端 | frame(帧) |
用文字画出来就是:
应用层 [ 用户数据 ]
传输层 [TCP 首部][ 用户数据 ]
网络层 [IP 首部 ][TCP 首部][ 用户数据 ]
链路层 [帧首部 ][IP 首部 ][TCP 首部][ 用户数据 ][帧尾]「每一层加一个头」就是封装。也正因为头是逐层累加的,MTU(最大传输单元)的限制才会传导到 TCP 层,最终演变成 TCP 的分段与 MSS 协商。
用户 B 的处理过程正好相反:
| 步骤 | 所在层 | 做了什么 |
|---|---|---|
| ⑤ | 数据链路层 | 从帧首部找到 MAC 地址判断是否发给自己,不是则丢弃;是则从以太网包首部的类型确定数据类型,再传给相应模块(如 IP、ARP) |
| ⑥ | 网络层 | 从 IP 包首部判断 IP 地址是否与自己匹配,匹配则根据首部的协议类型把数据发给对应模块(如 TCP、UDP) |
| ⑦ | 传输层 | 计算校验和判断数据是否被破坏,检查是否按序号接收,检查端口号确定具体应用程序;数据被完整接收后交给由端口号识别的应用程序 |
| ⑧ | 应用层 | 应用程序直接接收数据,通过解析数据展示相应内容 |
数据从下往上走:链路层先核对 MAC 地址,网络层再核对 IP 地址,传输层计算校验和、检查序号、按端口号找到对应的应用程序,最后交给应用层解析。传输层的处理可以概括为三件事:校验和(完整性)、序号(顺序性)、端口号(交付目标)。后面讲 TCP 可靠性,讲的其实就是它们怎么被保证。
⑤⑥ 两步里都出现了「丢弃」动作:
- 二层发现 MAC 不是自己的,直接丢弃;
- 三层发现 IP 不是自己的,不会向上交付。
收到不属于自己的包,在网络上是常态(比如集线器时代的广播、同一网段里的其他流量),所以丢弃是协议栈里高频且开销很小的操作。抓包工具能抓到大量与本机无关的报文,也是这个原因:网卡工作在混杂模式下会把经过的帧都收上来,最终由协议栈丢掉。
5. 地址和端口号
网络通信要解决三个「找谁」的问题,对应三种地址:
- MAC 地址:这一段链路上,帧交给谁;
- IP 地址:跨越多个网络,找到哪台主机;
- 端口号:找到主机之后,数据交给哪个进程。
5.1 MAC 地址
MAC 地址全称媒体访问控制地址,也叫局域网地址、以太网地址或物理地址,由厂商在生产时写入网卡。
- 共 48 位(6 个字节):前 24 位是厂商标识,由 IEEE(电气和电子工程师协会)分配,后 24 位由厂商自行制定;
- 常见写法
FF:FF:FF:FF:FF:FF或FF-FF-FF-FF-FF-FF,其中全FF是广播地址; - MAC 地址与网络位置无关:设备接到哪里,MAC 地址都不变(厂商把它写在网卡的 BIOS 里),理论上除非盗取硬件,否则没法冒名顶替。
查看方式:
# Windows
ipconfig -all
# Linux 常见四种方式
ifconfig -a
ip link show
cat /sys/class/net/eth0/address # 查看 eth0 的 mac 地址
dmesg | grep eth05.2 IP 地址
IP 地址全称互联网协议地址,是给互联网上的每一个网络、每一台主机配置的逻辑地址,用来与物理地址作区分。
IPv4 由 32 位二进制数组成,通常写成 4 个「8 位二进制数」(4 个字节),格式为 A.B.C.D,其中 A、B、C、D 是 0~255 的十进制整数,例如 192.168.1.1。
它跟 MAC 地址的区别:
| 对比维度 | IP 地址 | MAC 地址 |
|---|---|---|
| 设计依据 | 出于拓扑设计,只要不重复就可以随意更改 | 由生产厂商烧录,一般不能改动 |
| 长度 | 32 位(IPv4) | 48 位 |
| 所在协议层 | OSI 的网络层 | OSI 的数据链路层 |
| 分配依据 | 基于自定义的网络拓扑 | 基于制造商 |
分工上,数据链路层协议负责把数据从一条链路上的一个节点送到另一个节点(靠 MAC 地址);网络层协议负责把数据从一个网络送到另一个网络(先由 ARP 根据目的 IP 地址找到中间节点的 MAC 地址,再经中间节点转发)。一台 PC 更换网卡后 MAC 地址会变、IP 地址通常不变——物理地址跟随硬件,逻辑地址跟随拓扑。
5.3 端口号

端口号是传输层的「地址」,用来识别同一台计算机上进行通信的不同应用程序,因此也叫程序地址。
它由 16 位的字段表示,理论上有 2^16 = 65536 个,除去被保留的 0 号端口(表示所有端口),实际可用 65535 个。
端口号的分配分两类:
- 知名端口号(静态方法):HTTP、FTP、Telnet 等应用的固定端口,分布在 0~1023,写自己的服务时尽量不要占用;
- 临时端口号(时序分配法):客户端不用自己设置端口,交给操作系统分配,一般大于 10000。
同一台服务器上,不同应用必须用不同端口,否则会报 Address already in use。而服务端必须固定端口才能被发现,客户端用哪个端口无所谓——这个「无所谓」引出了下面这个经典问题。
查看端口占用的常用命令:
# Windows
netstat -ano # 查看所有端口号
netstat -ano | findstr "<端口号>" # 查看指定端口
# Linux(root 用户)
lsof -i:端口号 # 查看指定端口占用
lsof -i -U # 显示所有打开的 UNIX domain 和端口文件
netstat -tunlp # 显示 tcp、udp 的端口和进程等相关情况
netstat -tunlp | grep 端口号 # 查看端口占用netstat -tunlp 各参数含义:
| 参数 | 含义 |
|---|---|
-t | 仅显示 tcp 相关选项 |
-u | 仅显示 udp 相关选项 |
-n | 拒绝显示别名,能显示数字的全部转化为数字 |
-l | 仅列出在 Listen(监听)的服务状态 |
-p | 显示建立相关链接的程序名 |
5.4 TCP 连接数上限

操作系统用五元组——源 IP、目标 IP、协议号、源端口、目标端口——唯一地标识一条通信。考察 TCP 连接时,协议号已确定为 TCP,五元组退化为四元组:源 IP、源端口、目的 IP、目的端口。四元组里任意一个元素不同,就是一条全新的连接。
服务端以 MySQL 为例,假设 IP 是 X、端口是 3306:用户 A 的 (A1, PA, X, 3306) 和用户 B 的 (B1, PB, X, 3306) 是两条连接,MySQL 不需要为 B 开新端口。目的端 (X, 3306) 固定,可变的只有源 IP(32 位,最多 2^32 个)和源端口(16 位,共 2^16 个),所以理论上限是 2^32 × 2^16,大约两百多万亿。实际当然达不到,目前工程上能到千万级,Java 应用大概百万级——限制因素是文件句柄数、内存、CPU 和内核参数,不是端口号。
客户端可用端口只有 6 万多个,但四元组里目的 IP 和目的端口也是可变的——同一个客户端端口连接不同服务器,就是两条不同的连接:
连接 1:客户端 IP + 客户端 10000 端口 → 服务器 IP + 服务器 10000 端口
连接 2:客户端 IP + 客户端 10000 端口 → 服务器 IP + 服务器 20000 端口客户端只要启动时不显式绑定端口,内核会自己选择并复用端口去连接不同服务端,不会产生数据混乱。所以客户端能同时支持的连接数比服务端还要大得多。
一句话总结:65535 是「单机单 IP 对单一目标服务」的临时端口上限,不是整机 TCP 连接上限——后面讲 Netty 支撑百万连接时还会用到这个结论。
6. TCP 的核心特性
TCP(Transmission Control Protocol)是面向连接的通信协议:先通过三次握手建立连接,然后才能开始读写数据,通信结束时再拆除连接,因此 TCP 只能用于端到端的通信。它提供的是可靠的数据流服务:
- 数据可能被拆开发送,靠超时重传 + 应答确认保证不丢;重传超时(RTO)根据 RTT 动态调整,而不是用固定值;
- IP 层不保证到达顺序,TCP 用序号把包排序并做错误检查,乱序可以重排,损坏可以重传;
- 用滑动窗口做流量控制:窗口表示接收能力,用来限制发送方的速度;
- 支持全双工,通信双方可以在同一个连接上同时收发数据。
需要高可靠的应用基本都用 TCP:Telnet、FTP、rlogin、SMTP 等;DNS 传输域名数据库时也用 TCP,普通查询用 UDP。
容易混淆的一点:流量控制关心「接收方能不能收得下」(
rwnd,接收窗口),拥塞控制关心「网络能不能扛得住」(cwnd,拥塞窗口),实际发送窗口取两者的较小值。
6.1 三次握手(建立连接)

在网络通信里,客户端和服务端不是靠硬件区分的,而是看谁发起连接:socket 编程中,三次握手由客户端执行 connect 触发,发起连接的一方是客户端,接收连接的一方是服务端。
三次握手就是建立连接时,客户端和服务端总共交换三个包来确认连接建立:
- 客户端发第一个包:SYN=1,seq 填入随机值 J,进入
SYN_SENT; - 服务端回第二个包:SYN=1、ACK=1,seq 填入随机值 K,ack=J+1,进入
SYN_RCVD; - 客户端回第三个包:ACK=1,ack=K+1;服务端校验通过后,双方进入
ESTABLISHED,可以开始传数据。
| 次序 | 方向 | 标志位 | seq | ack | 状态变化 |
|---|---|---|---|---|---|
| 1 | C → S | SYN=1 | J(随机) | — | 客户端 SYN_SENT |
| 2 | S → C | SYN=1, ACK=1 | K(随机) | J+1 | 服务端 SYN_RCVD |
| 3 | C → S | ACK=1 | J+1 | K+1 | 双方 ESTABLISHED |
为什么是三次:本质是双方交换初始序号、并确认对方收到了。为了可靠传输,TCP 通信双方都必须维护序列号,标识哪些数据已被对方收到——比如发送 10 字节、序号 500,接收方回确认号 510(500+10)就表示已收到。两次握手只能确认发起方的序号,另一方的序号得不到确认;三次之后双方都已互相确认,第四次没有必要。所以三次是保证可靠传输又提高效率的最小次数。
三次握手的缺陷:SYN 洪泛攻击。服务端处理第一个握手包后,需要分配 TCB(连接控制资源)并等待第三次握手,攻击者伪造大量源 IP 只发第一个包,队列被占满,正常连接就会被拒绝。常见防护:
| 方案 | 做法 | 评价 |
|---|---|---|
| 无效连接监视释放 | 达到阈值时拆除连接、释放系统资源 | 正常连接和攻击连接一视同仁,不推荐 |
| 延缓 TCB 分配 | 等连接真正建立后再分配 TCB | 有效,先不为不确定的请求分配资源 |
| 使用防火墙 | 确认连接有效后,才向内部服务器发起 SYN | 最好的方案 |
Linux 后续还引入了 SYN Cookies(net.ipv4.tcp_syncookies = 1):队列溢出时不再保存半连接状态,而是把状态编码进 SYN/ACK 的序列号,等客户端回 ACK 时再还原。
6.2 四次挥手(断开连接)

TCP 是全双工的,每个方向都要单独关闭,所以建立连接需要 3 个包,终止连接需要 4 个包。先调用 close 的一方执行主动关闭,另一方执行被动关闭:
- 主动方发 FIN,进入
FIN-WAIT-1; - 被动方回 ACK,进入
CLOSE-WAIT(半关闭:主动方不再发数据,但仍要接收被动方发来的数据);主动方收到确认后进入FIN-WAIT-2; - 被动方也调用
close,发 FIN,进入LAST-ACK; - 主动方回 ACK,进入
TIME-WAIT,等 2×MSL(最长报文段寿命)后进入CLOSED;被动方收到 ACK 后立即进入CLOSED。
| 次序 | 方向 | 报文 | 主动关闭方 | 被动关闭方 |
|---|---|---|---|---|
| 1 | A → B | FIN | FIN-WAIT-1 | — |
| 2 | B → A | ACK | FIN-WAIT-2 | CLOSE-WAIT(半关闭) |
| 3 | B → A | FIN | TIME-WAIT | LAST-ACK |
| 4 | A → B | ACK | TIME-WAIT(等 2×MSL)→ CLOSED | CLOSED |
为什么是四次:全双工意味着每个方向都需要一个 FIN 和一个 ACK。之所以说「通常」是四次,是因为个别情况下 FIN 可以随数据一起发送,被动方的 ACK 和 FIN 也可能被合并成一个包。
TIME-WAIT 存在的两个原因:
- 可靠地终止连接:最后一个 ACK 如果丢失,对方会重发 FIN,主动方需要留在某个状态里接收它并重发 ACK,这个状态就是
TIME-WAIT; - 让迟到的报文有足够时间被识别并丢弃:如果马上用相同的 IP 和端口建立新连接(旧连接的「化身」),可能收到旧连接迟到的数据,等待正是为了避免这种串扰。
等待 2×MSL 覆盖的,是一个报文在网络中可能存活的最长时间,加上它的应答可能存活的最长时间。
关于 TIME_WAIT 的一个常见误传:网上有说法称,把 /proc/sys/net/ipv4/tcp_fin_timeout 调小就能减少 TIME_WAIT,这个说法不准确——tcp_fin_timeout 主要是给 FIN_WAIT_2 用的,而且在不少内核版本里它更多是输出用的;内核里真正控制 TIME_WAIT 超时时间的是 include/net/tcp.h 里的宏:
#define TCP_TIMEWAIT_LEN (60 * HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds */缺省值是 60 秒,想真正改变 TIME_WAIT 的时长,改的是这个宏。
MySQL 大量 TIME_WAIT 的排查与处理:课程里给过一个典型案例。MySQL 侧可以用 show variables like "wait_timeout"; 查看连接最大空闲时长(针对非交互式连接),默认 28800 秒(8 小时);内核侧编辑 /etc/sysctl.conf,加入以下内容后执行 /sbin/sysctl -p 生效:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30| 参数 | 含义 |
|---|---|
net.ipv4.tcp_syncookies = 1 | 开启 SYN Cookies,SYN 等待队列溢出时用 cookies 处理,可防范少量 SYN 攻击,默认 0(关闭) |
net.ipv4.tcp_tw_reuse = 1 | 开启重用,允许将 TIME-WAIT sockets 重新用于新的 TCP 连接,默认 0(关闭) |
net.ipv4.tcp_tw_recycle = 1 | 开启 TIME-WAIT sockets 的快速回收,默认 0(关闭) |
net.ipv4.tcp_fin_timeout | 修改系统默认的 TIMEOUT 时间 |
补充一点:tcp_tw_recycle 在 Linux 4.12 已被移除(它依赖时间戳判断,在 NAT 环境下会丢包),现代内核上能用的主要是 tcp_tw_reuse。
产生大量 TIME_WAIT 的常见原因有两个:
- 连接 MySQL 的代码使用短连接,用完后系统自动回收资源,访问量一大就会产生大量连接;
- 程序代码中没有使用
close,触发了 MySQL 的空闲判断机制。
TIME_WAIT 本身不是 bug,它是 TCP 可靠关闭的必然代价;问题不在「有 TIME_WAIT」,而在「数量超出了端口和资源能承受的范围」。所以解法优先级是:先用连接池/长连接减少连接数量(治本)→ 再考虑内核参数复用(缓解)→ 最后才是调整超时时间(治标且有风险)。
7. UDP、UDT 与 QUIC
7.1 UDP 概述
UDP(User Datagram Protocol,用户数据报协议)是把数据直接发出去,而不管对方是不是在接收,也不管对方是否能接收得了,也不需要接收方确认,属于不可靠的传输,可能会出现丢包现象,实际应用中要求程序员编程验证。
7.2 UDP 的单播与广播
- 单播:定义为发送消息给一个由唯一的地址所标识的单一的网络目的地。面向连接的协议和无连接协议都支持这种模式。
- 广播:由于通讯不需要连接,所以可以实现广播发送。所谓广播——传输到网络(或者子网)上的所有主机。
需要补充的是:广播能实现的前提在 IP 层——正是因为有子网掩码和「主机位全 1」的广播地址,IP 数据报才能被定向投递给子网内所有主机。这正是前面说过的「分层是抽象的堆叠」的又一例证。
7.3 UDP 的使用场景
UDP 因为没有 TCP 等一系列复杂机制,所以使用也非常广泛:
- 包总量较少的通信(DNS、SNMP 等);NTP(网络时间协议)和 DNS(DNS 也使用 TCP)也属于此类;
- 视频、音频等多媒体通信(即时通信);
- 限定于 LAN 等特定网络中的应用通信;
- DHCP 等协议利用了 UDP 的广播功能。
常用的 QQ,就是一个以 UDP 为主、TCP 为辅的通讯协议。
选择 UDP 的判断依据其实很简单:当「实时性」比「完整性」更重要,或者业务自己就能处理重传时,UDP 更合适。 DNS 查询就是这样:一次查询丢了,客户端自己重发一次比 TCP 建立连接更快。
7.4 UDT(基于 UDP 的数据传输协议)
基于 UDP 的数据传输协议(UDP-based Data Transfer Protocol,简称 UDT)是一种互联网数据传输协议。UDT 的主要目的是支持高速广域网上的海量数据传输,最典型的例子是建立在光纤广域网上的网格计算,一些研究所在这样的网络上运行分布式的数据密集程序,例如远程访问仪器、分布式数据挖掘和高分辨率的多媒体流。
动机很明确:互联网上的标准数据传输协议 TCP 在高带宽长距离网络上性能很差(这通常被称为「长肥管道」问题)。顾名思义,UDT 建于 UDP 之上,并引入新的拥塞控制和数据可靠性控制机制。UDT 是面向连接的双向的应用层协议。
UDT 的特性主要包括:
| 特性 | 说明 |
|---|---|
| 面向连接 | 两个使用协议的应用在彼此交换数据之前必须先建立一个连接,当然 UDT 是逻辑上存在的连接通道。这种连接的维护基于握手、Keep-alive(保活)以及关闭连接 |
| 可靠 | 依靠包序号机制、接收者的 ACK 响应和丢包报告、ACK 序号机制、重传机制(基于丢包报告和超时处理)实现数据传输的可靠性 |
| 双工 | 每个 UDT 实例包含发送端和接收端的信息 |
| 新的拥塞算法 + 可扩展的拥塞控制框架 | 不同于基于窗口的 TCP 拥塞控制算法(慢启动和拥塞避免),是混合的基于窗口的、基于速率的拥塞控制算法。开源的代码和拥塞控制的 C++ 类架构,可支持开发者派生专用的拥塞控制算法 |
| 带宽估计 | UDT 使用对包(PP,Packet Pair)机制估计带宽值:每 16 个包为一组,最后一个是对包,即发送方不用等到下一个发送周期再发送。接收方接收到对包后对其到达时间进行记录,可结合上次记录的值计算出链路带宽(计算方法称为中值过滤法),并在下次 ACK 中反馈 |
UDT 的定位一句话总结:TCP 和 UDP 的区别和使用场景大家都知道,但有没有一种协议能同时兼顾 TCP 的安全可靠和 UDP 的高效?UDT 就是一种尝试。
7.5 QUIC
QUIC 代表「快速 UDP Internet 连接」,是基于 UDP 的传输层协议,它本身就是 Google 尝试将 TCP 协议重写为一种结合了 HTTP/2、TCP、UDP 和 TLS(用于加密)等多种技术的改进技术。Google 希望 QUIC 通信技术逐渐取代 TCP 和 UDP,作为在 Internet 上移动二进制数据的新选择协议,其主要目的是整合 TCP 协议的可靠性和 UDP 协议的速度和效率。
为什么值得重造一个协议? 核心原因是:
由于 TCP 是在操作系统内核和中间件固件中实现的,因此对 TCP 进行重大更改几乎是不可能的(TCP 协议栈通常由操作系统实现,如 Linux、Windows 内核或者其他移动设备操作系统;修改 TCP 协议是一项浩大的工程,因为每种设备、系统的实现都需要更新)。但是,由于 QUIC 建立在 UDP 之上,因此没有这种限制。
「部署能力」本身就是一种竞争力——这一点和前面 TCP/IP 战胜 OSI 的逻辑如出一辙。
QUIC 的优势:
- 多路复用。一个连接可以同时承载多个流(stream),同时发起多个请求,请求间完全独立,某个请求阻塞甚至报文出错均不影响其他请求。这直接解决了 HTTP/2 over TCP 的队头阻塞问题——TCP 层的队头阻塞会让所有流一起卡住,而 QUIC 把流的概念下沉到了传输层,让流之间真正互不影响。
- 低延迟建连。QUIC 只需要 1 RTT 就可以建立可靠安全的连接,相对于 TCP + TLS 的 3 次 RTT 要更加快捷。之后客户端可以在本地缓存加密的认证信息,再次与服务器建立连接时可以实现 0-RTT 的连接建立延迟。
- 重传 vs 纠错。TCP 采用重传机制,发生丢包时需要等待一个延时判断发生了丢包,然后再启动重传,这个过程会造成一定的阻塞,影响传输时间;而 QUIC 采用纠错机制,有点类似 RAID5:每 n 个包额外发一个校验和包,如果这 n 个包中丢了一个,可以通过其他包和校验和恢复出来,完全不需要重传。
- 用户态实现。QUIC 直接基于客户端(应用进程)实现,而非基于内核,可以快速迭代更新,不需要操作系统层面的改造,部署灵活。
- 连接保持。QUIC 在客户端保存连接标识,当客户端 IP 或者端口发生变化时可以快速恢复连接:客户端以标识请求服务端,服务端验证标识后感知客户端的新地址端口并重新关联,继续通讯。这对于改善移动端应用连接体验意义重大(比如从 Wi-Fi 切换到流量)。