Socket粘包问题的3种解决方案,最后一种最完美!

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

在  Java 语言中,传统的 Socket 编程分为两种实现方式,这两种实现方式也对应着两种不同的传输层协议:TCP 协议和 UDP 协议,但作为互联网中最常用的传输层协议 TCP,在使用时却会导致粘包和半包问题,于是为了彻底的解决此问题,便诞生了此篇文章。

什么是 TCP 协议?

TCP 全称是 Transmission Control Protocol(传输控制协议),它由 IETF 的 RFC 793 定义,是一种面向连接的点对点的传输层通信协议。

TCP 通过使用序列号和确认消息,从发送节点提供有关传输到目标节点的数据包的传递的信息。TCP 确保数据的可靠性,端到端传递,重新排序和重传,直到达到超时条件或接收到数据包的确认为止。


TCP 是 Internet 上最常用的协议,它也是实现 HTTP(HTTP 1.0/HTTP 2.0)通讯的基础,当我们在浏览器中请求网页时,计算机会将 TCP 数据包发送到 Web 服务器的地址,要求它将网页返还给我们,Web 服务器通过发送 TCP 数据包流进行响应,然后浏览器将这些数据包缝合在一起以形成网页。

TCP 的全部意义在于它的可靠性,它通过对数据包编号来对其进行排序,而且它会通过让服务器将响应发送回浏览器说“已收到”来进行错误检查,因此在传输过程中不会丢失或破坏任何数据。

目前市场上主流的 HTTP 协议使用的版本是 HTTP/1.1,如下图所示:

什么是粘包和半包问题?

粘包问题是指当发送两条消息时,比如发送了 ABC 和 DEF,但另一端接收到的却是 ABCD,像这种一次性读取了两条数据的情况就叫做粘包(正常情况应该是一条一条读取的)。

半包问题是指,当发送的消息是 ABC 时,另一端却接收到的是 AB 和 C 两条信息,像这种情况就叫做半包。

为什么会有粘包和半包问题?

这是因为 TCP 是面向连接的传输协议,TCP 传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息

粘包的主要原因:

  • 发送方每次写入数据 < 套接字(Socket)缓冲区大小;

  • 接收方读取套接字(Socket)缓冲区数据不够及时。

半包的主要原因:

  • 发送方每次写入数据 > 套接字(Socket)缓冲区大小;

  • 发送的数据大于协议的 MTU (Maximum Transmission Unit,最大传输单元),因此必须拆包。

小知识点:什么是缓冲区?

缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。

缓冲区的优势以文件流的写入为例,如果我们不使用缓冲区,那么每次写操作 CPU 都会和低速存储设备也就是磁盘进行交互,那么整个写入文件的速度就会受制于低速的存储设备(磁盘)。但如果使用缓冲区的话,每次写操作会先将数据保存在高速缓冲区内存上,当缓冲区的数据到达某个阈值之后,再将文件一次性写入到磁盘上。因为内存的写入速度远远大于磁盘的写入速度,所以当有了缓冲区之后,文件的写入速度就被大大提升了。

粘包和半包问题演示

接下来我们用代码来演示一下粘包和半包问题,为了演示的直观性,我会设置两个角色:

  • 服务器端用来接收消息;

  • 客户端用来发送一段固定的消息。

然后通过打印服务器端接收到的信息来观察粘包和半包问题。

服务器端代码如下:

/*** 服务器端(只负责接收消息)*/
class ServSocket {// 字节数组的长度private static final int BYTE_LENGTH = 20;  public static void main(String[] args) throws IOException {// 创建 Socket 服务器ServerSocket serverSocket = new ServerSocket(9999);// 获取客户端连接Socket clientSocket = serverSocket.accept();// 得到客户端发送的流对象try (InputStream inputStream = clientSocket.getInputStream()) {while (true) {// 循环获取客户端发送的信息byte[] bytes = new byte[BYTE_LENGTH];// 读取客户端发送的信息int count = inputStream.read(bytes, 0, BYTE_LENGTH);if (count > 0) {// 成功接收到有效消息并打印System.out.println("接收到客户端的信息是:" + new String(bytes));}count = 0;}}}
}

客户端代码如下:

/*** 客户端(只负责发送消息)*/
static class ClientSocket {public static void main(String[] args) throws IOException {// 创建 Socket 客户端并尝试连接服务器端Socket socket = new Socket("127.0.0.1", 9999);// 发送的消息内容final String message = "Hi,Java."; // 使用输出流发送消息try (OutputStream outputStream = socket.getOutputStream()) {// 给服务器端发送 10 次消息for (int i = 0; i < 10; i++) {// 发送消息outputStream.write(message.getBytes());}}}
}

以上程序的通讯结果如下图所示:

通过上述结果我们可以看出,服务器端发生了粘包和半包的问题,因为客户端发送了 10 次固定的“Hi,Java.”的消息,正常的结果应该是服务器端也接收到了 10 次固定的消息才对,但现实的结果并非如此。

粘包和半包的解决方案

粘包和半包的解决方案有以下 3 种:

  1. 发送方和接收方规定固定大小的缓冲区,也就是发送和接收都使用固定大小的 byte[] 数组长度,当字符长度不够时使用空字符弥补;

  2. 在 TCP 协议的基础上封装一层数据请求协议,既将数据包封装成数据头(存储数据正文大小)+ 数据正文的形式,这样在服务端就可以知道每个数据包的具体长度了,知道了发送数据的具体边界之后,就可以解决半包和粘包的问题了;

  3. 以特殊的字符结尾,比如以“\n”结尾,这样我们就知道结束字符,从而避免了半包和粘包问题(推荐解决方案)。

那么接下来我们就来演示一下,以上解决方案的具体代码实现。

解决方案1:固定缓冲区大小

固定缓冲区大小的实现方案,只需要控制服务器端和客户端发送和接收字节的(数组)长度相同即可。

服务器端实现代码如下:

/*** 服务器端,改进版本一(只负责接收消息)*/
static class ServSocketV1 {private static final int BYTE_LENGTH = 1024;  // 字节数组长度(收消息用)public static void main(String[] args) throws IOException {ServerSocket serverSocket = new ServerSocket(9091);// 获取到连接Socket clientSocket = serverSocket.accept();try (InputStream inputStream = clientSocket.getInputStream()) {while (true) {byte[] bytes = new byte[BYTE_LENGTH];// 读取客户端发送的信息int count = inputStream.read(bytes, 0, BYTE_LENGTH);if (count > 0) {// 接收到消息打印System.out.println("接收到客户端的信息是:" + new String(bytes).trim());}count = 0;}}}
}

客户端实现代码如下:

/*** 客户端,改进版一(只负责接收消息)*/
static class ClientSocketV1 {private static final int BYTE_LENGTH = 1024;  // 字节长度public static void main(String[] args) throws IOException {Socket socket = new Socket("127.0.0.1", 9091);final String message = "Hi,Java."; // 发送消息try (OutputStream outputStream = socket.getOutputStream()) {// 将数据组装成定长字节数组byte[] bytes = new byte[BYTE_LENGTH];int idx = 0;for (byte b : message.getBytes()) {bytes[idx] = b;idx++;}// 给服务器端发送 10 次消息for (int i = 0; i < 10; i++) {outputStream.write(bytes, 0, BYTE_LENGTH);}}}
}

以上代码的执行结果如下图所示:


优缺点分析

从以上代码可以看出,虽然这种方式可以解决粘包和半包的问题,但这种固定缓冲区大小的方式增加了不必要的数据传输,因为这种方式当发送的数据比较小时会使用空字符来弥补,所以这种方式就大大的增加了网络传输的负担,所以它也不是最佳的解决方案。

解决方案二:封装请求协议

这种解决方案的实现思路是将请求的数据封装为两部分:数据头+数据正文,在数据头中存储数据正文的大小,当读取的数据小于数据头中的大小时,继续读取数据,直到读取的数据长度等于数据头中的长度时才停止。

因为这种方式可以拿到数据的边界,所以也不会导致粘包和半包的问题,但这种实现方式的编码成本较大也不够优雅,因此不是最佳的实现方案,因此我们这里就略过,直接来看最终的解决方案吧。

解决方案三:特殊字符结尾,按行读取

以特殊字符结尾就可以知道流的边界了,因此也可以用来解决粘包和半包的问题,此实现方案是我们推荐最终解决方案

这种解决方案的核心是,使用 Java 中自带的 BufferedReader 和 BufferedWriter,也就是带缓冲区的输入字符流和输出字符流,通过写入的时候加上 \n 来结尾,读取的时候使用 readLine 按行来读取数据,这样就知道流的边界了,从而解决了粘包和半包的问题。

服务器端实现代码如下:

/*** 服务器端,改进版三(只负责收消息)*/
static class ServSocketV3 {public static void main(String[] args) throws IOException {// 创建 Socket 服务器端ServerSocket serverSocket = new ServerSocket(9092);// 获取客户端连接Socket clientSocket = serverSocket.accept();// 使用线程池处理更多的客户端ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100,TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));threadPool.submit(() -> {// 消息处理processMessage(clientSocket);});}/*** 消息处理* @param clientSocket*/private static void processMessage(Socket clientSocket) {// 获取客户端发送的消息流对象try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {while (true) {// 按行读取客户端发送的消息String msg = bufferedReader.readLine();if (msg != null) {// 成功接收到客户端的消息并打印System.out.println("接收到客户端的信息:" + msg);}}} catch (IOException ioException) {ioException.printStackTrace();}}
}

PS:上述代码使用了线程池来解决多个客户端同时访问服务器端的问题,从而实现了一对多的服务器响应。

客户端的实现代码如下:

/*** 客户端,改进版三(只负责发送消息)*/
static class ClientSocketV3 {public static void main(String[] args) throws IOException {// 启动 Socket 并尝试连接服务器Socket socket = new Socket("127.0.0.1", 9092);final String message = "Hi,Java."; // 发送消息try (BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {// 给服务器端发送 10 次消息for (int i = 0; i < 10; i++) {// 注意:结尾的 \n 不能省略,它表示按行写入bufferedWriter.write(message + "\n");// 刷新缓冲区(此步骤不能省略)bufferedWriter.flush();}}}
}

以上代码的执行结果如下图所示:


总结

本文我们讲了 TCP 粘包和半包问题,粘包是指读取到了两条信息,正常情况下消息应该是一条一条读取的,而半包问题是指读取了一半信息。导致粘包和半包的原因是 TCP 的传输是以流的形式进行的,而流数据是没有明确的开始和结尾标识的,因此就导致了此问题。

本文我们提供了 3 种粘包和半包的解决方案,其中最推荐的是使用 BufferedReaderBufferedWriter 按行来读、写和区分消息,也就是本文的第三种解决方案。

参考 & 鸣谢

https://zhuanlan.zhihu.com/p/126279630

https://www.jianshu.com/p/6a4ec6095f2c


往期推荐

文件写入的6种方法,这种方法性能最好


线程池的7种创建方式,强烈推荐你用它...


MySQL为Null会导致5个问题,个个致命!


关注我,每天陪你进步一点点!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/545065.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【万里征程——Windows App开发】控件大集合1

添加控件的方式有多种&#xff0c;大家更喜欢哪一种呢&#xff1f; 1&#xff09;使用诸如 Blend for Visual Studio 或 Microsoft Visual Studio XAML 设计器的设计工具。 2&#xff09;在 Visual Studio XAML 编辑器中将控件添加到 XAML 标记中。 3&#xff09;在代码中添…

Java Collections BinarySearch()方法与示例

集合类binarySearch()方法 (Collections Class binarySearch() method) Syntax: 句法&#xff1a; public static int binarySearch(List l, Type key_ele);public static int binarySearch(List l, Type key_ele, Comparator com);binarySearch() method is available in jav…

从String中移除空白字符的多种方式!?差别竟然这么大!

字符串&#xff0c;是Java中最常用的一个数据类型了。我们在日常开发时候会经常使用字符串做很多的操作。比如字符串的拼接、截断、替换等。这一篇文章&#xff0c;我们介绍一个比较常见又容易被忽略的一个操作&#xff0c;那就是移除字符串中的空格。其实&#xff0c;在Java中…

[JS][jQuery]清空元素html()、innerHTML= 与 empty()的区别 、remove()区别

清空元素html("")、innerHTML"" 与 empty()的区别 一、清空元素的区别 1、错误做法一&#xff1a; $("#test").html("");//该做法会导致内存泄露 2、错误做法二&#xff1a;$("#test")[0].innerHTML"&qu…

Properties文件位置

这种情况下Properties文件放在和‘当前类’同一目录下 <span style"font-size:14px;">Properties p new Properties(); InputStream in 当前类.class.getResourceAsStream("Properties文件名"); p.load(in); </span> 这种情况下Prop…

Java类class isAnnotation()方法及示例

类的类isAnnotation()方法 (Class class isAnnotation() method) isAnnotation() method is available in java.lang package. isAnnotation()方法在java.lang包中可用。 isAnnotation() method is used to check whether this Class object represents the annotation type or…

不要再用main方法测试代码性能了,用这款JDK自带工具

前言作为软件开发人员&#xff0c;我们通常会写一些测试程序用来对比不同算法、不同工具的性能问题。而最常见的做法是写一个main方法&#xff0c;构造模拟场景进行并发测试。如果细心的朋友可能已经发现&#xff0c;每次测试结果误差很大&#xff0c;有时候测试出的结果甚至与…

读书总结:周鸿祎,我的互联网方法论

目录&#xff1a;1、欢迎来到互联网时代2、互联网用户至上3、颠覆式创新4、免费时代5、体验为王6、互联网方法论第一章&#xff1a;互联网时代1、没人能打败趋势&#xff0c;趋势会导致非线性发展。2、信息的流通变快&#xff0c;商家很难利用非对称性赚钱。3、用户的体验最重要…

Java ClassLoader getResources()方法与示例

ClassLoader类的getResources()方法 (ClassLoader Class getResources() method) getResources() method is available in java.lang package. getResources()方法在java.lang包中可用。 getResources() method is used to identify all the resources with the given resource…

Java中Properties类的操作

http://www.cnblogs.com/bakari/p/3562244.html Java中Properties类的操作 知识学而不用&#xff0c;就等于没用&#xff0c;到真正用到的时候还得重新再学。最近在看几款开源模拟器的源码&#xff0c;里面涉及到了很多关于Properties类的引用&#xff0c;由于Java已经好久没用…

复盘线上的一次OOM和性能优化!

来源&#xff1a;r6d.cn/ZazN上周五&#xff0c;发布前一周的服务器小动荡????事情回顾上周五&#xff0c;通过Grafana监控&#xff0c;线上环境突然出现CPU和内存飙升的情况&#xff1a;但是看到网络输入和输入流量都不是很高&#xff0c;所以网站被别人攻击的概率不高&am…

scanf 输入十六进制_在C语言中使用scanf()输入一个十六进制值

scanf 输入十六进制Here, we have to declare an unsigned int variable and input a value in hexadecimal format. 在这里&#xff0c;我们必须声明一个无符号的int变量&#xff0c;并以十六进制格式输入一个值。 To input a value in hexadecimal format – we use "%…

阅读源码的 4 个绝技,我必须分享给你!

为什么要阅读源码&#xff1f;1.在通用型基础技术中提高技术能力在 JAVA 领域中包含 JAVA 集合、Java并发(JUC)等&#xff0c; 它们是项目中使用的高频技术&#xff0c;在各种复杂的场景中选用合适的数据结构、线程并发模型&#xff0c;合理控制锁粒度等都能显著提高应用程序的…

微信公众号开发 ssl connect error

微信获取公众号授权失败 &#xff1a;ssl connect error 本人用的是微擎&#xff0c;也是刚入手&#xff0c;碰到这个问题感觉很棘手。 通过一步步调试发现问题出在curl 认证这里&#xff0c;得到结果错误代码&#xff1a;35&#xff0c;错误信息就是&#xff1a;ssl connect …

struts2的java.lang.NoSuchMethodException异常处理

不久前在学习struts时出现这个错误&#xff0c;在网上搜索了半天&#xff0c;发现答案不一。将其总结如下&#xff0c;以方便大家参考。 1、 你有没有试试看 其它的方法能不能用&#xff0c;要是都是这种情况的话&#xff0c;可能是你的Action类没有继承structs里面的DispatchA…

Java String indexOf(int ch)方法与示例

字符串indexOf(int ch)方法 (String indexOf(int ch) Method) indexOf(int ch) is a String method in Java and it is used to get the index of a specified character in the string. indexOf(int ch)是Java中的String方法&#xff0c;用于获取字符串中指定字符的索引。 If…

innerHTML、innerText和outerHTML、outerText的区别

1、区别描述如下&#xff1a; innerHTML 设置或获取位于对象起始和结束标签内的 HTMLouterHTML 设置或获取对象及其内容的 HTML 形式innerText 设置或获取位于对象起始和结束标签内的文本outerText 设置(包括标签)或获取(不包括标签)对象的文本innerText和outerText在获取时是相…

Socket粘包问题终极解决方案—Netty版(2W字)!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;上一篇我们写了《Socket粘包问题的3种解决方案》&#xff0c;但没想到评论区竟然炸了。介于大家的热情讨论&#xff0c;以及…

Java高质量代码之 — 泛型与反射

在Java5后推出了泛型,使我们在编译期间操作集合或类时更加的安全,更方便代码的阅读,而让身为编译性语言的Java提供动态性的反射技术,更是在框架开发中大行其道,从而让Java活起来,下面看一下在使用泛型和反射需要注意和了解的事情 1.Java的泛型是类型擦除的 Java中的泛型是…

Java LocalDate类| isLeapYear()方法与示例

LocalDate类isLeapYear()方法 (LocalDate Class isLeapYear() method) isLeapYear() method is available in java.time package. isLeapYear()方法在java.time包中可用。 isLeapYear() method is used to check whether the year field value is a leap year or not based on …