TCP协议讲解

TCP 全称为 传输控制协议(Transmission Control Protocol)。人如其名,它需要对数据的传输进行全面且细致的控制。

TCP协议格式

  1. 源 / 目的端口号(各 16 位) 标识数据的来源进程目标进程,实现进程间的通信定位。

  2. 序号与确认号(各 32 位)

    • 32 位序号:标记数据段的字节位置,用于数据有序传输
    • 32 位确认号:确认已接收的数据,后续会详细说明
  3. 4 位 TCP 报头长度 表示 TCP 头部包含多少个32 位(4 字节)块 ,因此头部最大长度为:15 × 4 = 60字节(15 是 4 位的最大值)。

  4. 6 位标志位(功能标识)

    • URG:紧急指针是否生效
    • ACK:确认号是否有效(用于确认数据)
    • PSH:提示接收端应用立即读取TCP 缓冲区中的数据
    • RST:请求重建连接(携带该标识的报文称为 "复位报文段")
    • SYN:请求建立连接(携带该标识的报文称为 "同步报文段")
    • FIN:通知对方本端即将关闭(携带该标识的报文称为 "结束报文段")
  5. 16 位窗口大小用于流量控制,后续详细说明。

  6. 16 位校验和 由发送端计算(基于 TCP 首部 + TCP 数据),接收端通过 CRC 校验验证数据完整性;校验失败则判定数据损坏。

  7. 16 位紧急指针 标识报文中 "紧急数据" 的位置(需配合URG标志使用)。

  8. 40 字节头部选项可选扩展字段,暂不展开。


确认应答(ACK)机制

为什么需要 ACK 确认机制?

因为网络是不可靠的:数据可能在传输中丢失、重复、乱序。如果没有确认机制,发送方永远不知道接收方有没有收到数据,也就无法做到对消息的补发重传。接收方也没法保证消息有序地到达。

ACK 确认机制的核心逻辑

TCP 把要传输的每一个字节都分配唯一的 "序列号",发送方按顺序发数据,接收方收到数据后,会回复一个确认应答(ACK),其中包含确认序列号------ 这个确认序列号表示 "我已经收到了所有小于这个号的字节,你下次从这个号开始发"。

举个例子:

  • 发送方发了 1~1000 字节(序列号从 1 开始);
  • 接收方收到后,回复 ACK,确认序列号写 1001 → 意思是 "1~1000 我都收到了,你接下来发 1001 开始的字节";
  • 发送方再发 1001~2000 字节,接收方回复 ACK,确认序列号写 2001,以此类推。

报文数据的序号是哪来的

TCP有发送缓冲区,我们可以把它看错一个数组,Tcp又是面向字节流的,所以我们可以认为Tcp数据就被挨个从数组首地址开始填充到数组中,比如有一段数据是arr[101]~arr[500],我们发送这段序号就会发送长度为500字节+报头长度,序号为101.

异常出现

  • 主机 A 发现 "我发了 2001~3000,但对方回复的还是 ACK2001"→ 意识到 "这部分数据可能没传成功";
  • 如果主机 A 短时间内收到多个重复的 ACK2001 ,会触发快速重传(不等超时,直接重发 2001~3000);
  • 若没收到多个重复 ACK,主机 A 会等 "超时时间" 到后,触发超时重传,重新发送 2001~3000;
  • 直到主机 B 正确收到 2001~3000,回复 ACK3001,流程才会继续。

不过要注意我们日常通信很多时候是双方互相通信,即主机A会给B发消息,主机B也会给A发消息,所以为了提高效率我们一般不会单独发送一个ACK应答,而是在自己要发送的消息中携带上给对方的ACK应答。

ACK 确认机制的核心作用

  1. 保证数据 "可靠交付" 接收方通过 ACK 告诉发送方 "数据已收到";如果发送方等了一段时间没收到 ACK,就会重发这部分数据,直到收到确认。

  2. **实现 "流量控制"**ACK 里还会携带 "窗口大小"(表示接收方当前能接收的最大数据量),发送方会根据这个窗口调整发送速度,避免接收方缓冲区被撑满。

  3. **保证数据 "有序性"**因为每个字节都有序列号,接收方可以通过序列号把乱序到达的数据重新排好序;同时,ACK 的确认序列号也能让发送方明确 "哪些数据已经被正确接收",避免重复发送。


超时重传

发送方发送数据后,会启动一个超时计时器;如果在 "计时器到期前" 没收到接收方的 ACK 确认,就重新发送这部分数据。

场景 1:数据本身丢了

  • 主机 A 发了 "1~1000 字节",但数据在网络中丢了(比如网络拥堵);
  • 主机 B 没收到数据,不会回复 ACK;
  • 主机 A 的超时计时器到期后,重新发送 "1~1000 字节";
  • 直到主机 B 收到数据、回复 ACK1001,流程才继续。

场景 2:ACK 丢了

  • 主机 A 发了 "1~1000 字节",主机 B 收到了,但回复的 "ACK1001" 丢了;
  • 主机 A 没收到 ACK,超时后重发 "1~1000 字节";
  • 主机 B 收到重复的 1~1000 字节时,会通过 "序列号" 识别这是重复包,直接丢弃,然后再发一次 "ACK1001"。

超时时间的规则

  • 超时时间过长:会降低重传效率 ------ 若数据 / ACK 已丢失,发送方需等待较长时间才会重传,整体传输节奏会被明显拖慢。
  • 超时时间过短:易引发频繁重复发包 ------ 网络只是临时延迟(并非真丢包),但发送方提前触发超时重传,会导致接收方收到大量重复数据,造成网络资源浪费。

超时时间不能固定(网络环境多变),TCP 会动态计算 + 指数退避

  1. 基础单位:比如 Linux 中,超时时间以500ms为初始单位;
  2. 重传 1 次没收到 ACK:下一次超时时间翻倍(500ms → 1000ms);
  3. 再没收到:继续翻倍(1000ms → 2000ms → 4000ms...),以此类推;
  4. 重传次数达到阈值(比如多次重传后):TCP 认为 "网络 / 对方主机异常",强制关闭连接。

三次握手与四次挥手

三次握手的流程

握手:一方主动发送一个 TCP 控制报文,另一方成功接收该报文的单次单向交互过程

TCP 建立连接的目标是让双方都确认 "对方能收到我的消息、也能发给我消息"。

  1. 第一次握手 :客户端(从CLOSEDSYN_SENT)发SYN报文(同步请求),带自己的初始序号,意思是 "我想连你,这是我的起始序号",服务端(从LISTENSYN_RCVD)收到SYN
  2. 第二次握手 :服务端(SYN_RCVD)回复SYN+ACK报文:SYN是服务端的初始序号,ACK是确认客户端的序号,意思是 "我也想连接你,这是我的起始序号,我收到你的请求了"。客户端(从SYN_SENTESTABLISHED)收到SYN+ACK。
  3. 第三次握手 :客户端(ESTABLISHED)回复ACK报文(确认服务端的序号),意思是 "我收到你的序号了,现在可以互发数据了";服务端收到ACK后,也进入ESTABLISHED

三次握手异常场景:数据超前于第三次握手 ACK 到达

客户端其实会早一步进入 ESTABLISHED 状态,而我们默认进入此状态后即可发送消息。

假设场景:客户端先向服务端发送第三次握手的 ACK 应答,随后立即发送数据报文。但因网络原因,数据报文先到达服务端,而 ACK 应答尚未到达。

此时的关键矛盾与处理逻辑:

  • 服务端尚未收到 ACK 应答,仍处于SYN_RCVD 状态,未进入 ESTABLISHED,不具备接收数据的条件;
  • 在服务端看来,当前连接未完成三次握手,此时收到的数据报文属于非法报文;
  • 因此,服务端会向客户端发送RST 复位报文(表示 "当前连接非法,请重置")。

四次挥手的流程

TCP 是 "全双工"(双方可以同时收发数据),关闭连接的目标是让双方都安全关闭 "自己的发送通道",流程对应状态变化:

  1. 第一次挥手 :客户端(从ESTABLISHEDFIN_WAIT_1)主动关闭,发FIN报文,意思是 "我不再给你发数据了"。服务端(从ESTABLISHEDCLOSE_WAIT
  2. 第二次挥手 :服务端(从CLOSE_WAIT),回复ACK,意思是 "我知道你不发了,但我还有数据没处理完,等我发完"。客户端(从FIN_WAIT_1->FIN_WAIT_2
  3. 第三次挥手 :服务端处理完剩余数据后(从CLOSE_WAITLAST_ACK),发FIN报文,意思是 "我也不再给你发数据了"。客户端(从FIN_WAIT_2TIME_WAIT)收到FIN.
  4. 第四次挥手 :客户端回复ACK,然后等待2MSL(报文最大生存时间)后进入CLOSED;服务端收到ACK后直接进入CLOSED

三次握手可以合并ACK+SYN

三次握手是 **"建立连接" 的流程 **,此时服务端处于LISTEN状态(仅等待连接请求,没有要发给客户端的数据):当服务端收到客户端的SYN(第一次握手)时,它的需求是两个:

  1. 确认 "收到了客户端的连接请求"(发ACK);
  2. 同步自己的初始序号,同意建立连接(发SYN)。

因为此时服务端没有任何待发送的数据,这两个需求理论上可以通过一个报文(SYN+ACK) 完成,而实际上服务端作为响应客户端的一方也确实应该尽快和客户端建立连接,以供后续尽快响应服务。

四次挥手必须拆分ACK+FIN

四次挥手是 **"关闭连接" 的流程 **,此时服务端处于ESTABLISHED状态(正在和客户端传输数据):当服务端收到客户端的FIN(第一次挥手)时,它的需求也是两个:

  1. 确认 "收到了客户端的关闭请求"(发ACK);
  2. 关闭自己的发送通道(发FIN)。

但此时服务端可能还有未处理 / 未发送完的数据(TCP 是全双工,客户端关闭了 "自己的发送通道",但服务端的发送通道还在传输数据)------ 所以服务端必须先回复ACK(告诉客户端 "我知道你不发了"),然后等自己的数据处理完、发送完,才能发FIN(关闭自己的发送通道)。

这种情况下,ACKFIN无法合并:ACK要立即发(确认请求),FIN要等数据发完再发(不能中断数据传输),所以必须分开发送。


MSL

TCP 协议规定,主动关闭连接的一方需要进入 TIME_WAIT 状态,等待 两个 MSL(Maximum Segment Lifetime,报文最大生存时间) 的时间后,才能回到 CLOSED 状态。假定主动关闭的是客户端,具体原因有两个:

确保服务端能收到最后的 ACK

第四次挥手的客户端 ACK 报文可能丢失------ 如果客户端发完 ACK 就直接进入 CLOSED,服务端没收到 ACK 的话,会因为超时重传 FIN 报文。

此时客户端如果已经关闭,就无法回应重传的 FIN,服务端会一直重传,直到超时才关闭,导致服务端连接资源泄露。

等待 2MSL 的逻辑:

  • MSL 是报文在网络中存活的最长时间,服务端重传 FIN 的最大超时时间不会超过 MSL;
  • 客户端等待 2MSL,足以覆盖 "ACK 丢失 → 服务端重传 FIN → 客户端再次发送 ACK" 的整个往返过程;
  • 若 2MSL 内收到服务端重传的 FIN,客户端会重新发送 ACK,并重置 2MSL 计时器;
  • 若 2MSL 内没有收到重传的 FIN,说明服务端已经成功收到 ACK 并关闭,客户端可以安全进入 CLOSED。

让网络中残留的该连接报文自然失效

TCP 连接的四元组 (源 IP、源端口、目的 IP、目的端口)在连接关闭后,可能被后续新建的连接复用。如果客户端不等待 2MSL 就关闭,网络中可能还残留着本次连接的旧报文(比如未送达的 FIN、数据报文),这些旧报文可能会延迟到达对端,被新连接误判为自己的报文,导致通信混乱。


滑动窗口

核心目的:解决 "一发一收" 的性能瓶颈

之前 我们了解的是"发一段数据→等 ACK→再发下一段" 的方式(对应下图),会因为 "等待 ACK 的时间" 浪费很多时间(尤其是网络延迟高的时候)。

滑动窗口的作用是:让发送方可以 "批量发送多个数据段,不用等每个段的 ACK",把多个段的等待时间重叠,大幅提高传输效率(对应下图)。

滑动窗口的核心概念

  • 窗口大小:无需等待 ACK,发送方可以连续发送的最大数据量。窗口大小由接收方决定
    接收端通过 TCP 首部 中的 16 位窗口字段,将自身的窗口大小告知发送端。

    由此引出问题:16 位数字的最大表示值为 65535,那么 TCP 窗口的最大大小是否只能是 65535 字节?实际上,TCP 首部的 40 字节可选字段 中,还包含一个 窗口扩大因子 M。此时,实际窗口大小为:实际窗口大小 = 窗口字段的数值 左移M位。

  • 发送缓冲区:操作系统内核维护的一块内存,记录 "已发送但未收到 ACK 的数据";只有收到 ACK 的段,才会从缓冲区中删除。

滑动窗口的工作流程

  1. 初始发送:发送方按窗口大小,一次性发送多个段(比如窗口 4 段,发 1~1000、1001~2000、2001~3000、3001~4000,不用等 ACK)。
  2. 窗口滑动:当收到某一段的 ACK(比如收到 "下一个是 1001" 的 ACK,说明 1~1000 已确认),窗口向后滑动一段,继续发送下一个段(比如发 4001~5000)。
  3. 循环传输:重复 "发窗口内的段→收 ACK→窗口滑动→发新段",实现 "流水线式" 的连续传输。

滑动窗口下的丢包处理

情况 1:数据已到,ACK 丢了

比如 1~1000 的 ACK 丢了,但后续收到 "下一个是 2001" 的 ACK(说明 1~2000 都已收到)------ 此时后续 ACK 会 "覆盖" 丢失的 ACK,发送方无需重传(因为知道前面的段已经被接收)。

情况 2:数据丢了

比如 1001~2000 的段丢了,接收方会一直回复 "下一个是 1001" 的 ACK(提示 "我没收到 1001 开始的段"):

  • 当发送方连续收到 3 次相同的 ACK(比如连续 3 次 "下一个是 1001"),会触发快速重传(不等超时,直接重传丢失的 1001~2000 段);
  • 重传的段被接收后,接收方会回复 "下一个是 7001" 的 ACK(因为 2001~7000 其实已经收到,存在接收缓冲区里),窗口继续滑动。


拥塞控制

滑动窗口是解决 "收发双方的效率",但拥塞控制是解决 "整个网络的拥堵"------ 如果一上来就发大量数据,可能让本就拥挤的网络更堵(类似高峰期突然加塞大量车)。

所以 TCP 搞了 "先探路、再提速、堵了就减速" 的策略,核心靠两个概念:

  • 拥塞窗口(cwdn):发送方自己算的 "当前能发的最大数据量"(探路用的 "试探窗口");
  • 慢启动阈值(ssthresh):拥塞窗口的 "增速切换点"(超过它就从 "快速增长" 变 "平稳增长")。

拥塞控制的 3 个阶段

1. 慢启动阶段:

  • 初始状态:cwdn=1(先只发 1 个数据段),ssthresh=初始窗口最大值(图里是 16);
  • 增长规则:每收到 1 个 ACK,cwdn 就 + 1(指数级增长);
  • 图中对应:从传输轮次 0 到 4,cwdn 从 1→2→4→8→16(指数翻倍,很快到了 ssthresh=16)。
  • 为啥叫 "慢启动"?只是初始值慢,但增长速度是指数级的(像热恋初期,好感涨得飞快)。

2. 拥塞避免阶段

  • 触发条件:cwdn ≥ ssthresh(图里 cwdn 到 16 后);
  • 增长规则:每轮传输(收到所有 ACK),cwdn 才 + 1(线性增长,不再指数翻倍);
  • 图中对应:从轮次 4 到 12,cwdn 从 16 慢慢涨到 24

3. 网络拥塞后

  • 触发条件:网络拥堵(图里 cwdn 到 24 时丢包,触发超时重传);
  • 调整规则:
    1. ssthresh变成原来的一半(图里 24→12);
    2. cwdn重置为1,重新进入慢启动阶段;
  • 图中对应:轮次 12 后,cwdn 回到 1,重新指数增长到 12(新的 ssthresh),然后又进入拥塞避免的线性增长。

那么最终发多少数据呢,就是min(拥塞窗口,滑动窗口)

延迟应答

若接收数据的主机立即返回 ACK 应答,此时返回的窗口大小可能较小。

  • 假设接收端缓冲区大小为 1M,一次收到 500K 数据;若立即应答,返回的窗口大小为 500K。
  • 实际场景中,接收端的应用程序可能处理速度很快,10ms 内就能将 500K 数据从缓冲区消费完毕。这种情况下,接收端的处理能力远未达到极限,即使窗口放大一些,也能正常处理。
  • 若接收端延迟一段时间再应答(例如等待 200ms),此时缓冲区已被清空,返回的窗口大小可恢复为 1M。

需明确:窗口越大,网络吞吐量越高,传输效率也越高。延迟应答的目标是在保证网络不拥塞的前提下,尽可能提升传输效率。

延迟应答的限制规则

并非所有数据包都可延迟应答,需遵循数量限制时间限制的双重约束,避免 ACK 延迟过久导致发送方超时重传:

  1. 数量限制 :每接收 N 个数据包,必须应答一次(不能无限延迟)。
  2. 时间限制 :若未达到数量限制,超过最大延迟时间后,必须立即应答。

具体的数量阈值与超时时间因操作系统而异,通用配置为:

  • 数量阈值 N 取 2
  • 最大延迟时间 200ms

TCP 面向字节流

创建一个 TCP 的 Socket 时,内核会同时为其创建发送缓冲区接收缓冲区,数据传输全程基于缓冲区运作:

  1. 发送数据
    • 调用 write 时,数据会先写入发送缓冲区;
    • 若发送字节数过长,会被拆分为多个 TCP 数据包发出;
    • 若发送字节数过短,会先在缓冲区等待,直到缓冲区长度达标、或触发超时 / 其他发送时机时再发出。
  2. 接收数据
    • 数据从网卡驱动程序到达内核的接收缓冲区;
    • 应用程序调用 read 从接收缓冲区读取数据。
  3. 全双工特性 :一个 TCP 连接同时拥有发送缓冲区和接收缓冲区,因此该连接既可以读数据、也可以写数据,这一特性称为全双工

核心特点:TCP 程序的读和写无需一一匹配(缓冲区解耦):

  • 写 100 字节:可调用 1 次 write 写 100 字节,也可调用 100 次 write 每次写 1 字节;
  • 读 100 字节:无需关注写的方式,可 1 次 read 100 字节,也可每次 read 1 字节、重复 100 次。

粘包问题

1. 粘包的成因、
  • 粘包中的 "包",指应用层的数据包
  • TCP 协议头无 "报文长度" 字段(UDP 有),仅有序号字段;
  • 传输层视角:TCP 按序号将报文排序后放入缓冲区,是 "一个一个报文" 的形式;
  • 应用层视角:看到的是一串连续的字节数据,无法区分 "哪个字节到哪个字节是一个完整的应用层数据包"------ 这就是粘包问题的本质。
2. 粘包问题的解决方法(核心:明确应用层数据包的边界)
解决方式 适用场景 实现逻辑
定长包读取 数据包长度固定 每次从缓冲区按固定大小读取(如 sizeof(Request)),依次拆分即可。
包头约定长度 数据包长度可变 在数据包头部约定一个 "包总长度" 字段,读取时先解析长度,再按长度截取完整包。
分隔符分隔 数据包长度可变 在包与包之间加入唯一分隔符(需避免与正文冲突),按分隔符拆分数据包。

总结

  1. TCP 面向字节流的核心是缓冲区机制:发送 / 接收数据均经过内核缓冲区,读和写无需一一匹配,且基于双缓冲区实现全双工;
  2. 粘包问题不是 TCP 协议的缺陷,而是应用层未定义数据包边界导致的 ------TCP 仅传递字节流,需开发者在应用层明确包的分割规则;
  3. 解决粘包的核心思路:给应用层数据包加 "边界标识"(定长、长度字段、分隔符三选一)。
相关推荐
航Hang*2 小时前
第二章:网络系统建设与运维(高级)—— IS-IS路由协议
运维·服务器·网络·笔记·智能路由器·ensp
翼龙云_cloud2 小时前
腾讯云渠道商:如何在腾讯云服务器上搭建一个属于自己的网站或者论坛?
运维·服务器·云计算·腾讯云
qq_310658512 小时前
webrtc源码走读(四)核心引擎层——视频引擎
服务器·c++·音视频·webrtc
安科瑞刘鸿鹏172 小时前
企业配电系统中开关柜“可视化运行管理”的实现路径
大数据·运维·网络·物联网
liulilittle2 小时前
Ubuntu挂在新云盘(Disk磁盘)
运维·服务器·ubuntu
sao.hk2 小时前
ubuntu2404,vbox,全屏显示
linux·运维·服务器
危笑ioi2 小时前
linux配置nfs在ubuntu22.04
linux·运维·服务器
Bruce_Liuxiaowei2 小时前
(2025最后一篇博客)Metasploit框架攻击Windows实例:三种渗透路径
网络·windows·网络安全·网络攻击模型
cehuishi95272 小时前
python和arcgispro的实践(AI辅助编程)
服务器·前端·python