1、粘包与半包 啥也不说了,直接上代码是不是有点不太友好,我所谓了,都快过年了,还要啥自行车 我上来就是一段代码猛如虎1。1服务器代码publicclassStudyServer{staticfinalLoggerlogLoggerFactory。getLogger(StudyServer。class);voidstart(){NioEventLoopGroupbossnewNioEventLoopGroup(1);NioEventLoopGroupworkernewNioEventLoopGroup();try{ServerBootstrapserverBootstrapnewServerBootstrap();serverBootstrap。channel(NioServerSocketChannel。class);serverBootstrap。group(boss,worker);serverBootstrap。childHandler(newChannelInitializerSocketChannel(){OverrideprotectedvoidinitChannel(SocketChannelch){ch。pipeline()。addLast(newLoggingHandler(LogLevel。DEBUG));ch。pipeline()。addLast(newChannelInboundHandlerAdapter(){OverridepublicvoidchannelActive(ChannelHandlerContextctx)throwsException{连接建立时会执行该方法log。debug(connected{},ctx。channel());super。channelActive(ctx);}OverridepublicvoidchannelInactive(ChannelHandlerContextctx)throwsException{连接断开时会执行该方法log。debug(disconnect{},ctx。channel());super。channelInactive(ctx);}});}});ChannelFuturechannelFutureserverBootstrap。bind(8080);log。debug({}binding。。。,channelFuture。channel());channelFuture。sync();log。debug({}bound。。。,channelFuture。channel());关闭channelchannelFuture。channel()。closeFuture()。sync();}catch(InterruptedExceptione){log。error(servererror,e);}finally{boss。shutdownGracefully();worker。shutdownGracefully();log。debug(stopped);}}publicstaticvoidmain(String〔〕args){newStudyServer()。start();}}1。2粘包现象 客户端代码publicclassStudyClient{staticfinalLoggerlogLoggerFactory。getLogger(StudyClient。class);publicstaticvoidmain(String〔〕args){NioEventLoopGroupworkernewNioEventLoopGroup();try{BootstrapbootstrapnewBootstrap();bootstrap。channel(NioSocketChannel。class);bootstrap。group(worker);bootstrap。handler(newChannelInitializerSocketChannel(){OverrideprotectedvoidinitChannel(SocketChannelch)throwsException{log。debug(connected。。。);ch。pipeline()。addLast(newChannelInboundHandlerAdapter(){OverridepublicvoidchannelActive(ChannelHandlerContextctx)throwsException{log。debug(sending。。。);每次发送16个字节的数据,共发送10次for(inti0;i10;i){ByteBufbufferctx。alloc()。buffer();buffer。writeBytes(newbyte〔〕{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});ctx。writeAndFlush(buffer);}}});}});ChannelFuturechannelFuturebootstrap。connect(127。0。0。1,8080)。sync();channelFuture。channel()。closeFuture()。sync();}catch(InterruptedExceptione){log。error(clienterror,e);}finally{worker。shutdownGracefully();}}} 服务器接收结果7999〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x5b43ecb0,L:127。0。0。1:8080R:127。0。0。1:53797〕READ:160B0123456789abcdef00000000000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000010000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000020000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000030000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000040000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000050000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000060000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000070000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000080000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000090000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。 可见虽然客户端是分别以16字节为单位,通过channel向服务器发送了10次数据,可是服务器端却只接收了一次,接收数据的大小为160B,即客户端发送的数据总大小,这就是粘包现象1。3半包现象 将客户端服务器之间的channel容量进行调整 服务器代码调整channel的容量serverBootstrap。option(ChannelOption。SORCVBUF,10); 注意 serverBootstrap。option(ChannelOption。SORCVBUF,10)影响的底层接收缓冲区(即滑动窗口)大小,仅决定了netty读取的最小单位,netty实际每次读取的一般是它的整数倍 服务器接收结果5901〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xc73284f3,L:127。0。0。1:8080R:127。0。0。1:49679〕READ:36B0123456789abcdef00000000000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。00000010000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。0000002000010203。。。。5901〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xc73284f3,L:127。0。0。1:8080R:127。0。0。1:49679〕READ:40B0123456789abcdef000000000405060708090a0b0c0d0e0f00010203。。。。。。。。。。。。。。。。000000100405060708090a0b0c0d0e0f00010203。。。。。。。。。。。。。。。。000000200405060708090a0b。。。。。。。。5901〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xc73284f3,L:127。0。0。1:8080R:127。0。0。1:49679〕READ:40B0123456789abcdef000000000c0d0e0f000102030405060708090a0b。。。。。。。。。。。。。。。。000000100c0d0e0f000102030405060708090a0b。。。。。。。。。。。。。。。。000000200c0d0e0f00010203。。。。。。。。5901〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xc73284f3,L:127。0。0。1:8080R:127。0。0。1:49679〕READ:40B0123456789abcdef000000000405060708090a0b0c0d0e0f00010203。。。。。。。。。。。。。。。。000000100405060708090a0b0c0d0e0f00010203。。。。。。。。。。。。。。。。000000200405060708090a0b。。。。。。。。5901〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xc73284f3,L:127。0。0。1:8080R:127。0。0。1:49679〕READ:4B0123456789abcdef000000000c0d0e0f。。。。 可见客户端每次发送的数据,因channel容量不足,无法将发送的数据一次性接收,便产生了半包现象1。4现象分析1。4。1粘包现象发送abcdef,接收abcdef原因应用层接收方ByteBuf设置太大(Netty默认1024);接收方缓冲数据,一起发送传输层网络层滑动窗口:假设发送方256bytes表示一个完整报文,但由于接收方处理不及时且窗口大小足够大(大于256bytes),这256bytes字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包Nagle算法:会造成粘包1。4。2半包现象发送abcdef,接收abcdef原因应用层接收方ByteBuf小于实际发送数据量传输层网络层滑动窗口:假设接收方的窗口只剩了128bytes,发送方的报文大小是256bytes,这时接收方窗口中无法容纳发送方的全部报文,发送方只能先发送前128bytes,等待ack后才能发送剩余部分,这就造成了半包数据链路层MSS限制:当发送的数据超过MSS限制后,会将数据切分发送,就会造成半包1。4。3本质 发生粘包与半包现象的本质是因为TCP是流式协议,消息无边界1。5解决方案1。5。1短链接 客户端每次向服务器发送数据以后,就与服务器断开连接,此时的消息边界为连接建立到连接断开。这时便无需使用滑动窗口等技术来缓冲数据,则不会发生粘包现象。但如果一次性数据发送过多,接收方无法一次性容纳所有数据,还是会发生半包现象,所以短链接无法解决半包现象 客户端代码改进 修改channelActive方法publicvoidchannelActive(ChannelHandlerContextctx)throwsException{log。debug(sending。。。);ByteBufbufferctx。alloc()。buffer(16);buffer。writeBytes(newbyte〔〕{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});ctx。writeAndFlush(buffer);使用短链接,每次发送完毕后就断开连接ctx。channel()。close();} 将发送步骤整体封装为send()方法,调用10次send()方法,模拟发送10次数据publicstaticvoidmain(String〔〕args){发送10次for(inti0;i10;i){send();}} 运行结果6452〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x3eb6a684,L:127。0。0。1:8080R:127。0。0。1:65024〕ACTIVE6468〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x3eb6a684,L:127。0。0。1:8080R:127。0。0。1:65024〕READ:16B0123456789abcdef00000000000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。6468〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x3eb6a684,L:127。0。0。1:8080!R:127。0。0。1:65024〕INACTIVE6483〔nioEventLoopGroup32〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x7dcc31ff,L:127。0。0。1:8080R:127。0。0。1:65057〕ACTIVE6483〔nioEventLoopGroup32〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x7dcc31ff,L:127。0。0。1:8080R:127。0。0。1:65057〕READ:16B0123456789abcdef00000000000102030405060708090a0b0c0d0e0f。。。。。。。。。。。。。。。。6483〔nioEventLoopGroup32〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x7dcc31ff,L:127。0。0。1:8080!R:127。0。0。1:65057〕INACTIVE。。。 客户端先于服务器建立连接,此时控制台打印ACTIVE,之后客户端向服务器发送了16B的数据,发送后断开连接,此时控制台打印INACTIVE,可见未出现粘包现象1。5。2定长解码器 客户端于服务器约定一个最大长度,保证客户端每次发送的数据长度都不会大于该长度。若发送数据长度不足则需要补齐至该长度 服务器接收数据时,将接收到的数据按照约定的最大长度进行拆分,即使发送过程中产生了粘包,也可以通过定长解码器将数据正确地进行拆分。服务端需要用到FixedLengthFrameDecoder对数据进行定长解码,具体使用方法如下ch。pipeline()。addLast(newFixedLengthFrameDecoder(16)); 客户端代码 客户端发送数据的代码如下约定最大长度为16finalintmaxLength16;被发送的数据向服务器发送10个报文for(inti0;i10;i){ByteBufbufferctx。alloc()。buffer(maxLength);定长byte数组,未使用部分会以0进行填充byte〔〕bytesnewbyte〔maxLength〕;生成长度为015的数据for(intj0;j(int)(Math。random()(maxLength1));j){bytes〔j〕(byte)c;}buffer。writeBytes(bytes);c;将数据发送给服务器ctx。writeAndFlush(buffer);} 服务器代码 使用FixedLengthFrameDecoder对粘包数据进行拆分,该handler需要添加在LoggingHandler之前,保证数据被打印时已被拆分通过定长解码器对粘包数据进行拆分ch。pipeline()。addLast(newFixedLengthFrameDecoder(16));ch。pipeline()。addLast(newLoggingHandler(LogLevel。DEBUG)); 运行结果8222〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xbc122d07,L:127。0。0。1:8080R:127。0。0。1:52954〕READ:16B0123456789abcdef0000000061616161000000000000000000000000aaaa。。。。。。。。。。。。8222〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xbc122d07,L:127。0。0。1:8080R:127。0。0。1:52954〕READ:16B0123456789abcdef0000000062626200000000000000000000000000bbb。。。。。。。。。。。。。8222〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xbc122d07,L:127。0。0。1:8080R:127。0。0。1:52954〕READ:16B0123456789abcdef0000000063630000000000000000000000000000cc。。。。。。。。。。。。。。。。。1。5。3行解码器 行解码器的是通过分隔符对数据进行拆分来解决粘包半包问题的 可以通过LineBasedFrameDecoder(intmaxLength)来拆分以换行符()为分隔符的数据,也可以通过DelimiterBasedFrameDecoder(intmaxFrameLength,ByteBuf。。。delimiters)来指定通过什么分隔符来拆分数据(可以传入多个分隔符) 两种解码器都需要传入数据的最大长度,若超出最大长度,会抛出TooLongFrameException异常 以换行符为分隔符 客户端代码约定最大长度为64finalintmaxLength64;被发送的数据for(inti0;i10;i){ByteBufbufferctx。alloc()。buffer(maxLength);生成长度为062的数据RandomrandomnewRandom();StringBuildersbnewStringBuilder();for(intj0;j(int)(random。nextInt(maxLength2));j){sb。append(c);}数据以结尾sb。append();buffer。writeBytes(sb。toString()。getBytes(StandardCharsets。UTF8));c;将数据发送给服务器ctx。writeAndFlush(buffer);} 服务器代码通过行解码器对粘包数据进行拆分,以为分隔符需要指定最大长度ch。pipeline()。addLast(newDelimiterBasedFrameDecoder(64));ch。pipeline()。addLast(newLoggingHandler(LogLevel。DEBUG)); 运行结果4184〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x9d6ac701,L:127。0。0。1:8080R:127。0。0。1:58282〕READ:10B0123456789abcdef0000000061616161616161616161aaaaaaaaaa4184〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x9d6ac701,L:127。0。0。1:8080R:127。0。0。1:58282〕READ:11B0123456789abcdef000000006262626262626262626262bbbbbbbbbbb4184〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x9d6ac701,L:127。0。0。1:8080R:127。0。0。1:58282〕READ:2B0123456789abcdef000000006363cc。。。 以自定义分隔符c为分隔符 客户端代码。。。数据以c结尾sb。append(c);buffer。writeBytes(sb。toString()。getBytes(StandardCharsets。UTF8));。。。 服务器代码将分隔符放入ByteBuf中ByteBufbufSetch。alloc()。buffer()。writeBytes(c。getBytes(StandardCharsets。UTF8));通过行解码器对粘包数据进行拆分,以c为分隔符ch。pipeline()。addLast(newDelimiterBasedFrameDecoder(64,ch。alloc()。buffer()。writeBytes(bufSet)));ch。pipeline()。addLast(newLoggingHandler(LogLevel。DEBUG)); 运行结果8246〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x86215ccd,L:127。0。0。1:8080R:127。0。0。1:65159〕READ:14B0123456789abcdef000000006161616161616161616161616161aaaaaaaaaaaaaa8247〔nioEventLoopGroup31〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0x86215ccd,L:127。0。0。1:8080R:127。0。0。1:65159〕READ:3B0123456789abcdef00000000626262bbb。。。1。5。4长度字段解码器 在传送数据时可以在数据中添加一个用于表示有用数据长度的字段,在解码时读取出这个用于表明长度的字段,同时读取其他相关参数,即可知道最终需要的数据是什么样子的 LengthFieldBasedFrameDecoder解码器可以提供更为丰富的拆分方法,其构造方法有五个参数publicLengthFieldBasedFrameDecoder(intmaxFrameLength,intlengthFieldOffset,intlengthFieldLength,intlengthAdjustment,intinitialBytesToStrip) 参数解析maxFrameLength数据最大长度表示数据的最大长度(包括附加信息、长度标识等内容)lengthFieldOffset数据长度标识的起始偏移量用于指明数据第几个字节开始是用于标识有用字节长度的,因为前面可能还有其他附加信息lengthFieldLength数据长度标识所占字节数(用于指明有用数据的长度)数据中用于表示有用数据长度的标识所占的字节数lengthAdjustment长度表示与有用数据的偏移量用于指明数据长度标识和有用数据之间的距离,因为两者之间还可能有附加信息initialBytesToStrip数据读取起点读取起点,不读取0initialBytesToStrip之间的数据 参数图解 lengthFieldOffset0lengthFieldLength2lengthAdjustment0initialBytesToStrip0(donotstripheader)BEFOREDECODE(14bytes)AFTERDECODE(14bytes)LengthActualContentLengthActualContent0x000CHELLO,WORLD0x000CHELLO,WORLD 从0开始即为长度标识,长度标识长度为2个字节 0x000C即为后面HELLO,WORLD的长度lengthFieldOffset0lengthFieldLength2lengthAdjustment0initialBytesToStrip2(thelengthoftheLengthfield)BEFOREDECODE(14bytes)AFTERDECODE(12bytes)LengthActualContentActualContent0x000CHELLO,WORLDHELLO,WORLD 从0开始即为长度标识,长度标识长度为2个字节,读取时从第二个字节开始读取(此处即跳过长度标识) 因为跳过了用于表示长度的2个字节,所以此处直接读取HELLO,WORLDlengthFieldOffset2(thelengthofHeader1)lengthFieldLength3lengthAdjustment0initialBytesToStrip0BEFOREDECODE(17bytes)AFTERDECODE(17bytes)Header1LengthActualContentHeader1LengthActualContent0xCAFE0x00000CHELLO,WORLD0xCAFE0x00000CHELLO,WORLD 长度标识前面还有2个字节的其他内容(0xCAFE),第三个字节开始才是长度标识,长度表示长度为3个字节(0x00000C) Header1中有附加信息,读取长度标识时需要跳过这些附加信息来获取长度lengthFieldOffset0lengthFieldLength3lengthAdjustment2(thelengthofHeader1)initialBytesToStrip0BEFOREDECODE(17bytes)AFTERDECODE(17bytes)LengthHeader1ActualContentLengthHeader1ActualContent0x00000C0xCAFEHELLO,WORLD0x00000C0xCAFEHELLO,WORLD 从0开始即为长度标识,长度标识长度为3个字节,长度标识之后还有2个字节的其他内容(0xCAFE) 长度标识(0x00000C)表示的是从其后lengthAdjustment(2个字节)开始的数据的长度,即HELLO,WORLD,不包括0xCAFElengthFieldOffset1(thelengthofHDR1)lengthFieldLength2lengthAdjustment1(thelengthofHDR2)initialBytesToStrip3(thelengthofHDR1LEN)BEFOREDECODE(16bytes)AFTERDECODE(13bytes)HDR1LengthHDR2ActualContentHDR2ActualContent0xCA0x000C0xFEHELLO,WORLD0xFEHELLO,WORLD 长度标识前面有1个字节的其他内容,后面也有1个字节的其他内容,读取时从长度标识之后3个字节处开始读取,即读取0xFEHELLO,WORLD 使用 通过EmbeddedChannel对handler进行测试publicclassEncoderStudy{publicstaticvoidmain(String〔〕args){模拟服务器使用EmbeddedChannel测试handlerEmbeddedChannelchannelnewEmbeddedChannel(数据最大长度为1KB,长度标识前后各有1个字节的附加信息,长度标识长度为4个字节(int)newLengthFieldBasedFrameDecoder(1024,1,4,1,0),newLoggingHandler(LogLevel。DEBUG));模拟客户端,写入数据ByteBufbufferByteBufAllocator。DEFAULT。buffer();send(buffer,Hello);channel。writeInbound(buffer);send(buffer,World);channel。writeInbound(buffer);}privatestaticvoidsend(ByteBufbuf,Stringmsg){得到数据的长度intlengthmsg。length();byte〔〕bytesmsg。getBytes(StandardCharsets。UTF8);将数据信息写入buf写入长度标识前的其他信息buf。writeByte(0xCA);写入数据长度标识buf。writeInt(length);写入长度标识后的其他信息buf。writeByte(0xFE);写入具体的数据buf。writeBytes(bytes);}} 运行结果146〔main〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xembedded,L:embeddedR:embedded〕READ:11B0123456789abcdef00000000ca00000005fe48656c6c6f。。。。。。Hello146〔main〕DEBUGio。netty。handler。logging。LoggingHandler〔id:0xembedded,L:embeddedR:embedded〕READ:11B0123456789abcdef00000000ca00000005fe576f726c64。。。。。。World 作者:博学谷 链接:https:juejin。cnpost7185044730512670781