我们都知道TCP是一种基于字节流的传输协议。那么在通信层传播的数据其实就像一条河流,并没有明显的分界线,但是数据具体是什么意思,哪里有句号,哪里有分号,TCP底层就不清楚了。应用层向TCP层发送一个8位字节表示的用于跨网传输的数据流,然后TCP将数据流分成适当长度的段,然后TCP将结果包发送给IP层,IP层将通过网络将数据包传输到接收实体的TCP层。那么把这个数据拆分成大小包的问题就是我们今天要讲的粘包和解包的问题。1.TCPsticking和unpacking问题的解释sticking和unpacking这两个概念大家可能不太清楚。我们通过下图来分析一下:假设客户端向服务端分别发送了两个数据包D1和D2。但是,在发送过程中,并不清楚数据是以什么形式传输的。有以下四种情况:服务器同时收到两个数据包D1和D2,两个数据包粘在一起,称为粘包;数据包D1和D2被读取了两次,没有发生粘包和拆包;服务器读取数据包两次,第一次读取D1和D2的一部分,第二次读取D2剩下的部分,这叫做拆包;服务器读取数据部分三次,第一次读取D1包,第二次读取D2包的一部分,第三次读取D2包的其余部分。2、TCP粘包产生的原因我们知道,在TCP协议中,应用数据被分成TCP认为最适合发送的数据块。这部分由“MSS”(最大数据包长度)选项控制。通常这种机制也称为协商机制,MSS指定了TCP可以发送给另一端的最大数据块的长度。这个值在TCP协议实现时经常被MTU值代替(需要减去IP包头20Bytes和TCP数据段头20Bytes的大小),所以MSS往往是1460。通信双方会确定此连接的最大MSS值基于双方提供的MSS值的最小值。为了提高tcp的性能,发送方会将要发送的数据发送到缓冲区,等待缓冲区满了,再将缓冲区中的数据发送给接收方。同样,接收端也有缓冲区等机制来接收数据。粘包解包的主要原因有:应用程序写入的数据字节大小大于socket发送缓冲区的大小,会发生解包;执行MSS大小的TCP分段。MSS是TCP消息段中数据字段的最大长度。当TCP报文长度-TCP头长度>mss时,会发生解包;应用程序写入的数据小于socketbuffer的大小,网卡会使用更多如果第一次写入的数据发送到网络,会出现粘包;当数据包大于MTU时,将被分片。MTU是最大传输单元(MaxitumTransmissionUnit)。由于以太网传输的电气限制,每个以太网帧的最小大小为64字节,最大为1518字节。去掉以太网帧的帧头14Bytes和帧尾的CRC校验。如果校验部分是4Bytes,那么其余的上层协议,也就是Data域只能有1500Bytes的值,我们称之为MTU。这是网络层协议非常关心的地方,因为IP协议等网络层协议会根据这个值来决定是否对上层传来的数据进行分片。3、如何解决TCP粘包拆包我们知道tcp是无界数据流,协议本身无法避免粘包和拆包,所以只能在应用层数据协议上进行控制。通常,在制定传输数据时可以采用以下方法:设置一个固定长度的消息,服务器每次读取预定长度的内容作为一个完整的消息;使用带有消息头的协议,消息头中存放消息起始标识和消息长度信息,服务端获取到消息头后,解析出消息的长度,然后向后读取长度的内容;设置消息边界,服务器根据消息边界将消息内容从网络流中分离出来。例如,在消息末尾添加一个换行符,以区分消息的结束。当然,在应用层还有更复杂的方法来解决这个问题。这是网络层的问题。我们还是使用java提供的方法来解决这个问题。SpringBoot学习笔记分享给大家,我们先来看一个例子,看看粘包是怎么发生的。服务端:publicclassHelloWordServer{privateintport;publicHelloWordServer(intport){this.port=port;}publicvoidstart(){EventLoopGroupbossGroup=newNioEventLoopGroup();EventLoopGroupworkGroup=newNioEventLoopGroup();ServerBootstrapserver=newServerBootstrap().group(bossGroup,workGroup).channel(NioServerSocketChannel.class).childHandler(newServerChannelInitializer());try{ChannelFuturefuture=server.bind(port).sync();future.channel().closeFuture().sync();}catch(InterruptedExceptione){e.printStackTrace();}finally{bossGroup.shutdownGracefully();workGroup.shutdownGracefully();}}publicstaticvoidmain(String[]args){HelloWordServerserver=newHelloWordServer(7788);server.start();}}服务端初始化:publicclassServerChannelInitializerextendsChannelInitializer{@OverrideprotectedvoidinitChannel(SocketChannelsocketChannel)throwsException{ChannelPipelinepipeline=socketChannel.pipeline();//字串解析和编码pipeline.addLast("decoder",newStringDecoder());pipeline.addLast("encoder",newStringEncoder());//自己的递归Handlerpipeline.addLast("handler",newHelloWordServerHandler());}}服务端handler:publicclassHelloWordServerHandlerextendsChannelInboundHandlerAdapter{privateintcounter;@OverridepublicvoidchannelRead(Chanctxrow,andlermExceptions){Stringbody=(String)msg;System.out.println("serverreceiveorder:"+body+";thecounteris:"+++counter);}@OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)throwsException{super.exceptionCaught(ctx,cause);}}客户端:publicclassHelloWorldClient{privateintport;privateStringaddress;publicHelloWorldClient(intport,Stringaddress){this.port=port;this.address=address;}publicvoidstart(){EventLoopGroupgroup=newNioEventLoopGroup();Bootstrapbootstrap=newBootstrap();bootstrap.group(group).channel(NioSocketChannel.class).handler(newClientChannelInitializer());try{ChannelFuturefuture=bootstrap.connect(address,port).sync();future.channel().closeFuture().sync();}catch(Exceptione){e.printStackTrace();}finally{group.shutdownGracefully();}}publicstaticvoidmain(String[]args){HelloWorldClientclient=newHelloWorldClient(7788,"127.0.0.1");client.start();}}客户端初始化器:publicclassClientChannelInitializerextendsChannelInitializer{protectedvoidinitChannel(SocketChannelsocketChannel)throwsException{ChannelPipelinepipeline=socketChannel.pipeline();Stripdeline.addLast("stringDecoder",new());pipeline.addLast("encoder",newStringEncoder());//客户端的发送pipeline.addLast("handler",newHelloWorldClientHandler());}}客户端handler:publicclassHelloWorldClientHandlerextendsChannelInboundHandlerAdapter{privatebyte[]req;privateintcounter;publicBaseClientHandler(){req=("除非适用法律要求或书面同意,否则软件\n"+"在许可证下分发\"ASIS\"BASIS,\n"+"WITHOUTWARRANTIESORCONDITIONSOFANYKIND,eitherexpressorimplied.\n"+"SeetheLicenseforthespecificlanguagegoverningpermissionsand\n"+"limitationsundertheLicense.ThisconnectorusestheBIOimplementationthatrequirestheJSSE\n"+"styleconfiguration.WhenusingtheAPR/nativeimplementation,the\n"+"penSSLstyleconfigurationisrequiredasdescribedintheAPR/native\n"+"documentation.AnEnginerepresentstheentrypoint(withinCatalina)thatprocesses\n"+"每个请求。Tomcat独立的引擎实现\n"+"分析包含在请求中的HTTP标头,并将它们\n"+"传递到适当的主机(虚拟主机)#Unlessrequiredbyapplicablelaworagreeedtoinwriting,software\n"+"#distributedundertheLicenseisdistributedonan\"ASIS\"BASIS,\n"+"#ORTHOUTCONDINANTFANYIES,明示或暗示。\n"+"#SeetheLicenseforthespecificlanguagegoverningpermissionsand\n"+"#limitationsundertheLicense.#Forexample,settheorg.apache.catalina.util.LifecycleBaseloggertolog\n"+"#eachcomponentthatextendsLifecycleBasechangingstate:\n"+"#org.apache.catalina.util.LifecycleBase.level=FINE").getBytes();}@OverridepublicvoidchannelActive(ChannelHandlerContextctx)throwsException{ByteBufmessage;//将以上所有字符串作为消息体发送message=Unpooled.buffer(req.length);message.writeBytes(req);ctx.writeAndFlush(message);}@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{Stringbuf=(String)msg;System.out.println("Nowis:"+buf+";thecounteris:"+(++counter));}@OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)throwsException{ctx.close();}}运行客户端和服务端可以看到:我们看到这个长字符串被截成2段发送,这就是解包现象。我们也很容易模拟粘包。我们把channelActive方法放在BaseClientHandler中:message=Unpooled.buffer(req.length);message.writeBytes(req);ctx.writeAndFlush(消息);这几行代码是将上面一长串字符转换成的字节数组写入流中发送出去,然后我们可以将上面发送的消息的行循环几次,这样发送的内容就增加了,就可以解包赋值了上一条消息的一部分到下一条消息,修改如下:for(inti=0;i<3;i++){message=Unpooled.buffer(req.length);message.writeBytes(req);ctx。writeAndFlush(message);}修改后,我们再运行一??下。输出太长,无法截图。我们在输出中可以看到,服务器在循环3次后收到的消息并不是之前的完整消息,而是被拆分发送了4次。针对上面出现的粘包和拆包问题,Netty考虑并实现了一个方案:LineBasedFrameDecoder。另外,微信搜索Java技术栈,后台回复:面试,可以得到我整理的Java系列面试题及答案。我们重新改写一下ServerChannelInitializer:publicclassServerChannelInitializerextendsChannelInitializer{@OverrideprotectedvoidinitChannel(SocketChannelsocketChannel)throwsException{ChannelPipelinepipeline=socketChannel.pipeline();pipeline.addLast(newLineBasedFrameDecoder(2048));//字符串解码和编码pipeline.addLast("decoder",newStringDecoder());pipeline.addLast("encoder",newStringEncoder());//OwnlogicHandlerpipeline.addLast("handler",newBaseServerHandler());}}Add:pipeline.addLast(newLineBasedFrameDecoder(2048)).同时,我们还得对上面发送的消息进行改造BaseClientHandler:publicclassBaseClientHandlerextendsChannelInboundHandlerAdapter{privatebyte[]req;privateintcounter;req=("Unlessrequiredbyapplicabledfslaworagreedtoinwriting,software"+"distributedundertheLicenseisdistributedonan\"ASIS\"BASIS,"+"WITHOUTWARRANTIESORCONDITIONSOFANYKIND,eitherexpressorimplied."+"SeetheLicenseforthespecificlanguagegoverningpermissionsand"+"limitationsundertheLicense.ThisconnectorusestheBIOimplementationthatrequirestheJSSE"+"styleconfiguration.WhenusingtheAPR/nativeimplementation,the"+"penSSLstyleconfigurationisrequiredasdescribedintheAPR/native"+"documentation.AnEnginerepresentstheentrypoint(withinCatalina)thatprocesses"+"everyrequest.TheEngineimplementationforTomcatstandalone"+"analyzestheHTTPheadersincludedwiththerequest,andpassesthem"+"ontotheappropriateHost(虚拟主机)#Unlessrequiredbyapplicablelaworagreedtoinwriting,software"+"#distributedundertheLicenseisdistributedonan\"ASIS\"BASIS,"+"#WITHOUTWARRANTIESORCONDITIONSOFANYKIND,eitherexpressorimplied."+"#SeetheLicenseforthespecificlanguagegoverningpermissionsand"+"#limitationsundertheLicense.#Forexample,settheorg.apache.catalina.util."LifecycleBase"#loggertolog应用程序"+"#distributedundertheLicenseisdistributedonan\"ASIS\"BASIS"eachcomponentthatextendsLifecycleBasechangingstate:"+"#org.apache.catalina.util.LifecycleBase.level=FINE\n").getBytes();@OverridepublicvoidchannelActive(ChannelHandlerContextctx)throwsException{ByteBufmessage;message=Unpooled.buffer(req.length);消息。writeBytes(req);ctx.writeAndFlush(message);}@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{Stringbuf=(String)msg;System.out.println("Nowis:"+buf+";thecounteris:"+(++计数器));}@OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)throwsException{ctx.close();}}去掉所有的“\n”,只保留字符串末尾的原因。我稍后再说。在channelActive方法中,我们不用循环多次发送消息,只发送一次(第一个例子中,发送一次就发生了解包),然后我们再次运行,就会看到这样一个longstring只发送一串字符,发送完成。我不会截取程序输出的屏幕截图。让我们解释一下LineBasedFrameDecoder。LineBasedFrameDecoder的工作原理是逐个遍历ByteBuf中的可读字节,判断是否有“\n”或“\r\n”。如果是,则将这个位置作为结束位置,从可读索引到结束位置的一段字节组成一行。它是换行符终止字符的解码器。支持带终结符和不带终结符两种解码方式,支持配置单行最大长度。如果连续读取到最大长度后仍未找到换行符,则抛出异常,忽略之前读取的异常码流。这个对于我们确定消息最大长度的应用场景还是很有帮助的。对于上面判断是否有“\n”或“\r\n”作为结束符,我们可以回忆一下,如果没有“\n”或“\r\n”,那么还有其他的判断方式消息结束了吗?不用担心,Netty已经考虑到了这一点,还有其他解码器可以帮助我们解决问题。另外,关注公众号Java技术栈,后台回复:面试,可以拿到我整理的Java系列面试题及答案,很全。