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

在 Java 语言中,传统的 Socket 编程分为两种实现方式,这两种实现方式也对应着两种不同的传输层协议:TCP 协议和 UDP 协议,但作为互联网中最常用

老铁们,大家好,相信还有很多朋友对于Socket粘连问题的3种解决方案,最后一种最完美和的相关问题不太懂,没关系,今天就由我来为大家分享分享Socket粘连问题的3种解决方案,最后一种最完美以及的问题,文章篇幅可能偏长,希望可以帮助到大家,下面一起来看看吧!

什么是 TCP 协议?

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

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

图片.png

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

TCP 的全部要点在于它是可靠的,它通过对数据包进行编号来对数据包进行排序,并且通过让服务器向浏览器发送“已收到”的响应来进行错误检查,因此在传输过程中不会丢失或损坏任何数据。

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

什么是粘包和半包问题?

粘包问题是指发送了两条消息,例如ABC和DEF,但另一端收到了ABCD。这种同时读取两块数据的情况称为粘包(一般情况下应该是一条一条地读取)。

半包问题是指当发送的消息为ABC时,另一端收到AB和C两条消息,这种情况称为半包。

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

这是因为TCP是面向连接的传输协议。 TCP传输的数据是流的形式,而流数据没有明确的开始和结束边界,因此TCP无法确定哪个流段属于某个消息。

粘包的主要原因:

发送方每次向套接字(Socket)缓冲区写入数据大小;接收方没有及时读取套接字(Socket)缓冲区中的数据。

半包的主要原因:

发送方每次向socket缓冲区写入数据大小;发送的数据大于协议的MTU(最大传输单元),因此必须解包。

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

缓冲区也称为高速缓存,是内存空间的一部分。也就是说,内存空间中预留了一定的存储空间。该存储空间用于缓冲输入或输出数据。这个保留的空间称为缓冲区。

缓冲区的优点是以写入文件流为例。如果我们不使用缓冲区,那么CPU每次写操作都会与低速存储设备即磁盘进行交互,整个文件写入速度就会受到低速存储设备的限制。存储设备(磁盘)。但如果使用缓冲区,则每次写操作都会首先将数据保存在高速缓冲存储器中。当缓冲区中的数据达到一定阈值时,文件将立即写入磁盘。由于内存的写入速度远大于磁盘的写入速度,因此当提供缓冲区时,文件的写入速度会大大提高。

粘包和半包问题演示

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

服务器用于接收消息;客户端用于发送固定消息。然后通过打印服务器收到的信息来观察粘包和半包问题。

服务器端代码如下:

/***服务器端(只负责接收消息)*/classServSocket{//字节数组的长度privatestaticfinalintBYTE_LENGTH=20; publicstaticvoidmain(String[]args) throwsIOException{//创建Socket服务器ServerSocketserverSocket=newServerSocket(9999);//获取客户端连接SocketclientSocket=serverSocket.accept();//获取客户端发送的流对象try(InputStreaminputStream=clientSocket.getInputStream()){while(true){//循环获取客户端发送的信息byte[]bytes=newbyte[BYTE_LENGTH];//读取客户端发送的信息intcount=inputStream.read(bytes, 0,BYTE_LENGTH);if(count0){//成功接收并打印一条有效消息System.out.println(‘接收到客户端的信息为:’+newString(bytes));}count=0;}} }}客户端代码如下:

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

/***客户端(只负责发送消息)*/staticclassClientSocket{publicstaticvoidmain(String[]args) throwsIOException{//创建Socket客户端并尝试连接服务器Socketsocket=newSocket(‘127.0.0.1′,9999) ;//发送的消息内容finalStringmessage=’Hi,Java.’;//使用输出流发送消息try(OutputStreamoutputStream=socket.getOutputStream()){//向服务器发送10次消息(inti=0;i10;i++ ){//发送消息outputStream.write(message.getBytes());}}}} 上述程序的通信结果如下图所示:

从上面的结果可以看出,粘包和半包的问题发生在服务器端,因为客户端发送了固定的“Hi, Java”。留言10次。正常的结果应该是服务器端也收到了10次。固定消息是正确的,但实际结果却不正确。

粘包和半包的解决方案

粘袋和半袋有3种解决方案:

发送方和接收方指定一个固定大小的缓冲区,即发送和接收都使用固定大小的byte[]数组长度。当字符长度不够时,用空字符来弥补;在TCP协议的基础上封装了一层数据请求协议。将数据包封装成数据头(存储数据体大小)+数据体的形式,这样服务器就可以知道每个数据包的具体长度。知道了发送数据的具体边界之后,问题就解决了一半。包裹和粘包裹的问题消失了;以特殊字符结尾,例如“\n”,这样我们就知道结束字符,从而避免半换行和粘换行的问题(推荐解决方案)。那么我们来演示一下上述方案的具体代码实现。

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

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

服务器端实现代码如下:

/***服务器端,改进版本一(只负责接收消息)*/staticclassServSocketV1{privatestaticfinalintBYTE_LENGTH=1024;//字节数组长度(用于接收消息) publicstaticvoidmain(String[]args)throwsIOException{ServerSocketserverSocket=newServerSocket(9091) ;//获取连接SocketclientSocket=serverSocket.accept();try(InputStreaminputStream=clientSocket.getInputStream()){while(true){byte[]bytes=newbyte[BYTE_LENGTH];//读取客户端发送的信息intcount=inputStream.read(bytes,0,BYTE_LENGTH);if(count0){//收到消息打印System.out.println(‘从客户端收到的消息为:’+newString(bytes).trim());} count=0;}}}}客户端实现代码如下:

/***客户端,改进版本一(只负责接收消息)*/staticclassClientSocketV1{privatestaticfinalintBYTE_LENGTH=1024;//字节长度publicstaticvoidmain(String[]args)throwsIOException{Socketsocket=newSocket(‘127.0.0.1′,9091) ; FinalStringmessage=’Hi,Java.’;//发送消息try(OutputStreamoutputStream=socket.getOutputStream()){//将数据组装成定长字节数组byte[]bytes=newbyte[BYTE_LENGTH];intidx=0; for(byteb:message.getBytes()){bytes[idx]=b;idx++;}//向服务器发送10条消息for(inti=0;i10;i++){outputStream.write(bytes,0,BYTE_LENGTH); }}}}上述代码的执行结果如下图所示:

优缺点分析

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

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

该方案的实现思路是将请求的数据封装成两部分:数据头+数据体。将数据体的大小存储在数据头中。当读取的数据小于数据头中的大小时,继续读取数据,直到读取的数据长度等于数据头中的长度。

由于这种方法可以得到数据的边界,因此不会造成粘包和半包的问题。但这种实现方式的编码成本较高,而且不够优雅,所以并不是最佳的实现方案,所以这里我们就跳过它,直接看最终方案。

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

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

这个方案的核心是利用Java自带的BufferedReader和BufferedWriter,即带有缓冲区的输入字符流和输出字符流,写入时在末尾添加\n,读取时使用readLine。按行读取数据,这样就知道流的边界,从而解决粘包和半包的问题。

服务器端实现代码如下:

/***服务器端,改进版本三(只负责接收消息)*/staticclassServSocketV3{publicstaticvoidmain(String[]args)throwsIOException{//创建Socket服务器ServerSocketserverSocket=newServerSocket(9092);//获取客户端连接SocketclientSocket=serverSocket.accept();//使用线程池处理更多客户端ThreadPoolExecutorthreadPool=newThreadPoolExecutor(100,150,100,TimeUnit.SECONDS,newLinkedBlockingQueue(1000));threadPool.submit(()-{//消息处理processMessage(clientSocket); });}/***消息处理*@paramclientSocket*/privatestaticvoidprocessMessage(SocketclientSocket){//获取客户端发送的消息流对象try(BufferedReaderbufferedReader=newBufferedReader(newInputStreamReader(clientSocket.getInputStream()))){while( true ){//逐行读取客户端发送的消息Stringmsg=bufferedReader.readLine();if(msg!=null){//成功接收到客户端发来的消息并打印System.out.println(‘收到客户端发来的消息信息:’+msg);}}}catch(IOExceptionioException){ioException.printStackTrace();}}} PS:上面代码使用线程池解决了多个客户端访问服务器的问题同时,从而实现一对多的服务器响应。

客户端实现代码如下:

/***客户端,改进版本三(只负责发送消息)*/staticclassClientSocketV3{publicstaticvoidmain(String[]args) throwsIOException{//启动Socket并尝试连接服务器Socketsocket=newSocket(‘127.0.0.1′, 9092); FinalStringmessage=’Hi,Java.’;//发送消息try(BufferedWriterbufferedWriter=newBufferedWriter(newOutputStreamWriter(socket.getOutputStream()))){//向服务器发送10条消息for(inti=0;i10;i++){//注意:末尾的\n不能省略,表示按行写入bufferedWriter.write(message+’\n’);//刷新缓冲区(这一步不能省略) bufferedWriter.flush();} }}} 上面代码的执行结果如下图所示:

用户评论

Socket粘连问题的3种解决方案,最后一种最完美
箜明

终于找到解决 Socket 粘包问题的文章了!我之前一直困扰这个问题,导致数据混乱影响程序正常运行。感谢作者分享这三种方案,我打算先试试第一种方法,看看效果如何。

    有20位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
葵雨

太厉害了!我一直以为粘包问题只有重写程序才能解决,没想到还有这么巧妙的解决方案。这三种方法都很有实用性,特别是最后一种最完美的方法,让我对网络编程有了更深的理解

    有11位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
我怕疼别碰我伤口

同意文章里的说法,第一种 TEACK-数据分割办法确实有其局限性,在实时系统可能会造成较大延迟影响。但第四种方案太复杂了,需要对底层协议进行深入修改,一般开发者不太容易实现吧?

    有18位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
良人凉人

最近也在开发一款跨平台的应用,遇到不少 Socket 粘包问题,搞得人头疼!这篇文章真是及时雨啊!我打算把三种解决方案都试一下,看看哪个效果最好。希望能顺利解决这个难题

    有20位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
容纳我ii

我比较认同文章中最后一种最完美的方法,从底层协议优化角度出发,能有效避免粘包问题,代码也更易维护。但我个人觉得,第一种方法在简单的场景下使用也是可以的,效率也很高。

    有14位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
怪咖

这篇文章挺详细的,虽然我接触过 Socket 比较少, 但还是能理解文章的意思。感谢作者的分享!

    有7位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
枫无痕

为什么最后一项总是最完美呢?感觉有点主观啊,没提到其他方法的优缺点,直接说 “最完美” 就显得有些可疑了。

    有18位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
不浪漫罪名

确实有时候会遇到 Socket 粘包的问题,搞得程序数据混乱。不过我觉得很多时候可以通过更规范的设计避免这类问题,而不是仅仅依赖于解决方案来解决根源问题啊!

    有18位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
糖果控

作者分析的很有深度,将三种方法分别讲解清楚,并指出各自的优缺点,能帮助读者更好地选择合适的解决方案。点赞!

    有18位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
肆忌

我的项目中也遇到了 Socket 粘包问题,看这篇文章我心里终于有了数!第一种 TEACK 的方法听起来很简单,可以直接操作,但我还是比较想尝试最后一种最完美的方法,看看效果怎么样!

    有5位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
来瓶年的冰泉

我一直觉得解决网络编程的问题更应该从设计层面去思考,而不是单纯依靠各种hack技巧。这些解决方案虽然能暂时缓解问题,但本质上并没有真正解决粘包产生的根源原因啊!

    有12位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
执拗旧人

对于大型复杂的项目来说,使用最完美的方法确实是一个不错的方案,能够提高代码的稳定性和可维护性。但是对于小型程序来说,可能不太需要那么复杂的操作了,使用其他方法也能达到预期的效果。文章说得很有道理!

    有15位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
烟雨离殇

我刚开始学编程,对 Socket 协议还不是很了解。看了这篇文章之后,我对粘包问题的产生机制和解决方案有了更深入的理解。感谢作者分享,真是太牛了!

    有10位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
她最好i

这三种解决方案虽然各有优缺点,但都没有能彻底解决粘包问题啊!感觉这是一个永远无法完全根治的问题,只能采取各种措施尽可能地减少出现频率吧!

    有19位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
凝残月

我很认同文章中最后一种最完美的方法的逻辑,避免底层协议出现错误就能从根本上解决粘包问题。如果时间允许,我一定会尝试实现这个方案!

    有20位网友表示赞同!

Socket粘连问题的3种解决方案,最后一种最完美
淡抹丶悲伤

这篇文章写得太好了! 以前做项目时也遇到过 Socket 粘包问题,现在看到这么详细的解释和解决方案,真是受益匪浅啊!

    有10位网友表示赞同!

原创文章,作者:小su,如若转载,请注明出处:https://www.sudun.com/ask/116508.html

(0)
小su's avatar小su
上一篇 2024年9月1日 上午6:46
下一篇 2024年9月1日 上午6:52

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注