
前言:在 TCP/IP 五层网络模型中,传输层是衔接应用层与网络层的关键枢纽,向下封装 IP 报文、向上为各类应用程序提供数据传输能力。我们日常上网刷网页、打游戏、视频通话、域名解析,底层全部依赖 TCP 与 UDP 两大传输层协议。二者设计理念截然不同:一个追求极致可靠,一个主打低延迟高效。本文结合学习笔记,从端口基础入手,逐层拆解 UDP、TCP 报文格式、核心特性、经典机制、踩坑问题与落地选型,搞定面试与开发中 90% 的传输层考点。

一、再谈端口号
IP 地址用来定位全网中的一台主机,端口号用来定位一台主机上具体的应用进程,二者配合才能精准完成端到端的数据投递。


1.1 端口号范围划分
- 0~1023:知名端口(Well-Know Port)系统预约定制固定端口,绑定主流应用协议,不能随意占用:
- 22:SSH 远程登录、21:FTP 文件传输、23:Telnet 远程终端、80:HTTP 网页、443:HTTPS 加密网页
- Linux 可通过
cat /etc/services查看系统全部知名端口映射表,自定义程序开发需要避开该区间端口。
- 1024~65535:动态临时端口客户端发起连接时,由操作系统随机自动分配,用完自动回收。
1.2 五元组唯一标识一条通信链路
在操作系统中,源 IP、源端口号、目的 IP、目的端口号、协议号五元组可以唯一标记一条网络通信,Linux 使用netstat -n命令查看五元组信息。
补充两个经典思考题:
⼀个进程是否可以bind多个端口号?
① 单个进程可以绑定多个端口(一个服务开多端口监听);
⼀个端口号是否可以被多个进程bind?
② 单个端口默认只能被一个进程绑定,如需多进程复用端口,需要设置
setsockopt开启SO_REUSEADDR选项。
二、UDP协议
UDP 全称用户数据报协议,设计思想:尽可能少的协议开销,优先保证传输速度,可靠性交给上层应用自行处理,类比生活中邮寄平信:填好收件地址直接投递,不确认对方是否签收、丢件不通知。
1. 为什么说 “UDP 没有真正意义上的发送缓冲区”?
UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作,可以理解:
- 当应用层调用
sendto(或write)发送数据时,本质是一次用户态→内核态的数据拷贝:序列化后的应用数据(比如 “你好啊 + 时间 + 昵称”)会被直接拷贝到内核,交给 UDP 协议处理;- UDP 是无连接、不可靠的协议,不需要缓存发送过的数据来等待重传(TCP 会缓存发送缓冲区,直到收到 ACK 才释放),因此内核不会在内核态为 UDP 维护 “发送队列 / 发送缓冲区”,数据拷贝到内核后,会立刻被 UDP 封装报文,交给 IP 层处理;
- 注意:
sendto调用成功,仅代表 “数据已成功拷贝到内核、交给 UDP 处理”,不代表数据已经发送到网络,更不代表对方已经收到,这和 TCP 的 “发送缓冲区缓存 + ACK 确认” 完全不同。2. UDP 接收缓冲区:有序?丢包?全解析
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再到达的UDP数据就会被丢弃,拆解核心特性:
- 作用:内核收到 UDP 报文后,会先把报文放到 socket 对应的接收缓冲区中,等待应用层调用
recvfrom(或read)来读取,避免应用层处理不及时导致报文丢失;- 顺序不保证:UDP 报文在网络传输中可能乱序到达(比如不同报文走了不同的路由),内核不会像 TCP 那样对报文排序,因此接收缓冲区里的报文顺序,和发送顺序可能不一致;
- 缓冲区满则丢包:接收缓冲区的大小是有限的(可通过
setsockopt调整SO_RCVBUF),如果应用层读取速度慢于报文到达速度,缓冲区被占满后,新到达的 UDP 报文会被内核直接丢弃,且不会通知发送方;- 补充:应用层调用
recvfrom读取数据时,也是一次内核态→用户态的拷贝,内核把接收缓冲区里的报文数据拷贝到用户态缓冲区,之后内核中的报文数据会被释放。3. 全双工:UDP Socket 既能读也能写
UDP的socket既能读,也能写,这个概念叫做全双工,本质原因是:
- UDP 是无连接协议,一个 UDP Socket 仅绑定 “本地 IP + 端口”,不关联任何对端 IP / 端口;
- 因此同一个 Socket,既可以调用
sendto向任意对端发送数据,也可以调用recvfrom接收任意对端发来的数据,天然支持全双工通信,不需要像 TCP 那样区分 “读 / 写方向”。
2.1 UDP 报文头部结构(固定 8 字节,无可变字段)

- 16 位 UDP 总长度:代表整个 UDP 报文(首部 + 数据)总字节长度,16 位数值最大值 65535,因此单个 UDP 报文最大承载 64KB 数据。超过 64KB 数据需要在应用层手动拆分、发送、接收端手动拼装。
- 16 位校验和:校验报文完整性,校验出错直接丢弃整包,不会向上报错。

Linux 内核中,所有网络报文都由 struct sk_buff(套接字缓冲区)管理,画的 sk_buff 结构,是理解内核处理的关键:
- 每个
sk_buff对应一个网络报文,内核通过head、data、tail、end四个指针,管理报文的内存空间; - 内存空间分为两部分:
- 头空间:用来存放链路层头、IP 头、UDP 头(或 TCP 头);
- 线性数据区:存放应用层数据,以及各层头部的实际内容;
所谓的封装和解包,本质就是移动data指针在缓冲区中的指向!,这是内核处理报文的核心逻辑:不需要拷贝数据,只需要调整data指针的位置,就能完成各层的封装 / 解包,效率极高。
结合
sk_buff指针移动,还原完整的封装 / 解包过程:发送方(封装流程):
- 应用层数据序列化后,通过
sendto拷贝到内核,内核创建sk_buff,把应用层数据放到线性数据区的尾部;- UDP 层封装:向前移动
data指针,在头空间构建 UDP 头部(源端口、目的端口、长度、校验和),此时data指针指向 UDP 头部;- IP 层封装:继续向前移动
data指针,构建 IP 头部;- 链路层封装:再向前移动
data指针,构建以太网头部;- 最终
sk_buff交给网卡驱动发送,此时data指针指向以太网头部,网卡直接发送data到tail的数据即可。接收方(解包流程):
- 网卡收到报文,内核创建
sk_buff,把报文数据放到线性数据区,此时data指针指向以太网头部;- 链路层处理:向后移动
data指针,跳过以太网头部;- IP 层处理:继续向后移动
data指针,跳过 IP 头部;- UDP 层处理:再向后移动
data指针,跳过 UDP 头部,此时data指针指向应用层数据;- 内核把这个
sk_buff放到 UDP Socket 的接收缓冲区中,等待应用层recvfrom读取;- 应用层调用
recvfrom,内核把data到tail的应用数据拷贝到用户态缓冲区,之后sk_buff被释放。
如果应用层正在进行报文的解析、处理,会不会影响OS从网络中读取报文?为什么?
不会影响。内核的
sk_buff和接收缓冲区是在内核态维护的,应用层调用recvfrom只是把内核接收缓冲区里的数据拷贝到用户态,之后应用层处理的是用户态的副本;内核会继续独立处理网卡收到的新报文,只要接收缓冲区没满,就会把新报文放到缓冲区中,和应用层的处理过程完全解耦;只有当应用层读取速度过慢,导致接收缓冲区被占满时,内核才会丢弃新的 UDP 报文,这是缓冲区大小限制导致的,不是应用层处理直接影响了内核的读取。
2.2 UDP 四大核心特性
- 无连接:通信前不需要握手建立连接,拿到目标 IP + 端口直接发送数据,建连开销几乎为 0;
- 不可靠传输:无 ACK 确认、无超时重传,报文丢包、乱序时 UDP 协议层不会给应用返回任何错误提示;
- 面向数据报:应用层交付多少字节数据,UDP 就打包成一个完整报文,既不分拆、也不合拼。
举例:sendto 一次性发送 100 字节,接收端必须调用一次 recvfrom 完整收取 100 字节,不能循环 10 次每次收 10 字节;天然自带报文边界,UDP 不存在 TCP 的粘包问题。
- 全双工通信:UDP Socket 可读可写;无发送缓冲区(sendto 数据直接丢给内核发网络层)、有接收缓冲区(接收报文无序,缓冲区满则新来数据直接丢弃)。
2.3 基于 UDP 的主流应用协议 & 落地场景
| 协议 | 用途 |
|---|---|
| DNS | 域名→IP 地址解析 |
| DHCP | 局域网设备自动分配 IP |
| TFTP/NFS | 轻量级文件传输、网络磁盘 |
| BOOTP | 无盘设备开机引导 |
业务选型:优先 UDP:直播、网络游戏、语音通话、视频会议,这类场景允许少量丢包,实时性优先级 > 数据完整性,丢少量数据只会轻微卡顿,延迟过高体验崩盘。
三、TCP协议
3.1TCP协议端格式

我们这里先大致了解:
TCP 报文段分为两大部分:
- TCP 首部(报头):包含所有控制信息(端口号、序号、标志位等),长度可变,范围是20~60 字节。
- 数据部分(有效载荷):应用层交给 TCP 传输的数据,长度可空(比如纯控制报文段),最大长度受限于 MSS(通常是 1460 字节)。
| 字段 | 长度 | 核心作用 | 对应说明 |
|---|---|---|---|
| 16 位源端口号 | 16 位 | 标识发送方的进程端口(传输层地址) | 用来区分同一主机上的不同进程 |
| 16 位目的端口号 | 16 位 | 标识接收方的进程端口 | 比如 HTTP 默认 80、HTTPS 默认 443 |
| 32 位序号(SN) | 32 位 | 本报文段数据的第一个字节的编号 | TCP 是面向字节流的,每个字节都有编号,用于保证数据有序性和去重 |
| 32 位确认序号(ACK) | 32 位 | 期望收到对方下一个报文段的第一个字节的序号 | 只有ACK=1时有效,格式为「已收到的最后一个字节序号 + 1」 |
| 4 位首部长度(数据偏移) | 4 位 | 表示 TCP 首部的长度,单位是4 字节 | 下面会说,核心考点,用来分离报头和数据 |
| 6 位保留 | 6 位 | 保留给未来扩展使用,当前全为 0 | 无实际作用 |
| 6 位标志位 | 6 位 | 控制 TCP 连接的状态(建立、释放、流量控制等) | 包含 URG/ACK/PSH/RST/SYN/FIN |
| 16 位窗口大小 | 16 位 | 接收方的接收缓冲区剩余空间(单位:字节) | 流量控制核心字段,防止发送方发送过快 |
| 16 位检验和 | 16 位 | 校验整个 TCP 报文段(首部 + 数据)是否出错 | 出错则直接丢弃报文段 |
| 16 位紧急指针 | 16 位 | 标识紧急数据的最后一个字节位置 | 只有URG=1时有效,用于处理紧急数据 |
1.理解 TCP 报头和数据分离的关键,理解4位首部程度
- 字段长度:4 位 → 取值范围
0000~1111(十进制 0~15) - 单位约定:TCP 协议约定「首部长度的单位是 4 字节」,因为报头长度必须是 4 字节的整数倍(方便对齐)
- 实际范围:
- 最小值:TCP 首部固定部分是 20 字节(无选项时),所以
20 ÷ 4 = 5→ 首部长度字段最小为 5 - 最大值:4 位最大取值 15,
15 × 4 = 60字节 → TCP 首部最大长度为 60 字节(选项字段最多 40 字节) - 因此首部长度字段的有效取值范围是
[5,15],对应报头长度[20,60]字节
- 最小值:TCP 首部固定部分是 20 字节(无选项时),所以
作用:接收方收到报文段后,先读取这个字段,乘以 4 得到报头总长度,就能直接定位到数据部分的起始位置,完美解决「报头和有效载荷分离」的问题。
2.6 位标志位(控制位)详解
- URG(紧急):
URG=1时,紧急指针有效,报文段包含紧急数据,需优先处理 - ACK(确认):
ACK=1时,确认序号有效,TCP 规定连接建立后所有报文段都必须置为 1 - PSH(推送):
PSH=1时,接收方需立即将数据交给应用层,不等待缓冲区满 - RST(复位):
RST=1时,强制释放异常连接,比如收到非法报文段时 - SYN(同步):
SYN=1时,请求建立连接,三次握手的核心标志,会占用一个序号 - FIN(结束):
FIN=1时,通知对方本方数据发送完毕,请求释放连接,会占用一个序号
3.为什么 TCP 报文段没有「总长度」字段,只有报头长度?
这是 TCP 和 IP 层的配合设计:
- IP 数据报的首部已经包含「总长度」字段,记录了整个 IP 数据报的长度(IP 首部 + IP 数据部分,也就是 TCP 报文段)
- 因此 TCP 报文段的总长度 = IP 数据报总长度 - IP 首部长度
- TCP 数据部分长度 = TCP 报文段总长度 - TCP 首部长度(通过首部长度字段得到)
- 结论:TCP 不需要重复定义总长度字段,通过 IP 层的信息 + 自身的首部长度字段,就能算出数据部分的长度。
4.报头和有效载荷如何分离?
完全依赖4 位首部长度字段:
- 发送方:根据报头实际长度设置该字段(比如无选项时设为 5)
- 接收方:读取该字段后乘以 4,得到报头总长度,数据部分就是「报文段起始位置 + 报头长度」的位置,直接分离。
- 发送方:应用层数据交给 TCP 后,TCP 加上首部(端口号、序号、标志位、校验和等),封装成报文段交给 IP 层。
- 传输中:IP 层负责路由转发,链路层负责数据校验。
- 接收方:
- IP 层收到数据后,交给 TCP;
- TCP 根据源 / 目的端口号,把数据交给对应的进程;
- 根据首部长度字段分离报头和数据;
- 校验和验证数据完整性,出错则丢弃;
- 根据序号和确认序号处理数据有序性、可靠性;
- 根据标志位处理连接建立 / 释放、紧急数据等控制逻辑;
- 根据窗口大小进行流量控制,防止接收缓冲区溢出。
3.2确认应答(ACK)机制

图里的三次握手,就是 TCP 建立连接的过程,分为三步:
- 第一次握手(SYN):客户端给服务器发一个「连接请求包」,只带一个
SYN(同步标志位),意思是:“服务器你好,我想和你建立连接,我的初始序号是 x”。 - 第二次握手(SYN+ACK):服务器给客户端回一个「同意连接包」,带
SYN+ACK,意思是:“客户端你好,我收到你的请求了,同意建立连接,我的初始序号是 y”。 - 第三次握手(ACK):客户端再给服务器回一个「确认包」,带
ACK,意思是:“服务器,我收到你的同意了,连接正式建立!”。
三次握手完成后,双方才进入ESTABLISHED(已建立)状态,之后才能正常收发数据。
1.为什么「前两次握手不能带数据」?
- 连接还没 “真正建立”,状态不允许
前两次握手结束后,双方都还没完全确认对方收到了自己的消息:
- 客户端发了 SYN,服务器回了 SYN+ACK,但服务器不知道客户端有没有收到自己的回复;
- 客户端收到了 SYN+ACK,但还没给服务器发最终确认,服务器还在 “等确认” 的状态。
这时候双方的连接状态是「半开」的,TCP 还没完成初始序列号的同步,也没确认对方的接收能力,根本没法可靠传输数据 —— 就算带了数据,对方也不知道要不要接收、怎么确认。
- 防止攻击,保护服务器资源
如果允许 SYN 包(第一次握手)带数据,会有很大的安全隐患: 攻击者可以疯狂发送带大量数据的 SYN 包,服务器收到后,需要为这些「半开连接」分配内存、资源来处理数据,但这些连接永远不会完成第三次握手。最终服务器的资源会被占满,无法处理正常请求(这就是 SYN Flood 攻击的变种)。
所以 TCP 规范直接规定:SYN 报文(前两次握手的包)不允许携带数据,只能有 TCP 头部。
序列号:TCP给每个字节的数据都进行了排序
这样我们每次进行ACK的时候带上对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发
3.3超时重传机制

场景 1:发送的数据报文丢失
- 主机 A 发送
1~1000字节数据,数据包在网络中丢失; - A 等待预设超时时间后,始终未收到主机 B 的 ACK 确认;
- 触发超时重传,重新发送
1~1000完整数据; - B 收到重传报文,返回确认应答
ack=1001,告知 A 全部数据已接收。
场景 2:数据到达,但 ACK 确认报文丢失
- 主机 A 发送
1~1000数据,主机 B 正常收到; - B 回复
ack=1001确认报文,但 ACK 在传输途中丢失; - A 等待超时无应答,判定数据丢失,再次重传
1~1000; - B 收到重复报文,通过序列号识别重复数据包,直接丢弃重复数据,再次回复 ACK;
- A 收到 ACK 后停止重传,本轮传输完成。
💡核心关键点:序列号是 TCP 天然的去重工具,接收端无需额外逻辑,仅比对 seq 序号即可过滤重复重传包。
超时时间(RTO)的取值存在两难矛盾:
- 超时时间过长:丢包后等待很久才重传,大幅降低传输效率;
- 超时时间过短:网络轻微延迟就误判丢包,频繁重发重复数据包,浪费带宽,加剧网络拥塞。
因此 TCP 不会使用固定超时值,而是动态计算 RTO,适配不同网络时延。
重传间隔呈指数倍增:
- 第 1 次重传等待:
1 * 500ms - 第 2 次重传等待:
2 * 500ms - 第 3 次重传等待:
4 * 500ms - 第 4 次重传等待:
8 * 500ms以此类推,等待时间指数上涨。
当重传次数达到系统预设上限后,TCP 判定对端主机 / 网络故障,直接强制关闭 TCP 连接,释放内核资源。
3.4连接管理
我们知道正常情况下TCP要进行三次握手建立连接,四次挥手断开连接

3.4.1 服务端完整执行链路
socket():创建监听文件描述符,TCP 初始状态CLOSEDbind():绑定固定 IP 与端口listen():切换为LISTEN,内核创建半连接 / 全连接队列等待客户端accept():阻塞等待客户端连接,三次握手完成后返回通信 fd,进入ESTABLISHEDread/write:循环收发业务数据close(connfd):主动发送 FIN,走完四次挥手流程
3.4.2 客户端完整执行链路
socket():创建客户端 fd,初始CLOSEDconnect():发起 SYN 握手报文,状态切换SYN_SENT,阻塞直至握手完成进入ESTABLISHEDread/write:业务数据交互close(fd):主动关闭连接,走完 FIN、ACK、TIME_WAIT 全流程
3.4.3三次握手(建立连接)状态变化
服务端状态流转
CLOSED → LISTEN → SYN_RCVD → ESTABLISHED
CLOSED → LISTEN:调用listen()开启监听,等待外部 SYN 请求;LISTEN → SYN_RCVD:收到客户端 SYN 报文,回复SYN+ACK,连接存入内核半连接队列;SYN_RCVD → ESTABLISHED:收到客户端最终 ACK,握手完成,移入全连接队列,accept()返回。
客户端状态流转
CLOSED → SYN_SENT → ESTABLISHED
CLOSED → SYN_SENT:connect()调用触发,向外发送 SYN 同步报文;SYN_SENT → ESTABLISHED:收到服务端SYN+ACK,回复 ACK,握手完成,connect函数解除阻塞返回。
3.4.4四次挥手(断开连接)状态变化
以客户端主动关闭、服务端被动关闭的标准场景讲解:
客户端(主动关闭方)状态流转
ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
ESTABLISHED → FIN_WAIT_1:调用close()发送 FIN 报文,停止发送数据;FIN_WAIT_1 → FIN_WAIT_2:收到服务端对 FIN 的 ACK,此时客户端只能收数据,不能发;FIN_WAIT_2 → TIME_WAIT:收到服务端 FIN 关闭报文,回复最后一个 ACK;TIME_WAIT → CLOSED:强制等待2MSL(报文最大生存时间),超时后彻底释放连接。
服务端(被动关闭方)状态流转
ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
ESTABLISHED → CLOSE_WAIT:收到客户端 FIN,回复 ACK,此时服务端仍可发送剩余业务数据;CLOSE_WAIT → LAST_ACK:业务数据发送完毕,调用close()发送 FIN 报文;LAST_ACK → CLOSED:收到客户端最后的 ACK,直接关闭连接,无等待阶段。

3.5理解TIME_WAIT机制
如果我们启动服务端监听 8000 端口,客户端建立连接;使用Ctrl+C强制终止服务端程序,服务端作为主动关闭方发送 FIN;立刻重新执行服务端程序,bind()系统调用报错。


CLOSE_WAIT:客户端收到服务端 FIN,尚未关闭自身 fd;FIN_WAIT2:服务端等待客户端 FIN 报文; 此时内核 TCP 连接未完全释放,端口被占用,无法重新绑定。
3.5.1 协议硬性规则
主动关闭 TCP 连接的一端,必须进入TIME_WAIT状态,持续2MSL后才会切换至CLOSED释放端口。
- MSL(Maximum Segment Lifetime):TCP 报文在网络中的最大生存时间;
- RFC1122 标准规定 MSL=2 分钟,Linux(CentOS7/Ubuntu)系统默认简化为 60s;
- 查看系统 MSL 配置:
cat /proc/sys/net/ipv4/tcp_fin_timeout
输出值即为单 MSL 时长,完整 TIME_WAIT 等待时间 = 2× 该数值。
3.5.2 为什么必须等待 2MSL(两大核心作用)
作用 1:保证最后一个 ACK 可靠送达对端
四次挥手最后一步:主动关闭方发送 ACK 给对端(被动关闭方)。
- 若这条 ACK 在网络中丢失,被动关闭方会超时重发 FIN;
- 主动关闭方仍处于 TIME_WAIT,内核连接资源未销毁,可以重新补发 ACK;
- 若没有 2MSL 等待,连接直接销毁,收到重传 FIN 时只能回复 RST,造成对端连接异常断开。
作用 2:隔离旧连接延迟报文,避免脏数据干扰新连接
网络中存在延迟滞留的旧数据包,最长存活时间为 1MSL:
- 等待完整 2MSL,能确保双向传输路径里所有上一轮连接的延迟报文全部过期消失;
- 若立刻重启服务复用端口,新建立的连接可能收到上一次连接残留的乱序报文,引发数据错乱。
Q1:被动关闭连接的一方为什么不会进入 TIME_WAIT?
被动关闭方走完
LAST_ACK后,收到最后一个 ACK 即可直接释放连接,不存在补发报文、隔离旧数据包的需求,协议无需设计等待周期。Q2:大量 TIME_WAIT 堆积会造成什么问题?
- 客户端:耗尽本机临时端口,无法新建 TCP 连接;
- 服务端:端口被持续占用,服务重启失败;
- 内核维护大量连接状态,消耗内存与 CPU 资源。
Q3:2MSL 为什么不是 1MSL?
报文往返需要双向链路,1MSL 仅能保证单向报文过期,2MSL 才能覆盖往返全链路所有延迟数据包。
3.6解决TIME_WAIT状态引起的bind失败的方法
3.6.1大量 TIME_WAIT 产生的业务场景痛点
1. 高并发短连接服务场景
服务端承载海量短连接,客户端请求生命周期极短、每秒新建大量连接;若由服务端主动关闭连接(如清理空闲超时客户端),每一次关闭都会生成一条TIME_WAIT连接。
2. 五元组资源耗尽问题
TCP 连接由五元组唯一标识:源IP、源端口、目的IP、目的端口、协议
- 服务端
目的IP、目的端口、协议固定不变; - 大量
TIME_WAIT连接会持续占用五元组资源; - 当新客户端的源 IP + 源端口与旧 TIME_WAIT 连接重复时,新建连接会失败,同时服务重启执行
bind()会直接抛出Address already in use。
3. 原生 TCP 限制
默认情况下,端口在存在TIME_WAIT连接时,不允许程序重新bind监听该端口,对开发调试、高并发线上服务不友好。
3.6.2SO_REUSEADDR 原理与代码实现
1. 核心作用
通过setsockopt将SO_REUSEADDR置 1,允许同一端口绑定多个不同 IP的 socket,同时允许在端口存在 TIME_WAIT 连接时,重新执行bind()监听该端口,直接解决端口占用报错。
2. 标准 C 语言代码
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
⚠️ 调用时机:必须在
bind()之前设置,若已完成绑定再设置该选项不会生效。
| 选项 | 核心能力 | 适用场景 |
|---|---|---|
| SO_REUSEADDR | 允许端口存在 TIME_WAIT 时 bind;同端口绑定多 IP | 单进程服务、开发调试、普通 Web 服务 |
| SO_REUSEPORT | 允许多个进程 / 线程同时 bind 同一个 IP + 端口 | 多进程负载均衡服务(Nginx 多 worker、网关集群) |
3.7理解CLOSE_WAIT状态
// 读取客户端数据失败(客户端关闭连接)时,注释掉关闭fd代码
if (!ret) {
printf("[client %s:%d] disconnect!\n", ip.c_str(), port);
// new_sock.Close(); // 关键代码被注释,未释放通信套接字
break;
}
启动服务端,客户端建立连接,两端均为ESTABLISHED正常传输状态;客户端主动关闭程序,发送 FIN 报文执行四次挥手;查看 TCP 连接状态:
- 客户端:
FIN_WAIT2(等待服务端 FIN 报文) - 服务端:
CLOSE_WAIT(永久卡住,无法走完挥手流程)
tcp 0 0 127.0.0.1:49958 127.0.0.1:9090 FIN_WAIT2
tcp 0 0 127.0.0.1:9090 127.0.0.1:49958 CLOSE_WAIT 5038/./dict_server
IN_WAIT2:客户端已发 FIN、收到服务端 ACK,等待服务端 FIN;CLOSE_WAIT:服务端收到客户端 FIN 并回复 ACK,但迟迟不发送 FIN,挥手流程停滞。
3.7.1CLOSE_WAIT 底层原理(结合四次挥手)
- 客户端主动
close()发送 FIN → 服务端 TCP 内核收到 FIN; - 内核自动回复 ACK,服务端连接进入
CLOSE_WAIT状态; - 协议规则:进入
CLOSE_WAIT后,必须由应用层主动调用 close (),内核才会发送 FIN 报文,进入LAST_ACK完成挥手; - 若应用层不关闭 socket 文件描述符:内核不会发送 FIN,连接永久卡在
CLOSE_WAIT,占用内核资源、文件描述符。
3.7.2大量 CLOSE_WAIT 带来的线上危害
- 文件描述符泄漏:持续堆积会触发
too many open files,无法接受新客户端连接; - 内存占用:内核持续维护失效连接状态,消耗服务器内存;
- 客户端连接卡死:客户端长期停留在
FIN_WAIT2,无法正常释放连接。
服务端读取客户端数据返回失败(代表客户端关闭)后,业务代码未调用close()释放通信 socket,属于典型 IO 逻辑 BUG。
Q:为什么内核收到 FIN 后不会自动发送 FIN,必须等应用层 close?
A:TCP 是全双工协议,客户端仅关闭发送通道,服务端可能仍有剩余业务数据需要发给客户端;内核无法判断业务是否还有数据待发送,因此将关闭决定权交给应用层,只有调用
close()才代表业务无数据要发送,内核才会发送 FIN。
3.8滑动窗口
传统一发一收模型:发送 1 段数据,必须等待 ACK 返回才能发送下一段。 在网络往返时延 RTT 较长时,大量时间浪费在等待应答上,吞吐极低。

3.8.1 滑动窗口核心思想
一次性发送窗口内所有数据,将多段报文的等待时延重叠,大幅提升传输效率。
- 窗口大小:无需等待 ACK 即可连续发送的最大字节数,示例窗口 = 4000 字节(4 个 1000 字节分段);
- 发送逻辑:窗口未满时持续发包;收到对应 ACK 后,窗口向右滑动,释放缓冲区空间,补充发送新数据;
- 内核配套:操作系统维护发送缓冲区,缓存所有未收到 ACK 的数据,用于丢包重传;
- 性能关系:窗口越大,单位时间传输数据量越高,网络吞吐率越高。

- 初始窗口覆盖
1001~4000四段数据,一次性全部发送; - 收到
ack=2001(代表 1001~2000 已完整接收); - 窗口向右滑动一格,覆盖
2001~5000,此时可发送5001~6000新分段。

场景 1:数据报文抵达,但 ACK 丢失
TCP 采用累计确认机制,后序 ACK 可以覆盖前面所有已接收数据。 例如ack=4001会直接确认 1~3000 全部数据,中间丢失的ack=2001/3001无需重传,不会影响传输,无额外开销。这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认

场景 2:数据报文直接丢失(快重传机制)
- 假设
1001~2000报文丢失,主机 B 持续收到后续分段,会重复回复ack=1001; - 发送端连续收到3 次重复 ACK,无需等待超时定时器,立刻重传丢失的
1001~2000分段; - 重传后接收端补齐数据,返回最新累计 ACK,传输恢复。
- 这个时候接收端收到了1001之后,再次返回的ACK就是7001了(因为2001-7000)接收端其实之前 就已经收到了,放到了接收端操作系统内核的接收缓冲区中

💡 快重传价值:跳过漫长的超时退避等待,快速修复丢包,大幅降低延迟。
3.9流量控制
接收端处理数据能力存在上限,若发送端发包速率过快,会填满接收内核缓冲区,引发丢包、连锁超时重传,严重降低传输效率。 流量控制的核心逻辑:由接收端告知发送端自身剩余接收容量,限制发送端最大发送数据量,避免接收缓冲区溢出。
3.9.1流量控制完整工作流程
1. 窗口大小传递规则
TCP 头部内置 16 位「窗口大小」字段,接收端每次回复 ACK 时,将当前接收缓冲区剩余空间填入该字段,同步给发送端:
- 窗口数值越大,代表接收端空闲缓存越多,允许发送更多数据,吞吐越高;
- 接收端缓存即将占满时,主动缩小窗口值,通知发送端降低发送速度;
- 发送端严格遵循:单次可发送数据总量 ≤ 接收端通告窗口大小。
2. 零窗口场景(接收缓冲区完全占满)
- 接收端缓存写满,ACK 中窗口置
0; - 发送端立即停止发送业务数据;
- 为避免「窗口更新 ACK 丢失」导致永久断连,发送端会周期性发送窗口探测报文;
- 接收端收到探测包后,回复最新窗口值;若缓存释放,窗口恢复非 0,发送端恢复传输。

3.9.1窗口扩大因子(解决 16 位窗口上限问题)
1. 原生限制
TCP 头部窗口仅 16 位,理论最大值 2^16-1 = 65535 字节,高速网络下窗口过小会限制吞吐。
2. 扩展方案(Window Scale)
TCP 头部 40 字节可选字段中携带窗口扩大因子 M,实际窗口计算公式: 真实窗口 = 头部16位窗口值 << M 通过左移操作,可将窗口上限提升至 GB 级别,适配万兆、高速链路传输。
- 为什么需要窗口探测包? 若接收端窗口从 0 恢复为非 0 的 ACK 报文在网络中丢失,发送端会永久停发数据。窗口探测包可主动触发接收端返回最新窗口,打破死锁。
- 滑动窗口的两个限制是什么? 发送端实际发送上限 = min (拥塞窗口 cwnd, 接收通告窗口 rwnd),同时受流量控制、拥塞控制双重约束。
- 窗口扩大因子何时协商? 仅在三次握手的 SYN 报文携带协商,连接建立后无法修改 M 值。
3.10拥塞控制
滑动窗口解决了接收端处理能力限制(流量控制),但无法解决中间网络链路拥堵问题: 网络中存在多台主机共享链路,若连接刚建立就大量发包,会瞬间填满路由器缓存,造成大面积丢包、全网延迟飙升。 拥塞控制核心思路:先少量发包探测网络状态,逐步提升发送速率,一旦检测到拥堵立刻减速,平衡传输速度与网络负载。

- 拥塞窗口 cwnd:由发送端维护,代表当前网络允许发送的最大字节;
- 接收窗口 rwnd:流量控制,由接收端通告,代表接收缓冲区剩余容量;
- 实际发送窗口 = min (cwnd, rwnd),发送速率同时受网络、接收端双重限制;
- 慢启动阈值 ssthresh:区分「慢启动指数增长」与「拥塞避免线性增长」的临界值。
3.10.1阶段 1:慢启动(Slow Start)
1. 初始规则
- 连接建立初期,
cwnd = 1; - 每收到 1 个完整 ACK,
cwnd += 1,一轮 RTT 窗口直接翻倍,指数级快速扩张; - “慢启动” 仅指初始发包量小,实际窗口增长速度极快。
2. 终止条件
当 cwnd >= ssthresh,退出慢启动,进入拥塞避免阶段
初始 cwnd=1,发送 1 段数据;收到 ACK 后 cwnd 变为 2,一次性发送 2 段;收到 2 个 ACK 后 cwnd 变为 4…… 以此类推,窗口指数上涨,快速探测网络承载上限。

3.10.2阶段 2:拥塞避免(Congestion Avoidance)
1. 增长规则(加法增大)
越过 ssthresh 阈值后,不再指数翻倍: 每经过 1 个完整 RTT 往返周期,cwnd += 1,窗口线性缓慢增长,平缓试探网络极限,避免瞬间压垮链路。
此时图表中曲线斜率明显放缓,从陡峭指数曲线变为平缓斜线。
TCP 通过两种丢包事件区分拥堵程度,执行不同减速策略:
3.10.3场景 1:超时重传(严重网络拥堵)
- 判定逻辑:报文丢失且长时间无 ACK 返回,触发超时定时器;
- 处理规则:
ssthresh = cwnd / 2(乘法减小,阈值减半)cwnd = 1,重新回到慢启动阶段,极低速率重新探测网络;
- 适用场景:链路严重拥塞、大面积丢包。
3.10.4场景 2:收到 3 次重复 ACK(轻度丢包,快重传触发)
- 判定逻辑:单段报文丢失,但后续报文正常抵达,接收端持续返回重复 ACK;
- 处理规则:仅将
ssthresh = cwnd / 2,cwnd 直接等于新 ssthresh,进入拥塞避免,无需重置到 1; - 优势:网络仅轻微波动,无需退回到极低发送速率,传输吞吐下降幅度更小。
| 维度 | 流量控制 Flow Control | 拥塞控制 Congestion Control |
|---|---|---|
| 管控对象 | 接收端主机缓冲区 | 整条中间传输网络链路 |
| 控制窗口 | 接收窗口 rwnd(接收端通告) | 拥塞窗口 cwnd(发送端计算) |
| 增长逻辑 | 随接收缓存空闲空间动态变化 | 慢启动指数增长、拥塞避免线性增长 |
| 触发减速条件 | 接收缓冲区即将占满 | 网络丢包(超时 / 重复 ACK) |
| 核心目标 | 防止接收端缓存溢出丢包 | 防止路由器链路拥堵、全网雪崩 |
3.11延迟应答
接收端不收到数据就立刻回复 ACK,而是短暂等待一段时间再应答,带来两大收益:
1.通告更大接收窗口,提升吞吐 举例如图中场景:接收缓冲区总大小 1M,一次性收到 500K 数据
- 立刻应答:缓冲区剩余 500K,ACK 携带窗口 = 500K;
- 等待 200ms 再应答:应用层快速消费完 500K 数据,缓冲区空闲回到 1M,ACK 携带窗口 = 1M; 窗口数值越大,发送端可一次性发送更多数据,整体传输效率更高。
2.减少网络中 ACK 报文数量,降低带宽开销 支持累计确认,连续多段数据合并为单条 ACK 回复,避免每收到一段数据就单独回包,减少往返小包数量。 时序图示例:收到1~1000、1001~2000两段数据,仅回复一条ack=2001,而非两次独立 ACK。

为避免 ACK 长时间滞留、触发发送端超时重传,系统设置两条硬性约束,满足任意一条就立即回复 ACK:
- 数量限制:每收到 2 个分段,强制回复一次 ACK(通用系统默认 N=2);
- 时间限制:最大等待超时 200ms,无论收到几段数据,超时必须应答。 不同操作系统数值略有差异,但 200ms、每 2 段应答是行业通用标准。
3.12捎带应答
承接上一节延迟应答,绝大多数 C/S 业务是「一问一答」交互模型:客户端发送请求,服务端一定会返回业务响应数据。 如果单独发送一条纯 ACK 报文,会额外消耗一次网络往返开销;捎带应答将确认信息和业务回复合并在同一个 TCP 报文段传输,省去独立 ACK 小包。

3.13面向字节流
创建 TCP socket 时,操作系统内核会同步分配两块独立缓冲区:发送缓冲区、接收缓冲区,这是 TCP 字节流特性的底层支撑。
3.13.1 发送缓冲区工作逻辑
- 应用调用
write/send写入数据,数据不会立刻发往网络,先存入内核发送缓冲区; - 大数据场景:缓冲区数据超出 MSS 最大分段大小,内核自动拆分为多个 TCP 报文发送;
- 短数据场景:少量字节会在缓冲区暂存,等待凑齐足量数据、或延迟应答时机统一发送(Nagle 算法),减少小包数量。
3.13.2 接收缓冲区工作逻辑
- 网卡收到对端 TCP 报文,内核将数据存入接收缓冲区;
- 应用调用
read/recv时,仅从内核缓冲区拷贝数据,和对方发送时的分段、调用次数完全解耦。
3.13.3 全双工定义
一条 TCP 连接同时持有发送、接收双向缓冲区,两端可同时收发数据,互不阻塞、互不干扰,该特性称为全双工。
3.13.4面向字节流核心特征:读写完全解耦
缓冲区隔离了应用层读写操作,发送方write调用次数、单次写入长度,和接收方read完全不需要一一对应:
- 发送侧示例:写入 100 字节,可以
write(fd, buf, 100)一次写完,也可以循环 100 次每次写 1 字节; - 接收侧示例:读取这 100 字节,可一次性
read(buf,100)读完,也可以分 100 次每次读 1 字节; - 核心本质:TCP 只传输连续字节序列,不保留应用层数据包边界
3.14粘包问题
3.14.1粘包核心成因
1. 底层根本原因
TCP 是面向字节流协议,头部仅有序列号,不存在 UDP 那样的「报文长度」字段,内核只负责把字节按序号排序存入缓冲区,不会保留应用层数据包边界:
- 传输层视角:数据是一段段独立 TCP 报文;
- 应用层视角:读取到的是连续无分割的字节流,无法区分哪一段是完整业务包。
2. 两类粘包场景
- 发送粘包:多次短数据写入发送缓冲区,内核合并为单条 TCP 报文发出;
- 接收粘包:多条 TCP 报文的数据堆积在接收缓冲区,一次
read读取多段业务数据; 最终应用层分不清包与包的分割点,出现半包、粘包故障。
3.14.2三种通用解决方案(核心思路:人为定义包边界)
方案 1:定长数据包
约定每个业务数据包固定字节长度,读取时按固定大小循环截取。 适用场景:结构体固定、指令长度统一的简单通信; 示例:struct Request固定大小,每次从缓冲区读取sizeof(Request)字节。
方案 2:变长包 + 长度头(工业主流方案)
数据包头部预留固定长度字段(如 4 字节 uint32),存储后续包体的字节长度; 读取逻辑:
- 先读取 4 字节长度值
len; - 再从缓冲区读取
len字节作为完整包体; 优势:支持任意长度可变消息,解析稳定,HTTP、RPC 框架均采用该思路。
方案 3:特殊分隔符分割
在每条业务包末尾添加唯一自定义分隔符(如\r\n、#); 约束:业务正文内部不能出现该分隔符,否则会出现拆包错乱; 适用场景:文本类协议(HTTP、Redis 协议)。
3.14.3关键对比:UDP 不存在粘包问题
- UDP 头部自带报文长度字段,内核完整保留报文边界;
- 内核交付规则:一次
recv只能读取一个完整 UDP 报文,不会出现半段数据; - 应用层特征:要么收到完整数据包,要么收不到,不存在多包合并、半包的情况;
- 补充:UDP 仅存在丢包问题,无粘包 / 半包故障。
Q:粘包是 TCP 协议本身的缺陷吗?
A:不是。TCP 设计目标是可靠传输字节流,不关心应用层业务边界;粘包是应用层未做分包处理导致的业务问题,需要上层自定义协议解决。
Q:Nagle 算法会加重粘包吗?
A:会。Nagle 算法会缓存短数据凑大包发送,加剧发送侧粘包;实时交互场景(游戏、IM)可关闭该算法。
3.15TCP的异常情况
3.15.1 进程正常终止
程序调用close()或进程退出,操作系统自动回收 socket 文件描述符,内核发送 FIN 报文,完整执行四次挥手流程,和主动正常关闭无区别。
3.15.2 机器重启
系统内核销毁所有 socket 资源,同样会发送 FIN 完成挥手,流程与进程终止一致。
3.15.3 机器断电 / 网线断开(无 FIN 报文)
- 对端不会收到任何 FIN 报文,内核连接状态暂时保留;
- 触发两种检测机制释放失效连接:
- TCP 保活定时器:内核周期性发送探测报文,多次无应答则判定连接失效,释放资源;
- 应用层写入检测:当程序调用
write发送数据时,内核检测链路不可达,直接返回 RST 复位连接。
- 上层应用补充保活:如 HTTP 长连接、IM 软件(QQ)会自定义心跳包,断线后主动重连。
3.16TCP 完整核心机制小结
TCP 设计复杂的根本目标:兼顾传输可靠性与网络吞吐性能
3.16.1 可靠性保障机制(杜绝丢包、乱序、重复、损坏)
- 校验和:检测报文传输过程中数据损坏;
- 序列号:为每个字节编号,实现有序接收、重复包过滤;
- 确认应答 ACK:告知发送端已接收的数据边界;
- 超时重传:报文丢失后自动补发,兜底可靠传输;
- 完整连接管理:三次握手建立、四次挥手断开,保证连接生命周期合法;
- 流量控制:限制发送速率,防止接收端缓冲区溢出。
3.16.2 性能优化机制(降低延迟、提升吞吐、减少小包)
- 滑动窗口:批量发送多段数据,重叠 RTT 等待时延;
- 快重传:收到 3 次重复 ACK 立刻重传,跳过超时等待;
- 延迟应答:合并多条 ACK、等待应用释放缓存以通告更大窗口;
- 捎带应答:将 ACK 搭载在反向业务数据上,省去独立 ACK 小包;
- 拥塞控制:慢启动 + 拥塞避免,探测网络承载上限,避免全网拥堵。
3.16.3 配套定时器机制
TCP 内置多组定时器驱动全流程:超时重传定时器、保活定时器、TIME_WAIT 2MSL 定时器、零窗口探测定时器。
3.17TCP 与 UDP 对比 & 场景选型
核心选型逻辑
不存在绝对优劣,二者是适配不同业务需求的工具,根据实时性、可靠性要求取舍。
TCP 适用场景
要求数据 100% 送达、有序不丢失,对延迟容忍度较高: 文件下载、数据库交互、网页访问、配置同步、金融事务。
UDP 适用场景
追求超低延迟、允许少量丢包,对实时性要求优先于可靠: 直播视频、语音通话、网络游戏、广播 / 组播消息。
关键区分
| 维度 | TCP | UDP |
|---|---|---|
| 连接属性 | 面向连接,握手 / 挥手 | 无连接,直接发包 |
| 可靠性 | 可靠有序,无丢包无重复 | 不可靠,可能丢包乱序 |
| 头部开销 | 20~60 字节,控制字段多 | 仅 8 字节,极简头部 |
| 边界特性 | 字节流,存在粘包问题 | 面向报文,天然保留包边界 |
| 内置控制 | 流量 / 拥塞控制、重传、窗口 | 无任何内置控制逻辑 |
3.18经典面试题:如何用 UDP 实现可靠传输
思路:在应用层复刻 TCP 全套可靠机制,自主实现以下核心逻辑:
- 自定义序列号:为每条报文分配序号,保证接收端有序重组、过滤重复包;
- 自定义 ACK 确认应答:接收端返回确认号,告知发送端已接收数据;
- 超时重传定时器:发送报文后计时,无 ACK 则自动重发;
- 滑动窗口、流量控制、拥塞控制(可选,优化吞吐);
- 心跳保活、连接握手 / 断开逻辑(可选,管理连接状态)。


179

被折叠的 条评论
为什么被折叠?



