概述 本文介绍什么是TCP粘包和拆包现象,并通过Netty编写详细的案例来重现TCP粘包问题,最后再通过一个Netty的demo来解决这个问题。具体内容如下什么是TCP粘包和拆包现象重现TCP粘包和拆包现象Netty解决TCP粘包和拆包现象带来的问题什么是TCP粘包和拆包现象 TCP编程底层都有粘包和拆包机制,因为我们在CS这种传输模型下,以TCP协议传输的时候,在网络中的byte其实就像是河水,TCP就像一个搬运工,将这流水从一端转送到另一端,这时又分两种情况:如果客户端的每次制造的水比较多,也就是我们常说的客户端给的包比较大,TCP这个搬运工就会分多次去搬运如果客户端每次制造的水比较少的话,TCP可能会等客户端多次生产之后,把所有的水一起再运输到另一端对于第一种情况,TCP会再客户端先进行拆包,在另一端接收的时候,需要把多次获取的结果组合在一起,变成我们可以理解的信息对于第二种情况,TCP会在客户端先进行粘包,在另一端接收的时候,就必须进行拆包处理,因为每次接收的信息,可能是另一个远程端多次发送的包,被TCP粘在一起的重现TCP粘包和拆包现象通过在客户端1次发送超大数据包给服务器端来重现TCP拆包现象通过在客户端分10次发送较小的数据包给服务器端来重现TCP粘包现象 下面通过Netty重现TCP粘包和拆包现象。Nettymaven依赖dependencygroupIdio。nettygroupIdnettyallartifactIdversion4。1。76。Finalversiondependency通过Netty重现TCP拆包现象Netty客户端启动类:NettyClientpackagecom。ckjava。test。importio。netty。bootstrap。Bimportio。netty。channel。ChannelIimportio。netty。channel。ChannelOimportio。netty。channel。ChannelPimportio。netty。channel。EventLoopGimportio。netty。channel。nio。NioEventLoopGimportio。netty。channel。socket。SocketCimportio。netty。channel。socket。nio。NioSocketCimportlombok。extern。slf4j。Slf4j;importorg。springframework。stereotype。CSlf4jComponentpublicclassNettyClient{staticfinalStringHOSTSystem。getProperty(host,127。0。0。1);staticfinalintPORTInteger。parseInt(System。getProperty(port,8080));staticfinalintSIZEInteger。parseInt(System。getProperty(size,256));publicstaticvoidmain(String〔〕args)throwsException{初始化客户端事件组EventLoopGroupgroupnewNioEventLoopGroup();try{BootstrapbnewBootstrap();b。group(group)初始化通道。channel(NioSocketChannel。class)。option(ChannelOption。TCPNODELAY,true)。handler(newChannelInitializerSocketChannel(){初始化通道处理器OverridepublicvoidinitChannel(SocketChannelch){ChannelPipelinepch。pipeline();p。addLast(newNettyClientHandler());}});b。connect(HOST,PORT)。addListener(future{log。info(String。format(连接服务器端:s:s成功!,HOST,PORT));})。await();}catch(Exceptione){log。error(启动客户端出现异常,e);}}}Netty客户端通道处理类:NettyClientHandlerpackagecom。ckjava。test。importio。netty。buffer。ByteBimportio。netty。buffer。Uimportio。netty。channel。ChannelHandlerCimportio。netty。channel。SimpleChannelInboundHimportlombok。extern。slf4j。Slf4j;importjava。nio。charset。StandardCimportjava。util。concurrent。TimeUimportjava。util。concurrent。atomic。AtomicISlf4jpublicclassNettyClientHandlerextendsSimpleChannelInboundHandlerByteBuf{privatefinalAtomicIntegercountRefnewAtomicInteger(0);客户端读取服务器发送的信息OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,ByteBufmsg)throwsException{byte〔〕buffernewbyte〔msg。readableBytes()〕;msg。readBytes(buffer);StringmessagenewString(buffer,StandardCharsets。UTF8);log。info(String。format(客户端接收到消息:〔s〕,message));log。info(String。format(客户端接收到消息的次数:s,countRef。accumulateAndGet(1,Integer::sum)));log。info();}重写channelActive,当客户端启动的时候自动发送数据给服务端OverridepublicvoidchannelActive(ChannelHandlerContextctx)throwsException{客户端只发送一次,但是本次数据量很大tcp会将数据拆分成多份后依次进行发送SStringBuilderstringBuildernewStringBuilder(data);for(inti0;i10000;i){stringBuilder。append(data);}ByteBufbufferUnpooled。copiedBuffer(数据stringBuilder。toString(),StandardCharsets。UTF8);ctx。writeAndFlush(buffer);}OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause){ctx。close();}} 其中关键的代码如下重写channelActive,当客户端启动的时候自动发送数据给服务端OverridepublicvoidchannelActive(ChannelHandlerContextctx)throwsException{客户端只发送一次,但是本次数据量很大tcp会将数据拆分成多份后依次进行发送SStringBuilderstringBuildernewStringBuilder(data);for(inti0;i10000;i){stringBuilder。append(data);}ByteBufbufferUnpooled。copiedBuffer(数据stringBuilder。toString(),StandardCharsets。UTF8);ctx。writeAndFlush(buffer);}Netty客户端启动类:NettyClientpackagecom。ckjava。test。importio。netty。bootstrap。ServerBimportio。netty。channel。ChannelIimportio。netty。channel。ChannelOimportio。netty。channel。EventLoopGimportio。netty。channel。nio。NioEventLoopGimportio。netty。channel。socket。SocketCimportio。netty。channel。socket。nio。NioServerSocketCimportlombok。extern。slf4j。Slf4j;importorg。springframework。stereotype。Cimportjava。net。InetSocketASlf4jComponentpublicclassNettyServer{publicNettyServer(intport){this。}publicvoidstart(){EventLoopGroupbossGroupnewNioEventLoopGroup(1);EventLoopGroupworkerGroupnewNioEventLoopGroup();try{ServerBootstrapsbsnewServerBootstrap()。group(bossGroup,workerGroup)。channel(NioServerSocketChannel。class)。localAddress(newInetSocketAddress(port))。childHandler(newChannelInitializerSocketChannel(){protectedvoidinitChannel(SocketChannelch){ch。pipeline()。addLast(newNettyServerHandler());}})。option(ChannelOption。SOBACKLOG,128)。childOption(ChannelOption。SOKEEPALIVE,true);绑定端口,开始接收进来的连接sbs。bind(port)。addListener(future{log。info(String。format(服务器端启动成功,开放端口:s,port));});}catch(Exceptione){log。error(启动服务器端出现异常,e);}}publicstaticvoidmain(String〔〕args){intport8080;newNettyServer(port)。start();}}Netty服务器端通道处理类:NettyServerpackagecom。ckjava。test。importio。netty。buffer。ByteBimportio。netty。buffer。Uimportio。netty。channel。ChannelHandlerCimportio。netty。channel。SimpleChannelInboundHimportlombok。extern。slf4j。Slf4j;importjava。nio。charset。StandardCimportjava。util。UUID;Slf4jpublicclassNettyServerHandlerextendsSimpleChannelInboundHandlerByteBuf{OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,ByteBufmsg){byte〔〕buffernewbyte〔msg。readableBytes()〕;msg。readBytes(buffer);将buffer转为字符串StringmessagenewString(buffer,StandardCharsets。UTF8);System。out。println(服务器收到的数据message);System。out。println(服务器收到的数据次数(this。count));服务器回送数据给客户端回送一个随机idByteBufbuffer1Unpooled。copiedBuffer(UUID。randomUUID()。toString(),StandardCharsets。UTF8);ctx。writeAndFlush(buffer1);}OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)throwsException{cause。printStackTrace();ctx。close();}}分别先启动服务器端后,再启动客户端,服务器端的输出如下 客户端的输出如下17:03:24。474〔nioEventLoopGroup21〕INFOcom。ckjava。test。client。NettyClient连接服务器端:127。0。0。1:8080成功!17:03:24。535〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔c471a239abe5440193aab3d5e432c422021b6ae349394d59b451235af6c9e2190536b0aa3b534b03bb68b0637d619d0f〕17:03:24。537〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:117:03:24。537〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler从服务器端和客户端的输出结果来看:客户端只发送了1次数据,但是服务器端却收到了3次数据,说明tcp在客户端拆包后分3次发送了;并且客户端之后只收到了一次数据,说明服务器的回复数据在服务器端也出现了粘包现象,并且导致了数据无法区分的问题。通过Netty重现TCP粘包现象还用上面的例子,将客户端通道处理类:NettyClientHandler中的channelActive方法修改成如下的方式重写channelActive,当客户端启动的时候自动发送数据给服务端OverridepublicvoidchannelActive(ChannelHandlerContextctx)throwsException{使用客户端分10次发送,每次数据量少tcp会等客户端多次生产后,一次性进行发送for(inti0;i10;i){ByteBufbufferUnpooled。copiedBuffer(ckjavai,StandardCharsets。UTF8);ctx。writeAndFlush(buffer);}}分别先启动服务器端后,再启动客户端,服务器端的输出如下17:12:27。239〔nioEventLoopGroup21〕INFOcom。ckjava。test。server。NettyServer服务器端启动成功,开放端口:8080服务器收到的数据ckjava0ckjava1ckjava2ckjava3ckjava4ckjava5ckjava6ckjava7ckjava8ckjava9服务器收到的数据次数1客户端的输出如下17:12:36。917〔nioEventLoopGroup21〕INFOcom。ckjava。test。client。NettyClient连接服务器端:127。0。0。1:8080成功!17:12:36。961〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔31b25c25bd324ff1b3900c31b2558d12〕17:12:36。962〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:117:12:36。962〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler从服务器端和客户端的输出结果来看:客户端只发送了10次数据,但是服务器端却收到了1次数据,说明tcp在客户端粘包后一次性发送了全部的数据。Netty解决TCP粘包和拆包现象带来的问题TCP粘包和拆包现象带来的问题 从上面的案例可以发现当出现TCP粘包和拆包现象后会出现下面的问题:tcp在粘包的时候,数据混合后,接收方不能正确区分数据的头尾,如果是文件类型的数据,会导致文件破坏。tcp在拆包的时候,数据拆分后,接收方不能正确区分数据的头尾,导致收到的消息错乱,影响语义。如何解决TCP粘包和拆包现象带来的问题 由于TCP粘包和拆包现象会导致不能正确区分数据的头尾,那么解决的办法也挺简单的,通过特殊字符串来分隔消息体或者使用定长消息就能够正确区分数据的头尾。 目前的主流解决方式有以下几种:使用定长消息,Client和Server双方约定报文长度,Server端接受到报文后,按指定长度解析;使用特定分隔符,比如在消息尾部增加分隔符。Server端接收到报文后,按照特定的分割符分割消息后,再解析;将消息分割为消息头和消息体两部分,消息头中指定消息或者消息体的长度,通常设计中使用消息头第一个字段int32表示消息体的总长度; Netty中也提供了基于分隔符实现的半包解码器和定长的半包解码器:LineBasedFrameDecoder使用和r作为分割符的解码器DelimiterBasedFrameDecoder使用自定义的分割符的解码器FixedLengthFrameDecoder定长解码器通过Netty的DelimiterBasedFrameDecoder解码器来解决TCP粘包和拆包现象带来的问题 使用DelimiterBasedFrameDecoder可以确保收到的数据会自动通过自定义的分隔符进行分隔。发送的时候消息的后面只需要增加上自定义的分隔符即可。基于上面的例子,服务器端NettyServer改动如下publicvoidstart(){EventLoopGroupbossGroupnewNioEventLoopGroup(1);EventLoopGroupworkerGroupnewNioEventLoopGroup();try{ServerBootstrapsbsnewServerBootstrap()。group(bossGroup,workerGroup)。channel(NioServerSocketChannel。class)。localAddress(newInetSocketAddress(port))。childHandler(newChannelInitializerSocketChannel(){protectedvoidinitChannel(SocketChannelch){使用分隔符的半包解码器ByteBufbyteBufUnpooled。copiedBuffer(DELIMITER。getBytes());ch。pipeline()。addLast(newDelimiterBasedFrameDecoder(1024,byteBuf));ch。pipeline()。addLast(newNettyServerHandler());}})。option(ChannelOption。SOBACKLOG,128)。childOption(ChannelOption。SOKEEPALIVE,true);绑定端口,开始接收进来的连接sbs。bind(port)。addListener(future{log。info(String。format(服务器端启动成功,开放端口:s,port));});}catch(Exceptione){log。error(启动服务器端出现异常,e);}}服务器端NettyServerHandler改动如下OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,ByteBufmsg){byte〔〕buffernewbyte〔msg。readableBytes()〕;msg。readBytes(buffer);将buffer转为字符串StringmessagenewString(buffer,StandardCharsets。UTF8);System。out。println(服务器收到的数据message);System。out。println(服务器收到的数据次数(this。count));服务器回送数据给客户端回送一个随机idStringreplyDataUUID。randomUUID()。toString();ByteBufbuffer1Unpooled。copiedBuffer(replyData。concat(NettyServer。DELIMITER),StandardCharsets。UTF8);ctx。writeAndFlush(buffer1);System。out。println(服务器回复数据replyData);}客户端NettyClient改动如下publicstaticvoidmain(String〔〕args)throwsException{初始化客户端事件组EventLoopGroupgroupnewNioEventLoopGroup();try{BootstrapbnewBootstrap();b。group(group)初始化通道。channel(NioSocketChannel。class)。option(ChannelOption。TCPNODELAY,true)。handler(newChannelInitializerSocketChannel(){初始化通道处理器OverridepublicvoidinitChannel(SocketChannelch){ChannelPipelinepch。pipeline();使用分隔符的半包解码器ByteBufbyteBufUnpooled。copiedBuffer(DELIMITER。getBytes());ch。pipeline()。addLast(newDelimiterBasedFrameDecoder(1024,byteBuf));p。addLast(newNettyClientHandler());}});b。connect(HOST,PORT)。addListener(future{log。info(String。format(连接服务器端:s:s成功!,HOST,PORT));})。await();}catch(Exceptione){log。error(启动客户端出现异常,e);}}客户端NettyClientHandler中接收数据的部分不变,发送数据的地方改动如下重写channelActive,当客户端启动的时候自动发送数据给服务端OverridepublicvoidchannelActive(ChannelHandlerContextctx)throwsException{tcp粘包现象使用客户端分10次发送,每次数据量少tcp会等客户端多次生产后,一次性进行发送for(inti0;i10;i){Slog。info(String。format(客户端发送消息:〔s〕,data));ByteBufbufferUnpooled。copiedBuffer(data。concat(NettyClient。DELIMITER),StandardCharsets。UTF8);ctx。writeAndFlush(buffer);}}服务器端输出如下18:14:33。627〔nioEventLoopGroup21〕INFOcom。ckjava。test。server。NettyServer服务器端启动成功,开放端口:8080服务器收到的数据ckjava0服务器收到的数据次数1服务器回复数据c6129b89c8694e0697ca55518c55aff7服务器收到的数据ckjava1服务器收到的数据次数2服务器回复数据bc3426cb072f4cb99f69d2797863c9e4服务器收到的数据ckjava2服务器收到的数据次数3服务器回复数据437907021978462ba86515c0ff2803af服务器收到的数据ckjava3服务器收到的数据次数4服务器回复数据4eb3e4e60c6a4cefa639d6c40ebc27d2服务器收到的数据ckjava4服务器收到的数据次数5服务器回复数据6a9f02f99e0d4eaea380605c3ba410d2服务器收到的数据ckjava5服务器收到的数据次数6服务器回复数据7ab9e20ea86b4f6886735bc024643274服务器收到的数据ckjava6服务器收到的数据次数7服务器回复数据3b6b68cfc0664e328b5a961c995fdd6d服务器收到的数据ckjava7服务器收到的数据次数8服务器回复数据cf2a5c5196d943098f051c09abbe04f2服务器收到的数据ckjava8服务器收到的数据次数9服务器回复数据4d586684be554c108071a88dad5f0684服务器收到的数据ckjava9服务器收到的数据次数10服务器回复数据22fd511ee65a4f109426f14b4524d4d0客户端输出如下18:14:50。056〔nioEventLoopGroup21〕INFOcom。ckjava。test。client。NettyClient连接服务器端:127。0。0。1:8080成功!18:14:50。058〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava0〕18:14:50。075〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava1〕18:14:50。076〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava2〕18:14:50。076〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava3〕18:14:50。076〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava4〕18:14:50。076〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava5〕18:14:50。076〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava6〕18:14:50。077〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava7〕18:14:50。077〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava8〕18:14:50。077〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端发送消息:〔ckjava9〕18:14:50。104〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔c6129b89c8694e0697ca55518c55aff7〕18:14:50。105〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:118:14:50。105〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler18:14:50。105〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔bc3426cb072f4cb99f69d2797863c9e4〕18:14:50。105〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:218:14:50。105〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler18:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔437907021978462ba86515c0ff2803af〕18:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:318:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler18:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔4eb3e4e60c6a4cefa639d6c40ebc27d2〕18:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:418:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler18:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔6a9f02f99e0d4eaea380605c3ba410d2〕18:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:518:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler18:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔7ab9e20ea86b4f6886735bc024643274〕18:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:618:14:50。106〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler18:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔3b6b68cfc0664e328b5a961c995fdd6d〕18:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:718:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler18:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔cf2a5c5196d943098f051c09abbe04f2〕18:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:818:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler18:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔4d586684be554c108071a88dad5f0684〕18:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:918:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler18:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息:〔22fd511ee65a4f109426f14b4524d4d0〕18:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler客户端接收到消息的次数:1018:14:50。107〔nioEventLoopGroup21〕INFOc。c。test。client。NettyClientHandler从上面的例子可以看出DelimiterBasedFrameDecoder会帮自动帮我们把消息切割好,确保收到的数据都是基于自定义分隔符分隔好的数据,但是不要忘记在发送数据的时候添加上自定义分隔符。