【JavaSE - 网络部分07】TCP 收尾:面向字节流(粘包问题)与异常场景处理【传输层】
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🎯你正在阅读「网络原理续命手册」系列文章🎯
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨🔥弹简特 个人主页
❄️个人专栏直通车:
- 💻软件测试入门记
- 📱野生测试修炼手册 | APP 专项测试笔记
- 🔌接口测试从入门到跑路
- ☕一个后端的 JavaEE 续命指南
- 🛜网络原理续命手册
- 🐍Python 从零摸索日记
✨靠热爱去书写自己,靠勇敢去书写生活!
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🌟 博主简介:
文章目录:
- 前言
- 一、TCP其他核心机制,确保程序正常运转
- 1、TCP核心机制9,面向字节流
- 1.1 粘包问题引入
- 1.2 解决粘包问题
- 方法一:引入分隔符
- 方法二:引入数据长度
- 1.3 粘包问题总结
- 2、TCP核心机制10,异常情况
- 2.1 进程崩溃
- 2.2 主机关机
- 2.3 主机掉电
- 2.3.1 如果掉电的是接收方
- 2.3.2 如果是发送方断电的情况
- 2.4 网络断开
- 二、补充
- 1、TCP标志位补充说明
- 2、TCP选项部分
- 3、TCP与UDP对比总结
- 三、写在最后
前言
上期吃透TCP延迟应答与捎带应答两大优化机制✨,高效缩减冗余报文、节约网络带宽。本期迎来TCP内容收尾环节🔎,详解字节流特性催生的粘包问题,剖析丢包、断连等异常处置方案,补齐传输层关键要点。
一、TCP其他核心机制,确保程序正常运转
1、TCP核心机制9,面向字节流
我们面向字节流他本身理解起来比较简单,我们可以把它比作源源不断流动的水流。但在字节流传输的过程中,有一个比较难理解的问题,那就是粘包问题。
这时不少小伙伴都会产生疑问,粘包到底粘连的是什么数据包呢?
其实粘包粘连的就是应用层数据包。
1.1 粘包问题引入
因为我们TCP 协议是以字节流的形式传输数据,这种传输方式没办法划分独立数据包的界限,很容易让各个数据包之间的边界变得模糊,接收设备也就没办法分辨,一段数据从起始位置到结束位置,哪一部分才是单独完整的应用层数据包。
就像图中展示的这样,应用程序会调用 read 方法来读取字节流数据,读取操作没有固定的读取规范与范围限制。图里一共存在三个应用层数据包,这些数据都会统一存放到接收缓冲区当中,我们没办法直观区分界限,自然也就判断不出读取到哪个位置,才算获取到一个完整的应用层数据包。
我们期望的是得到一个完整的应用层数据包,如果不加任何限制的读取,此时得到的大概率就不是一个完整的数据,所以我们对粘包问题提供了一些解决方案👇
1.2 解决粘包问题
出现了粘包问题之后,那我们该用什么样的方式去解决这个问题呢?
想要解决粘包问题,归根结底就是要把每一个应用层数据包之间的边界划分清楚,只要能够精准区分开相邻的数据包,就能规避粘包带来的影响。日常开发中主要有两种常用的解决方式👇
方法一:引入分隔符
第一种解决办法就是自定义分隔符来划分数据包边界。
我们可以选用\n这类符号当作分隔标记,程序在读取字节流数据的时候,只要读取到预先设定好的分隔符,就代表当前这一份应用层数据包已经读取结束。
不过这种方式也存在对应的缺陷,如果我们选定\n作为分隔符,一旦实际传输的数据内容里面,本身就包含了\n这个字符,系统就会错误判定数据包结束位置。
正是因为存在这样的隐患,我们在使用分隔符方案时,挑选的分隔符号一定不能出现在业务数据当中。具体选择哪种符号作为分隔标识,要结合实际传输的数据内容来判断,核心原则就是分隔符和真实业务数据不会出现重合。
方法二:引入数据长度
第二种解决办法是在数据包头部新增长度字段,依靠这个长度数值,界定出单个数据包的数据起止范围。
按照这个规则读取数据时,会先读取固定个数的字节,从中解析出数据包的实际大小。举个例子,解析得到长度数值为 3,那就说明紧随其后的 3 个字节数据,共同构成一份完整的应用层数据包。
1.3 粘包问题总结
粘包问题,本质是我们设计应用层通信协议时,必须提前规划和处理的问题。
粘包并不是 TCP 协议单独存在的问题,只要数据是以字节流的形式完成传输,就都有可能出现粘包现象,日常的文件读取场景,同样也会碰到这类问题。
2、TCP核心机制10,异常情况
这里边我们谈的一些异常情况如下:
- 进程崩溃
- 主机关机
- 主机掉点
- 网线断开
如果这几种情况出现,我们tcp会怎么做呢?
2.1 进程崩溃
当程序进程崩溃(比如程序自己挂掉了或被强制终止)时,操作系统会帮它做清理工作:
自动发出结束信号(FIN)
就像程序正常关闭时主动说“我这边没数据要发了”一样,进程崩溃后,操作系统会自动替它向对方发出同样的结束信号,然后开始四次挥手流程。所以从网络上看,崩溃和正常关闭的效果是一样的。连接不会马上消失
发出结束信号后,TCP连接并不会立刻释放。它会进入一个“半关闭”的等待状态,给对方一点时间来处理。如果对方一直没有回应,系统会等待一段时间(比如几十秒到几分钟),超时后才彻底释放这个连接。这就是“等待一定时间之后才会释放”的原因。对方怎么知道这边崩溃了?
对方程序会感觉到连接出了问题:比如尝试读取数据时发现读不到新内容(就像对方挂断了电话),或者尝试发送数据时收到错误提示(类似“对方已不在线”)。这样对方就知道连接已经失效,可以做相应处理(比如重连或报错)。
它本质上和我们正常的状态连接过程是一样的,也就是我们的四次挥手是一样的。
我们调用close方法就会四次挥手,触发fin,
那么我们进程退出它也会触发fin。
我们进程崩溃它不代表我们tcp连接就释放了,tcp连接仍然存在,等待一定时间之后才会释放。
总结
1)进程崩溃时,操作系统内核会自动清理进程持有的 TCP 套接字,触发 FIN 报文,启动四次挥手,这和主动调用close()的核心 TCP 行为一致。
2)进程崩溃后 TCP 连接不会立即释放,是因为连接会进入半关闭状态(如 FIN_WAIT_2),内核会等待对端回应或超时后才彻底释放。
3)对端(客户端)会通过读写异常(如BrokenPipeError、ConnectionResetError)感知到进程崩溃,从而知道连接已失效。
2.2 主机关机
正常关机时,操作系统会先强制结束所有用户进程。这个操作和进程崩溃本质上是一样的——内核会自动清理进程占用的TCP套接字,从而触发四次挥手,发出FIN报文。
下图展示了这一过程:以咱们客户端关机为例
不过,关机情况下的四次挥手可能挥完,也可能挥不完。原因在于电脑关机是一个有时间限制的过程(系统会等待一段时间让进程退出,但不会无限等待)。
如果四次挥手非常快:在网络状况良好、对端响应及时的情况下,整个四次挥手可以在关机完成之前顺利结束。此时连接正常关闭,就像我们主动调用close()一样。
如果四次挥手没那么快:可能出现挥手还没结束,电脑就已经关机断电的情况。例如下图所示:
在这种情况下,对端(仍在运行的那台机器,比如服务器)会收不到ACK回应。因为客户端这边已经关机了,无法回复任何报文。
收不到ACK怎么办?对端(服务器)就会启动超时重传机制。它会多次尝试重传FIN,等待对方的回应。这个过程可以参考下图:
如果重传几次之后仍然得不到任何回应,对端(服务器)就知道对方已经不可达了。此时,对端服务器会单方面释放自己保存的连接信息。
这里需要理解一个关键概念:断开连接,本质上是通信双方各自删除对方的信息。正常四次挥手完成后,双方都会删除对方的连接记录。但在关机这种异常情况下,规则无法完整执行。既然对方已经关机,这一端就不再等待了——至少保证把自己保存的信息删掉。这样,虽然挥手没完成,但连接在逻辑上已经断开。
总结:主机关机时,会强制结束进程并触发FIN,开始四次挥手。如果挥手在关机前完成,则正常关闭;如果挥手未完成就断电,则关机方不再参与,而对端会因收不到回应而超时重传,最终单方面释放自己记录的连接信息。
2.3 主机掉电
主机掉电属于瞬间关机,操作系统来不及做任何动作(比如发送FIN、保存状态等)。那么TCP在这种情况下会怎么处理呢?我们分两种情况讨论。
2.3.1 如果掉电的是接收方
假设服务器(接收方)突然掉电了,之前客户端发送的数据,服务器本来都会回复ACK。但现在服务器瞬间断电,无法发出任何回应。
此时客户端发现:发出去的数据迟迟收不到ACK。于是客户端就会触发超时重传机制——它会多次重传相同的数据,等待服务器的回应。
下图示意了这一过程:
如果重传达到一定的次数之后仍然没有任何ACK回来,客户端就知道对方已经不可达了。这时客户端会单方面释放自己这边保存的连接信息,并且主动向对方发送一个复位报文(RST),表示这个连接异常终止。
总结接收方掉电:发送方收不到ACK → 超时重传 → 重传失败 → 单方面释放连接 + 发送RST。
2.3.2 如果是发送方断电的情况
假设客户端(发送方)突然断电了,它正在发送的数据戛然而止,也不会再发送任何后续报文。
那么接收方(服务器)会怎么做?它会等。
接收方会等待发送方继续发送数据,但它不是一直傻等。TCP协议中有一个机制叫做保活机制(Keep-Alive),也就是我们常说的心跳包。
这个心跳包的作用是探测对方是否还在正常工作。为什么叫“心跳”呢?因为心跳停了,就代表对方“挂了”。
具体过程是这样的:接收方在等待一定时间后(这个时间可以配置),会定期向发送方发送一个不携带任何数据载荷的探测包(其实就是一种特殊的TCP报文)。然后它等待对方回复ACK。
- 如果对方回复了ACK:说明对方的工作状态正常,连接可以继续保持。
- 如果对方一直没有回复ACK:说明对方已经出问题(比如掉电、崩溃、网络断开),此时接收方就可以单方面释放这个连接了。
下图示意了发送方断电的情况:
总结发送方掉电:接收方收不到数据 → 等待一段时间 → 定期发送心跳包 → 收不到ACK → 单方面释放连接。
2.4 网络断开
网络断开的情况,可以理解为发送方断电和接收方断电两种情况的结合——因为断开的是网络,所以双方都无法正常收到对方的数据。
我们分别从发送方和接收方的视角来看:
站在发送方的视角:
- 发送方像往常一样发送数据,但一直收不到对方的ACK确认。
- 于是发送方会触发超时重传机制,多次重传数据。
- 重传一定次数后仍然没有回应,发送方就会触发复位(RST),即单方面释放自己这边保存的连接信息。
站在接收方的视角:
- 接收方原本能持续收到发送方的数据,但突然间对方发来的数据没有了。
- 接收方不会一直傻等,而是会定期给对方发送心跳包(也就是TCP的保活探测报文),用来探测对方是否还在正常工作。
- 如果心跳包发出去后一直没有反应(收不到ACK),接收方也会触发复位(RST),单方面释放连接。
总结:网络断开时,发送方因收不到ACK而超时重传后复位,接收方因收不到数据而用心跳探测后复位。双方最终都会单方面释放连接,不再走正常的四次挥手流程。
二、补充
1、TCP标志位补充说明
在前面我们学过TCP的6个标志位,已经知道其中4个是:ACK、PSH、RST、SYN、FIN,这里补充剩下的一个URG,以及进一步解释PSH:
| 标志位 | 名称 | 作用说明 |
|---|---|---|
| URG | 紧急指针有效 | 用来表示当前数据段中有紧急数据,需要优先处理。可以通俗理解为**“插队”**,让接收方跳过普通数据先处理这段紧急内容。 |
| PSH | 催促标志位 | 告诉接收方:收到这个报文段里的数据之后,尽快把这个数据交给上层应用程序,不要等缓冲区满了再交。可以理解为**“催你赶紧去处理这个数据”**。 |
其余四个标志位的简单回顾:
- ACK:确认号有效,用于确认收到数据。
- RST:复位连接,用于异常终止。
- SYN:同步序号,用于建立连接。
- FIN:结束标志,用于正常关闭连接。
2、TCP选项部分
TCP头部除了固定的20字节之外,还有可选的选项字段,用于扩展TCP的功能。常见的选项包括:
- 最大报文段长度(MSS):告诉对方自己能接收的最大数据段大小。
- 窗口扩大因子:用于支持更大的窗口(超过64KB)。
- 时间戳:用于计算往返时间(RTT)和防止回绕的序号。
- SACK(选择性确认):允许只重传丢失的特定数据,而不是全部重传。
下图展示了TCP选项在报文中的位置(了解即可):
注:此图仅作示意,具体选项格式和内容需要时可以查阅TCP协议标准文档。
3、TCP与UDP对比总结
到这里,我们的TCP协议就告一段落了。最后用一句话对比TCP和UDP的核心区别:
| 协议 | 特点 | 可靠性 | 是否支持广播 |
|---|---|---|---|
| TCP | 面向连接、可靠传输、有拥塞控制和流量控制 | 可靠性要求高 | 不支持广播(是一对一的) |
| UDP | 无连接、尽力交付、没有拥塞控制 | 传输效率要求高 | 支持广播(可以一对多) |
简单记忆:
- TCP:可靠但慢,不丢数据,适合文件下载、网页访问等。
- UDP:快但可能丢包,适合视频直播、语音通话、广播场景。
三、写在最后
至此,TCP协议的十大核心机制——从确认应答、超时重传、连接管理、滑动窗口、流量控制、拥塞控制,到延迟应答、捎带应答、面向字节流(含粘包处理),以及本文详述的四大异常场景处理——已全部讲解完毕。
TCP通过精妙的设计,在不可靠的IP网络上构建了可靠的传输服务。理解这些机制,不仅能帮你应对面试和笔试中的高频考点,更能让你在实际网络编程中做到“知其然,知其所以然”。
如果你觉得本文对你有帮助,欢迎点赞👍、收藏⭐、关注👀,后续将继续解锁更多网络原理与编程实战知识。我们下期再见!
