TCP相关问题的解决

为什么 TCP 握手一定是"三次"

很多教材会告诉你"为了防止已失效的连接请求报文段突然又传到了服务端",这句话太学术了。我们用一个容易理解的场景来模拟。

核心原因:

想象一下,你(客户端)给朋友(服务端)打电话,但信号极差。

  1. 第一次尝试(失效的请求): 你喊了一句"喂?听得到吗?"(发送 SYN A)。但这句声音卡在网络里了,并没有马上到达,你以为丢了。
  2. 第二次尝试(成功的请求): 你不耐烦了,挂断重拨,又喊了一句"喂?听得到吗?"(发送 SYN B)。这次通了,朋友回答"听到了"(SYN+ACK),你也回了一句"好,那我说了"(ACK)。通话结束,事情办完了,你们挂了电话。
  3. 误会发生了: 就在你们挂断后,那个迷路的第一次呼叫(SYN A)突然传到了朋友那里。

如果是"两次握手"会发生什么?

  • 朋友接到那个迟到的 SYN A,以为你要发起新通话。
  • 因为只需要两次握手,朋友回一句"好的"(SYN+ACK),连接就建立了
  • 朋友开始傻傻地等着你说话,一直占用着线路资源。
  • 而你(客户端)根本没想打电话,会直接忽略朋友的回复。
  • 后果: 服务端的资源被白白浪费,甚至可能导致死锁。

如果是"三次握手"怎么解决?

  • 朋友接到迟到的 SYN A,回了一句"好的,我准备好了"(SYN+ACK)。
  • 这一步只是半连接,还没有完全建立。
  • 你(客户端)收到这个莫名其妙的回复,心想:"我没给你打电话啊?"于是你会发送一个 RST(复位报文) 拒绝连接,或者干脆不理。
  • 朋友等不到你的第三次确认(ACK),就知道这事儿黄了,自动关闭连接。
总结

三次握手的主要目的是由客户端确认"这个连接是我现在真的想要的" ,从而防止网络延迟造成的旧报文引发错误的连接。同时,它也确保了双方的序列号(Sequence Number) 都得到了确认,这是可靠传输的基础。

TCP 四次挥手

核心原因在于:TCP 是全双工的(Full Duplex)。 意思是:客户端和服务端都有独立的发送能力。

  • 我不给你发数据了,不代表你也不给我发数据了。
  • 所以,断开连接必须是双方"各自"向对方说再见。
1. 形象类比:

想象一对情侣(客户端 Client 和 服务端 Server)要分手:

  1. 男方(Client): "我们要不分手吧,我没什么话对你说了。"(发送 FIN 包)。
  2. 女方(Server): "好的,我知道你想分手了。"(发送 ACK 包)。
    • 注意:此时女方可能还有话没说完,或者还有东西(数据)没还给男方。连接处于"半关闭"状态。男方只能听,不能说了。
  1. 女方(Server): (过了一会儿,东西收拾好了)"好了,我也没话说了,那就分吧。"(发送 FIN 包)。
  2. 男方(Client): "好的,祝你幸福。"(发送 ACK 包)。

彻底断开。

2. TIME_WAIT 状态

在第 4 步,客户端发完最后一个 ACK 后,并不是立刻关闭,而是进入一个叫 TIME_WAIT 的状态,通常要等待 2MSL(报文最大生存时间,一般是 2 分钟或者 4 分钟)这么久才真正释放资源。

为什么客户端发完最后一句还要傻等几分钟?

这里有两个极其重要的原因:

  1. 为了防丢包(给对方反悔的机会):
    • 万一你的最后一句"祝你幸福"(ACK)路上丢了怎么办?
    • 服务端没收到 ACK,会以为你没听到它的分手请求,它会重发一遍"那就分吧"(FIN)。
    • 如果你早就关机跑路了,服务端就会报错。所以你必须多等一会儿,确保没收到重发的 FIN,才说明对方真的收到确认了。
  1. 为了防"鬼影"(防止旧报文干扰):
    • 还记得 TCP 可能会有延迟到达的数据包吗?
    • 如果你立刻关闭连接,然后马上又用同样的端口建立了一个新连接。这时候,上一次连接中迷路的数据包突然到了,就会打乱新连接的数据。
    • TIME_WAIT 的作用就是:让那些迷路的数据包在网络中自然消亡(死透),保证新连接是干干净净的。

TCP 黏包与拆包

"黏包"这个词其实是中文开发圈的俗称,TCP 官方术语里其实没有"Sticky Packet"。这其实是 TCP 作为一个**"流式协议 (Stream Protocol)"** 的必然特性。

1. 核心原因:TCP 视数据如流水
  • UDP (用户数据报): 像是寄包裹。你寄 3 个包裹,对方就收到 3 个。边界很清晰,不会把两个包裹融化在一起。
  • TCP (传输控制流): 像是水管接水
    • 你(发送端)把一桶水倒进水管,又倒了一杯水进水管。
    • 对方(接收端)在另一头拿盆接。
    • 结果: 对方只接到了"一盆水"。由于水流是连绵不断的,对方根本不知道这盆水里,哪部分是你倒的那一桶,哪部分是那一杯。

这就是黏包:两个独立的数据包,在接收端粘在一起了。

2. 为什么会这样?

这通常是为了效率

  • 发送端 (Nagle 算法): TCP 觉得你每次发几个字节太浪费带宽了,它会把你的多次微小发送攒在一起,凑够大包一次性发出去(黏包)。
  • 接收端: 如果应用层处理得慢,TCP 缓冲区里积压了多个数据包,应用层一次性读取出来,也就黏在一起了。

当然,也有拆包(Packet Splitting):如果你发的数据太大,超过了 TCP 的限制(MSS),TCP 就会把它切成好几段发出去。

3. 怎么解决?(应用层的事)

既然 TCP 既然不管边界(它只管流),那应用层(HTTP、你的程序)必须自己想办法区分消息边界

常见的有三种办法:

  1. 固定长度 (Fixed Length):
    • 规定: 每个消息必须是 100 字节。不够就填 0 补齐。
    • 缺点: 浪费空间。
  1. 特殊分隔符 (Delimiter):
    • 规定: 遇到 \r\n (换行符) 就代表一条消息结束。
    • 例子: HTTP 协议、FTP 协议、JSON(靠大括号匹配)。
    • 缺点: 如果消息内容里本来就有换行符,就还要转义,比较麻烦。
  1. 头部加长度字段 (Length Field) ------ 最推荐!
    • 规定: 消息的前 4 个字节专门用来存"这条消息有多长"。
    • 流程:
      1. 先读 4 个字节,解析出长度值 N
      2. 再往后读 N 个字节。这就是完整的消息体。
    • 例子: 现代的 RPC 框架(Dubbo, gRPC 等)基本都用这种方式。

TCP 的"滑动窗口"机制是如何运作的

如果 TCP 像寄信一样,发一封信,等收到回信再发下一封(这叫"停止-等待"协议),那效率就太低了。网络带宽会被大量闲置。

滑动窗口(Sliding Window) 解决了这个问题:它允许你在没收到确认之前,连续发送多个数据包。

形象类比:传送带发货

想象你在工厂发货(发送端),有一个传送带(发送窗口)。

  1. 窗口大小(Window Size): 假设窗口大小是 3。意味着你可以把 1号、2号、3号 包裹接连放到传送带上发出去,而不需要等对方确认。
  2. 等待确认: 此时 1、2、3 号都在路上。
  3. 滑动:
    • 一旦你收到了对方对 1号 的确认(ACK 1),说明 1号 稳了。
    • 于是你的窗口向右滑动一格。
    • 现在的窗口变成了 2号、3号、4号 。你就可以立即把 4号 发出去了。
滑动窗口的两大作用

1. 流量控制(Flow Control):防止把对方撑死

  • 这个"窗口大小"不是你说了算的,是接收端告诉你的。
  • 如果接收端处理不过来了(比如内存满了),它会在回传的 ACK 包里告诉你:"我现在窗口为 0 了(Zero Window)"。
  • 发送端一看,哦,对方吃不消了,就会停止发送,直到对方窗口恢复。这就是 TCP 只有在对方能接收时才发送的智慧。

2. 提高效率:充分利用带宽

  • 不需要"发一个等一个",而是"批量发送,流水线作业"。只要在窗口范围内,想发多快发多快,大大提升了吞吐量。

丢包问题和快速重传

场景设定

假设你(发送端)一口气发送了 5 个数据包:包1、包2、包3、包4、包5 。 不幸的是,包3 在路上丢了,但是 包1、2、4、5 都顺利到达了接收端。

1. 笨办法:回退 N 步 (Go-Back-N)

在没有 SACK 之前,TCP 的确认机制是**"累计确认"**。这意味着接收端只能告诉发送端:"我收到了 X 之前的所有数据"。

  • 接收端收到 包1、2: 回复 ACK 3(意思是:我收到了 1 和 2,请发 3 给我)。
  • 接收端收到 包4: 发现中间缺了 3。它不能 回复 ACK 5,只能再次回复 ACK 3(还是在喊:我要 3!)。
  • 接收端收到 包5: 还是缺 3。它只能第三次回复 ACK 3。

发送端的反应: 发送端收到了一堆"我要 3"的请求,它意识到 包3 丢了。但是,它不知道 包4 和 包5 有没有到达 (因为 ACK 里没法说这事)。 为了保险起见,发送端只能把 包3、包4、包5 全部重传一遍

  • 结果: 包4 和 包5 被重复传送,浪费了带宽。如果网络本来就拥堵,这简直是雪上加霜。

2. 聪明办法:SACK (选择性确认)

现在的 TCP 头部有一个 "选项 (Options)" 字段,SACK 就在这里发挥作用。它允许接收端告诉发送端:"虽然我还要 3,但我手里已经有 4 和 5 了,你别重传它们。"

流程是这样的:

  1. 发送端 发出:1, 2, 3(丢), 4, 5。
  2. 接收端收到 包1、2: 回复 ACK 3(我要 3)。
  3. 接收端收到 包4: 发现乱序了。它回复 ACK 3,但在TCP 头部的 SACK 选项 里备注:SACK: [4-4](意思是:虽然我主线任务卡在 3,但我背包里已经有 4 了)。
  4. 接收端收到 包5: 回复 ACK 3,备注更新为:SACK: [4-5](意思是:我已经有 4 和 5 了)。

发送端的反应: 发送端一看:

  • 用户还要 3(ACK 3)。
  • 用户已经有 4 和 5 了(SACK [4-5])。
  • 决策: 只重传 包3

大结局: 接收端收到重传的 包3 后,缺口补齐,瞬间回复 ACK 6(意思是:1到5我全齐了,请发 6 吧)。


3. 快速重传 (Fast Retransmit)

你可能会想:发送端怎么知道什么时候该重传呢?是要傻傻地等几秒钟超时吗?

不等!这里有一个**"三次冗余 ACK"**定则。

在上面的例子中,因为 3 丢了,后续收到的 4、5 都会触发接收端发送"我要 3"的 ACK。

  • 当发送端连续收到 3 个重复的 ACK (加上原来的 1 个,一共 4 个 ACK 都在喊"我要3")时,它就立刻明白:"不用等超时计时器了,包3 肯定丢了,马上重传!"

这就叫快速重传,它和 SACK 配合使用,让 TCP 的反应速度极快。

TCP 的流量控制和拥塞控制

  • 流量控制 (Flow Control): 是怕接收方(对方)受不了。
    • 关注点: 点对点(我和你)。
    • 原因: 接收方处理数据的速度赶不上发送方的发送速度(比如接收方内存满了)。
    • 手段: 滑动窗口(接收方直接告诉发送方:"我窗口剩多少,你别发太快")。
  • 拥塞控制 (Congestion Control): 是怕**网络(路)**受不了。
    • 关注点: 全局(我和整个网络)。
    • 原因: 中间的路由器、网线堵车了,带宽不够用了。
    • 手段: 慢启动、拥塞避免(发送方自己感知到丢包了,主动减速,不给网络添乱)。

2. 形象类比:开车去朋友家

想象你(发送方)要开车去朋友家(接收方)送货。

场景 A:流量控制(Flow Control)

你开得飞快,路上一辆车都没有(网络很好)。但是当你到了朋友家门口,发现朋友家车库已经堆满了,根本卸不下货。

  • 朋友喊道: "停!我家满了,你先别发了,等我理一理再来!"
  • 这就是流量控制 。这是你和朋友两个人之间的协调。
场景 B:拥塞控制 (Congestion Control)

朋友家车库很大,空荡荡的(接收能力很强)。但是,去朋友家的公路上堵成了红紫色

  • 你虽然车技好,朋友也能收,但你被堵在半路动弹不得。如果你还不管不顾地往里挤,只会让路堵得更死。
  • 于是你决定:先慢慢开,看路况好了再加速。
  • 这就是拥塞控制 。这是你和交通状况之间的妥协。

3. 拥塞控制的"四部曲" (核心机制)

流量控制比较简单(靠窗口大小调整),但拥塞控制因为网络状况看不见摸不着,所以 TCP 设计了一套非常精妙的试探机制

你需要记住这个经典的**"拥塞窗口 (cwnd)"**变化图:

第一阶段:慢启动 (Slow Start) ------ 指数级增长

  • 策略: 刚开始建立连接时,TCP 不知道网络深浅,不敢太放肆。
  • 动作: 先发 1 个包,收到确认后发 2 个,再发 4 个,再发 8 个......
  • 比喻: 就像你试探水温,从小心翼翼开始,发现没问题就大胆翻倍加速。

第二阶段:拥塞避免 (Congestion Avoidance) ------ 线性增长

  • 策略: 当速度达到一个阈值(ssthresh)时,再翻倍就太危险了,容易瞬间堵死。
  • 动作: 变成"每收到一个 ACK,窗口加 1"。慢慢地、稳稳地往上加。
  • 比喻: 此时车速已经很快了,不能猛踩油门,只能一点点轻踩。

第三阶段:拥塞发生 (Congestion Detected) ------ 踩刹车

  • 情况: 突然丢包了!(没收到 ACK)。TCP 认为网络堵了。
  • 动作:
    • 如果是因为超时(最严重): 甚至可能直接把速度降回 1(一夜回到解放前),重新慢启动。
    • 如果是因为收到 3 个重复 ACK(即使有丢包也能传): 速度减半(乘法减小),然后进入快速恢复。

第四阶段:快速恢复 (Fast Recovery)

  • 策略: 既然还能收到重复 ACK,说明网络没彻底瘫痪,不需要降到 1 那么惨。
  • 动作: 仅仅把窗口减半,然后从这个减半后的位置开始继续"线性增长"。

相关推荐
工程师华哥2 小时前
【网工技术实验】华为S5700交换机堆叠配置实验案例
服务器·网络·华为
泽君学长2 小时前
CentOS 7 安装 Docker 完整教程
linux·docker·centos
记得记得就1512 小时前
Docker核心功能全解析:网络、资源控制、数据卷
网络·docker·容器
wheeldown2 小时前
【Linux网络编程】网络基础之MAC地址与IP地址的区别
linux·运维·网络·macos
悠哉悠哉愿意2 小时前
【EDA学习笔记】电子技术基础知识:读懂与画好原理图
笔记·单片机·嵌入式硬件·学习·eda
YJlio2 小时前
文件工具学习笔记(12.7):Sysinternals 文件工具实战总览与排障闭环
笔记·学习·计算机外设
叱咤月海鱼鱼猫*2 小时前
MMORPG复习:框架设计
笔记
U-52184F692 小时前
CGAL 实战笔记:深入理解 2D 符合三角剖分与网格生成 (针对 CAM 开发)
笔记·算法
chenyuhao20242 小时前
Linux系统编程:多线程同步与单例模式
linux·服务器·c++·后端·单例模式