应用层协议TCP

之前我们讲过,两主机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 关闭。

作用

  1. 保证最后一次 ACK 可靠到达对方如果最后这个 ACK 丢了,对方会超时重发 FIN;主动方在 TIME_WAIT 期间还能收到重传的 FIN,并再补发一次 ACK,确保对方正常关闭。

  2. 等待网络中旧连接残留报文自然消失 等够 2MSL,网络里旧连接迟到、延迟的报文全部过期消亡,防止旧报文混入新连接造成数据错乱。

可能产生的问题

  1. 占用端口、资源不释放TIME_WAIT 会占着端口和内核资源不立即释放,短时间内没法立刻复用该端口。
  2. 高并发服务下端口耗尽频繁短连接会产生大量 TIME_WAIT 状态套接字,端口被占满,新连接无法建立,导致服务吞吐量下降、建连失败。
  3. 延时关闭造成资源浪费大量连接长时间卡在 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)

拥塞控制的核心阶段:

  1. 慢启动
    • 刚建立连接时,cwnd从 1 个 MSS(最大报文段长度)开始。
    • 每收到一个 ACK,cwnd += MSS,也就是指数增长,快速探测网络带宽上限。
    • cwnd增长到 慢启动阈值 时,进入拥塞避免阶段。
  2. 拥塞避免
    • cwnd不再指数增长,而是线性增长 :每经过一个 RTT(往返时间),cwnd += MSS
    • 目的是平缓逼近网络容量,避免突然冲击导致拥塞。
  3. 快恢复
    • 收到 3 次重复 ACK 后(局部丢包),不直接把cwnd降到 1(避免回到慢启动),而是:
      • ssthresh(慢启动阈值) = cwnd / 2
      • cwnd = ssthresh
    • 然后直接进入拥塞避免阶段,避免大幅降低传输效率。

极端拥堵场景下后续报文全部无法抵达,接收端无法产生重复 ACK ,发送方会触发超时重传,同样把 ssthresh 减半,但会将 cwnd 直接降至 1 MSS,退回慢启动重新低速探测网络带宽。

补充

  • 拥塞控制解决的是网络整体负载过高、路由器队列溢出的问题。

五、延迟应答

延迟应答是接收方收到数据后,不立刻回复 ACK,而是等待一小段时间再回复,以此减少 ACK 报文的数量。

  • 默认机制 :TCP 标准规定,接收方可以延迟回复 ACK,但延迟时间不能超过 500ms,常见实现是 200ms 左右。
  • 核心目的
    1. 捎带确认:如果在等待期间,接收方也有数据要发给对方,就把 ACK "捎带" 在自己的数据报文中一起发送,这样 ACK 就不用单独发一个报文了。
    2. 合并 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 只保证:
    1. 字节按顺序到达;
    2. 没有丢失、没有重复;
    3. 不保证 "消息边界",也就是不区分业务层的一个 "消息" 从哪里开始、到哪里结束。
  • 面向字节流是 TCP 的根本特性,粘包问题就是这个特性的直接后果
  • 这个特性决定了:应用层必须自己定义消息边界,否则无法正确解析数据。

七、粘包问题

粘包问题 = 由于 TCP 是面向字节流的,多个业务消息在接收端的缓冲区里粘在一起,无法直接区分边界,导致解析出错。

产生原因

  1. 发送方合并(Nagle 算法)
    • Nagle 算法会把多个小的send()数据,合并成一个 TCP 报文段发送,减少网络中微小报文的数量,提高效率。
    • 结果就是:多个业务消息被合并成一个 TCP 报文段发送,接收方一次recv()就收到了多个消息。
  2. 接收方缓冲区合并
    • 接收方内核的接收缓冲区会缓存收到的数据,当应用程序调用recv()时,会一次性取出缓冲区中所有数据,不管里面包含了多少个业务消息。
    • 结果就是:多个 TCP 报文段的数据,被一次性读取,粘在一起。
  3. 应用读取不及时
    • 接收方应用读取速度慢,接收缓冲区里堆积了多个 TCP 报文段的数据,下次读取时一起取出。

问题后果

  • 接收方无法区分一个业务消息的起始和结束位置,导致解析时:
    • 收到的数据不完整,解析失败;
    • 多个消息粘在一起,解析出错误的业务数据;
    • 数据错乱,甚至引发业务逻辑错误。

根本解决办法(必须在应用层实现)

  1. 消息头 + 消息体(最常用)
    • 每个消息分为两部分:
      • 消息头:固定长度,包含消息体的长度;
      • 消息体:业务数据。
    • 接收方流程:先读取固定长度的消息头,解析出消息体长度,再按这个长度读取完整的消息体。
  2. 固定长度消息
    • 约定每个业务消息的长度固定,接收方每次按固定长度读取数据。
    • 缺点:不够灵活,数据不足时需要填充。
  3. 特殊分隔符
    • 在每个消息的末尾添加约定的分隔符(如\r\n\0),接收方读取数据后,按分隔符拆分消息。
    • 缺点:业务数据中不能出现分隔符,否则会被错误拆分。

TCP 三种异常连接情况

  1. 进程终止 进程退出时会自动释放 Socket 文件描述符,内核依旧会正常发出 FIN 报文 ,走四次挥手断开流程,和正常关闭连接没有区别

  2. 机器重启 机器重启前进程被强制退出,内核同样会处理资源释放、发起 FIN 挥手,表现和进程终止基本一致

  3. 机器掉电 / 网线断开 突然断电或断网时,对方来不及发任何 FIN ;此时另一端接收端内核仍认为连接正常存在,会一直维持连接状态。

  • 若接收端应用再次写入数据 ,内核检测到链路不通,直接发送 RST 复位报文,强制断开连接;
  • 若接收端一直没有读写操作,TCP 内置保活定时器会定期发探测报文询问对方;多次无响应后,判定对方下线,主动释放连接。
相关推荐
xhbh6661 小时前
Win系统实现网络转发与端口映射:从 IPEnableRouter 到 RRAS 完整步骤
网络·端口转发·流量端口转发·ssh端口转发·端口转发工具
原来是猿2 小时前
TCP Echo Server 深度解析:从单进程到线程池的演进之路(上)
服务器·网络·tcp/ip
小新同学^O^2 小时前
简单学习 --> TCP协议
java·网络·tcp
其实防守也摸鱼3 小时前
软件安全与漏洞--软件安全设计
运维·网络·安全·网络安全·密码学·需求分析·软件安全
一只数据集3 小时前
NVIDIA Nemotron AIQ Agentic Safety Dataset:面向企业级智能体系统的安全与防护评估数据集全面解析
网络·数据库·安全
你的保护色3 小时前
软件定义网络SDN
网络
艾莉丝努力练剑3 小时前
【Linux网络】Linux 网络编程入门:TCP Socket 编程(下)
linux·运维·服务器·网络·c++·tcp/ip
treesforest3 小时前
IP地址段查询完全指南:从单IP查到IPv4段批量归属地查询
网络·数据库·网络协议·tcp/ip·网络安全·运维开发
wangl_924 小时前
Modbus RTU 与 Modbus TCP 深入指南-字节顺序与跨平台问题
网络·网络协议·tcp/ip·tcp·modbus·rtu