
之前我们讲过,两主机AB之间通过TCP协议进行通信时,两主机会以拷贝为手段,将信息经过给自的发送/接受缓冲区以报头+数据段的形式在网络中进行传输,下面继续探讨这一过程。
一、TCP协议段格式

| 序号 | 字段名 | 长度 | 说明 |
|---|---|---|---|
| 1 | 源端口号 | 16 位 | 发送方应用进程使用的端口号,用于接收端回复时指明目标进程。 |
| 2 | 目的端口号 | 16 位 | 接收方应用进程的端口号。 |
| 3 | 序号 | 32 位 | 标识本 TCP 段中第一个数据字节的序号。TCP 传输的每个字节都有序号,用于保证数据有序、可靠以及重传。 |
| 4 | 确认序号 | 32 位 | 仅当 ACK 标志位 (序号8)为 1 时有效。表示期望收到对方下一个字节的序号,同时也隐含确认序号之前的所有数据都已正确接收。 |
| 5 | 首部长度 | 4 位 | 指出 TCP 首部的总长度。首部长度决定了"选项"部分的起始位置。(完整长度在IP报文中) |
| 6 | 保留 | 6 位 | 保留供将来使用,目前必须设置为 0。 |
| 7 | URG | 1 位 | 紧急指针标志位。当 URG = 1 时,表示"紧急指针"字段(序号15)有效,需要优先处理紧急数据。 |
| 8 | ACK | 1 位 | 确认标志位。当 ACK = 1 时,确认序号字段有效。除建立连接时的 SYN 报文外,所有报文都应设置 ACK = 1。 |
| 9 | PSH | 1 位 | 推送标志位。当 PSH = 1 时,接收方应立即将数据交给应用层,而不等待接收缓冲区满。 |
| 10 | RST | 1 位 | 重置标志位。当 RST = 1 时,表示连接出现严重错误(如主机崩溃、端口不可达),必须立即释放连接。 |
| 11 | SYN | 1 位 | 同步标志位。用于建立连接 |
TCP标志位
1. SYN
-
什么时候激活:在建立连接时,发起方发送第一个连接请求报文时激活;响应方同意连接时也会在回复报文中激活(与ACK一起)。
-
激活后的使命 :同步双方的初始序号,标志着这是一个连接建立的报文。接收方知道自己要开始一个新的TCP连接,并基于SYN中的序号进行后续通信。
2. ACK
-
什么时候激活 :除了连接请求(纯SYN)报文外,几乎所有的TCP报文都必须激活ACK。
-
激活后的使命 :告知对方"我已成功收到并确认序号之前的所有数据"。它使确认序号字段有效,从而实现可靠传输------发送方可以根据ACK知道哪些数据可以丢弃,哪些需要重传。
3. FIN
-
什么时候激活 :当一方没有更多数据要发送 ,希望正常关闭连接时激活。通常由调用
close()或shutdown()的应用触发。 -
激活后的使命 :声明发送方已经完成数据发送,请求释放连接。接收方收到FIN后知道对方不会再发数据,但自己可能还有数据要发(半关闭状态)。最终双方都要发送FIN才能完全关闭连接。
4. RST
-
什么时候激活 :当连接出现无法恢复的错误时激活。例如:连接请求的目标端口没有服务、收到不属于任何现存连接的数据包、连接超时、应用程序强制终止连接等。
-
激活后的使命 :立即终止连接,丢弃所有未确认的数据。收到RST的一方必须立刻关闭连接,无需应答,也不能再发送数据。它用于异常快速恢复,而非正常挥手。
5. PSH
-
什么时候激活 :当发送端希望接收方立即将数据交给应用程序,而不是等缓冲区填满时激活。TCP协议栈或应用程序(通过设置选项)可以决定激活PSH,典型场景是交互式命令(如SSH按键)或小数据块。
-
激活后的使命 :推送数据。接收方收到PSH报文后,不会缓存该报文的数据,而是马上交付给上层应用,从而减少延迟。
6. URG
-
什么时候激活 :当有紧急数据需要优先处理时激活,同时紧急指针字段会指明紧急数据的位置。通常由应用程序明确请求发送带外数据(OOB)时触发。
-
激活后的使命 :通知接收方,数据流中有需要立即读取的紧急信息,即使接收缓冲区还有未读的普通数据。接收方可通过特殊方式(如信号或带外读操作)优先取得紧急数据。现代协议中很少使用。
TCP常见选项
-
MSS 最大报文段长度: 协商双方可接收的最大 TCP 数据载荷长度
-
Window Scale 窗口扩大: 扩展 16 位窗口字段限制,增大滑动窗口,提升吞吐,握手阶段动态协商。
-
SACK 选择性确认: 支持乱序报文局部确认,只重传丢失片段,减少冗余重传。
-
Timestamp 时间戳: 精确计算RTT 往返时间 ;防止序号绕回,规避旧报文干扰新连接。
三次握手建立连接
报头中存放着源/目的端口号,根据IP信息不同主机之间可以进行通讯,但是在此之前需要经过三次握手建立链接:

我们先看图中的三次握手过程是SYN -> SYN+ACK -> ACK;我们先明白在三次握手中SYN表示想跟对方建立连接,而ACK则表示收到对方的请求并返回的结果。
三次握手的整体流程就是:己方向对方发送SYN请求,对方接受到SYN并向对方发送在自己SYN请求(全双工)的同时ACK说明自己确定收到了对方SYN请求。收到SYN+ACK的己方则向对方发送自己的ACK确认收到信息。
握手次数之所以选择三次,是其能保证高效正确的确定双方的发送接收能力(永远无法立即检测到当下这条是否被对方接受,必须依靠对方的下一条回复反向验证)
三次握手是否成功
三次握手用一句话概括就是:"我想做你女朋友(SYN);好啊,我也想做你的男朋友(ACK+SYN);当然可以(ACK)"。TCP 三次握手核心是通过三次报文交互,双方互相确认自身发送能力、对方接收能力均正常,任一阶段报文多超数重复丢失、超时无响应或收到拒绝报文,都将导致握手失败,无法进入连通状态。这也是TCP全双工的体现。
四次挥手断开连接

同样是因为TCP协议的全双工特征,当通信进入尾声时,请求断开的一方会发送FIN请求释放连接;对方接收到后进行ACK应答;同时因为对方仍可能存在没发送完的数据,对方需要一段时间处理完剩下的数据;当对方的数据确认处理完毕后,对方才会发送他的FIN请求;己方接收后再发送ACK。这样才算四次挥手完成。
整体流程:
ACK确认应答
TCP将每个字节都进行了编号,如下:
TCP报头中的32位序号和32位确认序号:
- 序号:发送方给每个字节数据编的号,代表当前报文段携带数据的起始序号。
- 确认序号 :接收方用来做确认,代表期望收到的下一个字节的序号 ,本质就是告诉发送方:「我已经收到了序号之前的所有数据 」。

当然,在ACK应答过程中我们也得到报头中的选项以及其它相关数据,从而作为依据制定后续策略。
超时重传
在实际情况下,一方没有接收到另一方的报文可能是:数据丢失或者应答丢失。但是我们统一将这一情况看成丢包--- ---发送方发了数据,在约定时间内没收到 ACK,就默认数据丢了,自动重传这段数据。


对于对方ACK丢失导致的重复问题,一般的处理形式是将新报文保留,旧报文丢弃(也就是丢掉重复序列号数据),并重新回复自己的ACK。
超时的确定
TCP 超时时间不设固定值,以500ms为基准单位,根据网络动态自适应计算,重传失败后按指数倍数拉长超时间隔,重传次数达到上限则判定网络或对端异常并强制关闭连接。
TIME_WAIT状态
四次挥手时,主动关闭方 发给被动方最后一次 ACK 后,不会直接关闭,立刻进入 TIME_WAIT 状态 ,要固定等待 2MSL(2倍报文最大生存时间) 时间,才彻底转到 CLOSED 关闭。
作用
-
保证最后一次 ACK 可靠到达对方如果最后这个 ACK 丢了,对方会超时重发 FIN;主动方在 TIME_WAIT 期间还能收到重传的 FIN,并再补发一次 ACK,确保对方正常关闭。
-
等待网络中旧连接残留报文自然消失 等够 2MSL,网络里旧连接迟到、延迟的报文全部过期消亡,防止旧报文混入新连接造成数据错乱。
可能产生的问题
- 占用端口、资源不释放TIME_WAIT 会占着端口和内核资源不立即释放,短时间内没法立刻复用该端口。
- 高并发服务下端口耗尽频繁短连接会产生大量 TIME_WAIT 状态套接字,端口被占满,新连接无法建立,导致服务吞吐量下降、建连失败。
- 延时关闭造成资源浪费大量连接长时间卡在 TIME_WAIT,占用系统内存和内核句柄。
解决方法
在 bind () 绑定端口之前,设置 SO_REUSEADDR = 1,允许端口即使处于 TIME_WAIT 状态,也能立刻被重新绑定使用。
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
CLOSE_WAIT状态
触发条件
当被动关闭一方 的 TCP 协议栈,收到对端主动发来的 FIN 关闭报文 后:内核自动立即回复 ACK 应答报文 ,无需应用层参与 ,连接就直接进入 CLOSE_WAIT 状态;只要收到 FIN 就触发 ,不管对方是正常关闭、进程异常退出,只要下发 FIN 就进该状态;若收到的是 RST 重置报文 ,直接断开连接,不会进入 CLOSE_WAIT。
其实就是FIN后没有close关闭。
对端已经关闭写方向通道,不再发数据;本机内核已确认关闭请求,等待应用程序主动调用 close/shutdown 关闭 socket ,此时本机仍可向对端发送剩余数据,属于半关闭状态。
CLOSE_WAIT 本身是 TCP 正常状态;连接长期卡在该状态,是应用程序逻辑问题。
造成的问题
持续占用文件描述符、内核内存、套接字资源,引发资源泄露;大量堆积会耗尽进程句柄,导致无法新建、接收新连接,服务阻塞不可用。
解决方案
业务结束、检测到对端断连时及时关闭 socket。
二、滑动窗口
在进行ACK应答的时候,如果是一条一条的发送,那么效率会非常低。于是为了提高效率,我们选择同一时间发送多条数据的方式。这种被称之为滑动窗口,与算法中的思想很相似。
-
窗口大小定义: 滑动窗口大小,是无需等待确认应答、可以连续一次性发送的最大数据量。
-
缓冲区维护策略: 内核开辟发送缓冲区 ,记录已发送但未收到 ACK 的数据;只有收到确认应答的数据,才会从缓冲区清除。
-
窗口滑动策略: 当收到已发送数据的 ACK 确认应答 后,滑动窗口向后滑动(之前的数据被丢弃),顺着窗口范围继续发送后续新数据,循环推进。
-
滑动窗口窗口设置越大 ,可一次性连发的数据越多,网络吞吐率就越高。
滑动窗的丢包应对措施
情况1,数据包送到但是ACK丢失:
这种情况可以依照后续的确认序号来判断,如上图虽然丢失了中间ACK,但最后正常收到了6001,表明6001之前的序号数据已经正常接收,无需重发。
情况2,数据包丢失:
若数据报文丢失,接收方无法收到该报文,会持续返回对前面正常数据的重复 ACK ;发送方要么等待超时未收到确认 ,要么收到三次重复 ACK ,就会主动重传丢失的报文段,重传完成后继续正常后续传输流程。
在缺失的报文补发确定前,这个数据流是乱序的。TCP 接收端乱序到达、后面序号更大的报文 ,会先暂存到内核接收缓冲区 ,不会丢弃,也不会上交应用层。当缺失报文补全后,缓冲区里暂存的乱序数据按顺序一并交给应用。
三、流量控制
流量控制是防止发送方发送太快,导致接收方的接收缓冲区溢出。 它是一种接收方主导的速率控制机制。
- 接收缓冲区:接收方内核为每个 TCP 连接维护一块缓冲区,用来存放收到但还没被应用程序读取的数据。
- 接收窗口(rwnd) :接收方每次回复 ACK 时,会在 TCP 头部的
window字段里,带上自己当前的接收窗口大小(即缓冲区剩余空间)。 - 发送窗口上限 :发送方的发送窗口,不能超过接收方通告的 rwnd,否则接收方缓冲区装不下,就会丢包。
- 动态调整:当接收方应用程序读取数据、缓冲区腾出空间后,下一次 ACK 会通告更大的 rwnd;如果应用读取慢,缓冲区快满了,就会通告更小的 rwnd,甚至是 0(此时发送方必须停止发送,直到收到非 0 的 rwnd)。
- 当 rwnd=0 时,发送方不会立刻停止,而是会发送零窗口探测报文,定期询问接收方窗口是否恢复,避免永远死等。
- 流量控制解决的是收发双方处理能力不匹配的问题,和网络拥堵无关。
四、拥塞控制
拥塞控制是防止发送方发送太快,导致整个网络链路拥塞、路由器丢包。 它是发送方自我约束的机制,目的是保护网络,而不是保护接收方。网络拥堵时就用到这一策略。
发送方维护一个拥塞窗口(cwnd) ,它和接收窗口(rwnd)一起,共同决定实际发送窗口:实际发送窗口 = min(rwnd, cwnd)
拥塞控制的核心阶段:
- 慢启动
- 刚建立连接时,
cwnd从 1 个 MSS(最大报文段长度)开始。 - 每收到一个 ACK,
cwnd += MSS,也就是指数增长,快速探测网络带宽上限。 - 当
cwnd增长到 慢启动阈值 时,进入拥塞避免阶段。
- 刚建立连接时,
- 拥塞避免
cwnd不再指数增长,而是线性增长 :每经过一个 RTT(往返时间),cwnd += MSS。- 目的是平缓逼近网络容量,避免突然冲击导致拥塞。
- 快恢复
- 收到 3 次重复 ACK 后(局部丢包),不直接把
cwnd降到 1(避免回到慢启动),而是:ssthresh(慢启动阈值) = cwnd / 2cwnd = ssthresh
- 然后直接进入拥塞避免阶段,避免大幅降低传输效率。
- 收到 3 次重复 ACK 后(局部丢包),不直接把
而极端拥堵场景下后续报文全部无法抵达,接收端无法产生重复 ACK ,发送方会触发超时重传,同样把 ssthresh 减半,但会将 cwnd 直接降至 1 MSS,退回慢启动重新低速探测网络带宽。

补充
- 拥塞控制解决的是网络整体负载过高、路由器队列溢出的问题。
五、延迟应答
延迟应答是接收方收到数据后,不立刻回复 ACK,而是等待一小段时间再回复,以此减少 ACK 报文的数量。
- 默认机制 :TCP 标准规定,接收方可以延迟回复 ACK,但延迟时间不能超过 500ms,常见实现是 200ms 左右。
- 核心目的 :
- 捎带确认:如果在等待期间,接收方也有数据要发给对方,就把 ACK "捎带" 在自己的数据报文中一起发送,这样 ACK 就不用单独发一个报文了。
- 合并 ACK:可以对多个收到的报文,只回复一个 ACK(比如每收到 2 个报文回复一次 ACK),减少 ACK 的数量,降低网络开销。
- 触发限制 :
- 延迟应答不能超过规定时间,否则发送方会认为数据丢失,触发超时重传,反而降低效率。
- 当收到乱序报文、或 TCP 栈判断需要立即回复时,会跳过延迟,直接回复 ACK。
补充
- 延迟应答是接收方的优化,和发送方无关,发送方感知不到。
- 它在交互式场景(比如 Telnet、SSH)中效果明显,能减少大量微小的 ACK 报文。
六、面向字节流
面向字节流是TCP 传输的是一串无结构、无边界的字节序列,而不是有固定格式的 "数据包"。
- 发送方的
send()和接收方的recv()操作,和网络上的 TCP 报文段没有一一对应关系 :- 发送方一次
send(1000字节),内核可能拆分成多个 TCP 报文段发送; - 发送方多次
send(100字节),内核也可能合并成一个 TCP 报文段发送(Nagle 算法会加剧这个合并); - 接收方一次
recv(1000字节),可能收到多个send()的数据;也可能需要多次recv()才能读完一次send()的数据。
- 发送方一次
- TCP 只保证:
- 字节按顺序到达;
- 没有丢失、没有重复;
- 不保证 "消息边界",也就是不区分业务层的一个 "消息" 从哪里开始、到哪里结束。
- 面向字节流是 TCP 的根本特性,粘包问题就是这个特性的直接后果。
- 这个特性决定了:应用层必须自己定义消息边界,否则无法正确解析数据。
七、粘包问题
粘包问题 = 由于 TCP 是面向字节流的,多个业务消息在接收端的缓冲区里粘在一起,无法直接区分边界,导致解析出错。
产生原因
- 发送方合并(Nagle 算法)
- Nagle 算法会把多个小的
send()数据,合并成一个 TCP 报文段发送,减少网络中微小报文的数量,提高效率。 - 结果就是:多个业务消息被合并成一个 TCP 报文段发送,接收方一次
recv()就收到了多个消息。
- Nagle 算法会把多个小的
- 接收方缓冲区合并
- 接收方内核的接收缓冲区会缓存收到的数据,当应用程序调用
recv()时,会一次性取出缓冲区中所有数据,不管里面包含了多少个业务消息。 - 结果就是:多个 TCP 报文段的数据,被一次性读取,粘在一起。
- 接收方内核的接收缓冲区会缓存收到的数据,当应用程序调用
- 应用读取不及时
- 接收方应用读取速度慢,接收缓冲区里堆积了多个 TCP 报文段的数据,下次读取时一起取出。
问题后果
- 接收方无法区分一个业务消息的起始和结束位置,导致解析时:
- 收到的数据不完整,解析失败;
- 多个消息粘在一起,解析出错误的业务数据;
- 数据错乱,甚至引发业务逻辑错误。
根本解决办法(必须在应用层实现)
- 消息头 + 消息体(最常用)
- 每个消息分为两部分:
- 消息头:固定长度,包含消息体的长度;
- 消息体:业务数据。
- 接收方流程:先读取固定长度的消息头,解析出消息体长度,再按这个长度读取完整的消息体。
- 每个消息分为两部分:
- 固定长度消息
- 约定每个业务消息的长度固定,接收方每次按固定长度读取数据。
- 缺点:不够灵活,数据不足时需要填充。
- 特殊分隔符
- 在每个消息的末尾添加约定的分隔符(如
\r\n、\0),接收方读取数据后,按分隔符拆分消息。 - 缺点:业务数据中不能出现分隔符,否则会被错误拆分。
- 在每个消息的末尾添加约定的分隔符(如
TCP 三种异常连接情况
-
进程终止 进程退出时会自动释放 Socket 文件描述符,内核依旧会正常发出 FIN 报文 ,走四次挥手断开流程,和正常关闭连接没有区别。
-
机器重启 机器重启前进程被强制退出,内核同样会处理资源释放、发起 FIN 挥手,表现和进程终止基本一致。
-
机器掉电 / 网线断开 突然断电或断网时,对方来不及发任何 FIN ;此时另一端接收端内核仍认为连接正常存在,会一直维持连接状态。
- 若接收端应用再次写入数据 ,内核检测到链路不通,直接发送 RST 复位报文,强制断开连接;
- 若接收端一直没有读写操作,TCP 内置保活定时器会定期发探测报文询问对方;多次无响应后,判定对方下线,主动释放连接。