mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4959 字
22 分钟
Linux 网络编程(7):TCP 协议相关知识 2

TCP 协议相关知识 2#

本节继续总结 TCP 协议,首先从字节流特性出发分析粘包问题,并整理分隔符、固定长度、长度字段和短连接等常见消息边界处理方法。

随后介绍心跳机制、TCP KeepAlive、Nagle 算法和拥塞控制,并对 TCP、UDP 及基于 UDP 构建可靠传输的思路进行综合比较。

一、TCP 粘包问题#

1. 什么是粘包#

TCP 是一种面向字节流的传输协议。

发送端多次调用 send 时,TCP 只把这些数据看成一串连续的字节,并不记录应用程序每一次 send 的边界。接收端调用 recv 时,读取到的是当前接收缓冲区中能够取得的一段字节。

因此可能出现:

  • 发送端连续发送的多个数据包,被接收端一次 recv 读出;
  • 发送端一次发送的数据,被接收端分多次 recv 读出;
  • 一次 recv 读到前一个数据包的结尾和后一个数据包的开头。

所谓“粘包”,本质上不是 TCP 把数据传错了,而是应用层没有办法从连续字节流中识别每一条消息的边界

2. 为什么会出现粘包#

常见原因包括:

  • TCP 只负责可靠地传输字节流,不保留消息边界;
  • 发送端和接收端都存在缓冲区;
  • sendrecv 并不是一一对应的;
  • 操作系统可能合并较小的数据后再发送;
  • 网络状况和接收方读取速度会影响每次 recv 得到的数据量。

粘包的形成位置分成两类:

  • 发送端粘包:多个较小数据在发送缓冲区中被合并后发送,Nagle 算法也可能促成这种合并;
  • 接收端粘包:接收程序没有及时读取,或一次读取了缓冲区中的多条消息,导致多条应用层数据连在一起。

需要牢记:

一次 send ≠ 对方的一次 recv

TCP 能保证字节的顺序和可靠性,但应用程序必须自己设计“如何拆包”。

二、解决粘包问题的四种方法#

方法一:设置开始或结束标志#

在每条消息的开始或结束位置放置特殊标志。接收端不断读取数据,遇到该标志后,就认为一条完整消息结束。

例如:

你好\n吃饭了吗\n

其中 \n 可以作为结束标志。

优点:

  • 实现思路简单;
  • 消息长度可以变化。

缺点:

  • 正文中也可能出现相同标志;
  • 如果正文允许出现标志,需要设计转义规则;
  • 接收端要在缓存中查找分隔符。

方法二:固定数据包大小#

规定每条消息都占固定数量的字节,例如每个包固定为 100 字节。接收端每收满 100 字节,就得到一条完整消息。

优点:

  • 接收端容易判断消息边界;
  • 拆包逻辑简单。

缺点:

  • 数据不足固定长度时会浪费空间;
  • 数据超过固定长度时仍要额外拆分;
  • 不适合消息长度变化很大的场景。

方法三:先发送数据长度,再发送数据#

发送端先发送一个固定长度的“包头”,在包头中说明后续数据的大小;接收端先取得长度,再按该长度读取正文。

数据格式可以表示为:

┌──────────────┬────────────────────────┐
│ 数据长度 │ 数据内容 │
│ 固定大小 │ 长度由前面的字段决定 │
└──────────────┴────────────────────────┘

接收过程:

先接收长度字段
根据长度申请空间
再接收指定长度的数据
还原并处理完整消息

优点:

  • 适合可变长度消息;
  • 接收端能够准确知道还需要读取多少字节;
  • 是实际项目中常用的应用层封包方式。

缺点:

  • 需要分别处理包头和包体;
  • 必须校验长度,防止错误或恶意长度导致内存问题。

1. 示例结构体#

struct Node
{
int a;
char b[10];
long c;
};

2. 发送端代码#

struct Node n1;
int nSize = sizeof(n1);
// 先发送数据包大小
send(sockClient, (char*)&nSize, sizeof(int), 0);
// 再发送数据包
send(sockClient, (char*)&n1, sizeof(n1), 0);

这段代码先让接收端知道结构体数据有多大,然后再发送结构体本身。

3. 接收端代码#

int nPackSize = 0;
// 先接收数据包大小
recv(sockClient, (char*)&nPackSize, sizeof(int), 0);
// 根据收到的大小申请空间
char* buf = new char[nPackSize];
// 再接收数据包
recv(sockClient, buf, nPackSize, 0);
delete[] buf;

4. 将接收缓冲区解释为结构体#

方式一:使用结构体指针。

struct Node* p = (struct Node*)buf;
cout << p->a << endl;

方式二:复制出一个结构体对象。

struct Node p1 = *(struct Node*)buf;
cout << p1.a << endl;

区别:

  • p 指向 buf 中的原始数据,不会另外复制一个结构体;
  • p1 会把 buf 中的内容复制到新的结构体对象中。

方法四:使用短连接#

每发送一条或一组数据就建立一次连接,发送结束后关闭连接。连接关闭可作为本次数据结束的标志。

优点:

  • 消息边界容易确定。

缺点:

  • 频繁进行三次握手和四次挥手;
  • 连接建立与释放的开销较大;
  • 不适合需要频繁通信的场景。

四种方法对比#

方法消息长度优点主要问题
开始/结束标志可变直观、容易理解标志冲突,需要转义
固定包大小固定拆包简单浪费空间,灵活性差
长度字段 + 数据可变通用、边界明确要处理不完整读写并校验长度
短连接可变可用断开表示结束建连、断连开销大

三、心跳机制#

1. 为什么需要心跳#

TCP 长连接建立后,如果双方长时间没有业务数据,仅靠“当前没有收到消息”不能判断:

  • 对方仍然在线,只是暂时没有发送数据;
  • 对方程序已经崩溃;
  • 对方主机断电;
  • 中间网络已经断开;
  • 连接实际上已经失效。

因此,可以定期发送很小的固定消息,用来检查对端是否仍然存活,这就是心跳机制

2. 基本工作过程#

客户端 服务端
│────── 心跳请求 ──────────>│
│<───── 心跳响应 ───────────│
│ │
│ 等待一个固定时间间隔 │
│────── 心跳请求 ──────────>│
│<───── 心跳响应 ───────────│

如果在规定时间内连续多次没有得到响应,程序可以认为连接已经失效,并执行:

  • 关闭当前连接;
  • 清理连接资源;
  • 通知上层业务;
  • 必要时尝试重新连接。

3. 两种实现方式#

应用层自定义心跳#

通信双方自行规定心跳包和响应包。

特点:

  • 可以根据具体业务设置时间间隔和失败次数;
  • 可以携带业务状态;
  • 应用程序可以明确知道对端业务是否仍在正常运行;
  • 需要自己编写定时、超时和重连逻辑。

TCP KeepAlive#

通过套接字选项开启 TCP 自带的保活检测:

int keepAlive = 1;
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE,
(char*)&keepAlive, sizeof(keepAlive));

特点:

  • 由操作系统的 TCP 协议栈完成;
  • 主要判断 TCP 连接是否仍然可达;
  • 默认探测时间通常比较长,具体参数与操作系统有关;
  • 它不能完全替代业务层心跳,因为 TCP 连接正常不代表应用业务一定正常。

四、Nagle 算法#

1. 作用#

如果应用程序频繁发送很小的数据包,大量 TCP/IP 首部会增加网络开销。

Nagle 算法的目标是:

尽量把多个小数据合并成较大的数据段再发送,减少“小包”数量,提高网络利用率。

Nagle 算法通常默认开启。

2. 发送条件#

满足下列条件之一时,数据可以被发送:

  1. 数据长度达到 MSS;
  2. 数据中包含 FIN
  3. 设置了 TCP_NODELAY,即关闭 Nagle 算法;
  4. 未设置 TCP_CORK 选项,并且此连接上先前发送的小数据已经全部收到确认;
  5. 前面条件都不满足,但等待达到超时时间,通常按约 200 ms 理解。

3. MSS#

MSS 是 Maximum Segment Size,即最大报文段数据长度

它表示一个 TCP 报文段中能够承载的最大应用数据量,不包含 TCP 首部和 IP 首部。

4. Nagle 算法的优缺点#

优点:

  • 减少小数据包的数量;
  • 减少协议首部开销;
  • 提高带宽利用率。

缺点:

  • 为了等待更多数据,可能增加发送延迟;
  • 对交互性强、要求低延迟的程序不一定合适。

例如终端输入、实时操作等场景,可能更关注响应速度,因此会考虑关闭 Nagle 算法。

5. 使用 TCP_NODELAY 关闭 Nagle 算法#

int value = 1;
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY,
(char*)&value, sizeof(int));

含义:

  • TCP_NODELAY = 1:关闭 Nagle 算法,小数据可以更及时地发送;
  • TCP_NODELAY = 0:允许使用 Nagle 算法。

关闭 Nagle 并不等于程序一定更快,它是在“更低延迟”和“更少网络开销”之间作选择。

TCP_CORK 会让发送端尽量先积累数据、再组成较大的报文段发送;它与 TCP_NODELAY 的关注点不同,而且主要见于 Linux。当前阶段先记住它会影响“小数据是否立即发送”即可。

五、TCP 拥塞控制#

1. 什么是网络拥塞#

网络中的路由器和交换机,其处理速度与缓存空间都是有限的。

当发送方送入网络的数据过多、过快时,可能出现:

  • 中间设备排队加剧;
  • 网络时延升高;
  • 缓存被占满;
  • 数据包丢失;
  • 发送方超时后再次重传;
  • 重传又进一步增加网络负担。

如果不进行控制,可能形成“越丢越重传,越重传越拥塞”的恶性循环。

拥塞控制的目标是根据网络状态调整发送速度,避免向网络中一次注入过多数据。

拥塞控制还可以让共享网络资源的连接获得相对更公平的使用机会。但不同连接的网络路径、RTT 和丢包情况并不相同,因此这种公平是相对的,不是理论上的绝对平均。

2. 流量控制与拥塞控制的区别#

对比项流量控制拥塞控制
保护对象接收方整个传输网络
主要问题接收方来不及处理网络设备和链路负担过重
关键窗口接收窗口 rwnd拥塞窗口 cwnd
信息来源接收端通告的可用缓冲区ACK、丢包、超时等网络反馈

发送方真正能够发送而未确认的数据量,通常受两者共同限制:

实际发送窗口 = min(rwnd, cwnd)

3. 两个重要变量#

拥塞窗口 cwnd#

由发送方维护,用于限制当前可以向网络中发送但尚未确认的数据量。

  • 网络状况良好时,逐步增大;
  • 检测到拥塞时,减小。

慢开始门限 ssthresh#

用于决定使用慢开始还是拥塞避免:

  • cwnd < ssthresh:执行慢开始;
  • cwnd > ssthresh:执行拥塞避免;
  • cwnd == ssthresh:两种算法都可以进入,具体以实现为准。

4. 四种拥塞控制算法#

4.1 慢开始#

“慢开始”是指一开始的拥塞窗口较小,并不是窗口增长得慢。

基本过程:

  1. 连接刚开始时,把 cwnd 设置得较小;
  2. 收到确认后逐步增加 cwnd
  3. 在理想化模型中,每经过一个 RTT,窗口大约扩大为原来的两倍;
  4. 当窗口达到 ssthresh 后,转入拥塞避免。

示意:

RTT 次数: 0 1 2 3 4
cwnd: 1 2 4 8 16 MSS

它采用近似指数增长,目的是快速探测当前网络能够承受的发送量。

4.2 拥塞避免#

cwnd 达到慢开始门限后,如果仍然按指数增长,网络很容易迅速拥塞。因此转入较平缓的增长阶段。

  • 慢开始:指数增长;
  • 拥塞避免:线性增长;
  • 每经过一个 RTT,cwnd 大约增加 1 MSS。

示意:

慢开始: 1 → 2 → 4 → 8 → 16
拥塞避免: 16 → 17 → 18 → 19 → 20

4.3 超时后的处理#

重传定时器超时通常说明网络中出现了较严重的拥塞。

调整过程:

ssthresh = 当前 cwnd / 2
cwnd = 1 MSS
重新进入慢开始

这是一种较强的降速方式。

4.4 快重传#

正常情况下,接收方收到失序报文段后,会重复确认自己下一步仍然希望得到的序号。

当发送方连续收到 3 个重复 ACK 时,可以推断某个报文段很可能已经丢失,不必等待重传定时器超时,立即重传缺失的报文段。

这就是快重传

作用:

  • 更早发现丢包;
  • 减少等待超时造成的停顿;
  • 提高恢复速度。

4.5 快恢复#

连续收到重复 ACK,说明虽然有个别报文段丢失,但网络仍然能够继续传输后面的报文段,因此拥塞程度通常没有“超时”那么严重。

基本过程:

收到 3 个重复 ACK
立即重传丢失的报文段
ssthresh = 当前 cwnd / 2
cwnd = ssthresh + 3 MSS
每多收到一个重复 ACK,cwnd 再增加 1 MSS
收到新的 ACK 后退出快恢复,进入拥塞避免

4.6 两种丢包信号的处理对比#

发现方式对拥塞程度的判断典型处理
重传超时拥塞较严重ssthresh 减半,cwnd 降至较小值,重新慢开始
3 个重复 ACK仍有数据能够到达,拥塞相对较轻快重传并进入快恢复

4.7 四种算法之间的关系#

连接开始
慢开始(指数增长)
↓ 达到 ssthresh
拥塞避免(线性增长)
├─ 发生超时 → 大幅减小 cwnd → 慢开始
└─ 3 个重复 ACK → 快重传 → 快恢复 → 拥塞避免

六、TCP 协议总结#

1. TCP 的三个核心特点#

TCP 是:

  1. 面向连接的协议;
  2. 可靠传输的协议;
  3. 基于字节流的传输协议。

2. 面向连接#

通信前,客户端和服务端需要先建立连接。

  • 建立连接:三次握手;
  • 释放连接:四次挥手;
  • 一条 TCP 连接的两个端点之间进行点对点通信。

3. 可靠传输#

TCP 通过多种机制实现可靠传输,例如:

  • 序列号 seq
  • 确认号 ack
  • 确认机制;
  • 超时重传;
  • 快重传;
  • 校验;
  • 失序数据重排;
  • 合理分段。

4. 字节流与粘包#

TCP 不保留应用层消息边界,因此应用程序必须自行制定消息格式。

四种拆包方法:

  1. 先发送包大小,再发送数据;
  2. 添加开始或结束标志;
  3. 固定包大小;
  4. 使用短连接。

5. TCP 的控制机制#

  • 流量控制:通过滑动窗口避免接收方来不及处理;
  • 拥塞控制:通过慢开始、拥塞避免、快重传、快恢复调节网络中的数据量;
  • 心跳或保活:发现长连接是否已经失效;
  • Nagle 算法:减少网络中的小数据包。

6. TCP 是否支持广播#

TCP 面向连接,一条连接的通信端点是确定的,因此不能像 UDP 那样直接进行广播或组播。

七、扩展知识:协议选择与“可靠 UDP”#

1. 一个应用不一定只使用一种传输协议#

协议的选择应该服从具体业务,而不是简单地认为“整个软件只能使用 TCP”或“整个软件只能使用 UDP”。

同一个功能较复杂的应用,可以在不同场景中组合使用 TCP 和 UDP。例如:

  • 游戏中的实时状态更新更看重及时性,可以考虑 UDP;
  • 游戏中的文字聊天、购买或赠送物品更看重可靠性,可以考虑 TCP;
  • 即时通信中的文字、文件传输更看重完整可靠,可以考虑 TCP;
  • 实时音视频更看重低延迟,可以考虑 UDP;
  • 以文件存取为核心的应用,可能主要使用 TCP。

FTP 是应用层的文件传输协议,其底层使用 TCP 提供可靠传输。

结论:

不要脱离业务直接问“TCP 和 UDP 哪个更好”,而要先判断该功能更重视可靠性、实时性,还是可控性。

2. 在 UDP 之上增加可靠机制#

UDP 本身不提供 TCP 那样的可靠传输,但应用层可以选择性实现部分可靠机制:

  1. 为每个应用数据包增加序号 SEQ
  2. 接收方收到数据后返回确认 ACK
  3. 发送方启动定时器;
  4. 在超时时间内没有收到确认,就重新发送;
  5. 使用发送队列保存尚未确认的数据;
  6. 引入滑动窗口,一次发送多个包,提高传输效率。

示意:

应用层发送队列:1 2 3 4 5 6 ...
当前窗口: [1 2 3]
└── 收到 ACK=4 后,窗口向后移动
下一窗口: [4 5 6]

这些机制的思想与 TCP 中的 SEQACK、滑动窗口和超时重传相似,但实现位置从传输层变成了应用层。

3. 为什么不一定直接使用 TCP#

应用层在 UDP 之上实现可靠性,会比原始 UDP 增加开销,但可以只选取业务真正需要的能力。例如:

  • 保留 UDP 的报文边界,不产生 TCP 字节流的拆包问题;
  • 自行决定确认、重传和窗口策略;
  • 不必照搬 TCP 的所有控制机制;
  • 可以针对实时业务在可靠性与延迟之间作更细的取舍。

相应地,应用程序也必须自行承担更多工作,例如:

  • 超时与重传;
  • 顺序和去重;
  • 发送窗口;
  • 心跳与连接状态管理;
  • 异常数据处理。

4. QUIC#

QUIC 是一种基于 UDP 的新型网络传输协议,由 Google 发起,并由互联网工程任务组(IETF)标准化为 RFC 9000

它的核心目标是比 TCP 更快、更安全,目前已成为 HTTP/3 的底层传输协议。

简单来说,它改善了传统网络在弱网环境下速度慢、切换网络易断连等问题,已被多种互联网服务采用。

说明:可以以 UDP 为基础,在其上层增加可靠传输等能力,形成更符合特定需求的新协议。

八、UDP 与 TCP 对比#

性能UDPTCP
是否连接无连接面向连接
是否可靠不保证可靠传输,不使用 TCP 式流量控制和拥塞控制可靠传输,具有流量控制和拥塞控制
通信对象支持一对一、一对多、多对一和多对多单条连接是点对点、一对一通信
传输方式面向报文,保留报文边界面向字节流,不保留应用消息边界
首部开销首部固定 8 字节首部最小 20 字节,最大 60 字节
适用场景更重视实时性、可以容忍少量丢包的应用,如 IP 电话、视频会议、直播等更重视可靠性的应用,如文件传输

补充理解:

  • “UDP 不可靠”表示协议本身不保证送达、顺序和去重,并不代表 UDP 完全不能用于可靠业务;应用层可以自行增加确认、重传等机制。
  • “TCP 一对一”是针对一条 TCP 连接而言。服务器可以监听一个端口并同时维护多条客户端连接。
  • UDP 每次发送的是一个报文,接收方能够识别报文边界;TCP 只提供连续字节流,因此需要应用层协议解决分包问题。
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Linux 网络编程(7):TCP 协议相关知识 2
https://joyanblog.com/posts/linux-network-programming-07-tcp-protocol-2/
作者
Joyan
发布于
2026-07-24
许可协议
CC BY-NC-SA 4.0

目录