【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网络上构建了可靠的传输服务。理解这些机制不仅能帮你应对面试和笔试中的高频考点更能让你在实际网络编程中做到“知其然知其所以然”。如果你觉得本文对你有帮助欢迎点赞、收藏⭐、关注后续将继续解锁更多网络原理与编程实战知识。我们下期再见