Tcp 粘包以及解决方法
1. 简介
1. TCP 是面向连接的,面向流的,提供可靠性服务,收发两端(客户端和服务器端) 都要有一一成对的Socket, 因此,发送端为了将多个发送给接收端的包更有效的发给对方,使用了优化算法(Nagle 算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包,这样虽然提高了效率,但是接收端就难于分辨出完整的数据包了。 因为面向流的通信是无消息保护边界的。
2. 由于TCP 无消息保护边界,需要在接收端处理消息边界问题, 也就是我们所说的粘包拆包问题。
2. 粘包问题演示
TcpServer:
package netty.tcp; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; public class TcpServer { private static final Integer PORT = 6666; public static void main(String[] args) throws InterruptedException { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new MyNettyServerInitializer()); ChannelFuture sync = serverBootstrap.bind(PORT).sync(); sync.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { System.out.println("服务端启动成功,监听地址: " + PORT); } } }); } }
MyNettyServerInitializer
package netty.tcp; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; public class MyNettyServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { // 向管道加入处理器 ChannelPipeline pipeline = ch.pipeline(); // 1. 增加一个自定义的handler pipeline.addLast(new MyServerHandler()); System.out.println("server is ok~~~~"); } }
MyServerHandler
package netty.tcp; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> { private int count; @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { int i = msg.readableBytes(); System.out.println("读取到的字节数: " + i); // 回显一个消息给客户端 String msgStr = msg.toString(CharsetUtil.UTF_8); String printMsg = "count: " + (++count) + "; msgStr:" + msgStr; System.out.println(printMsg); ctx.channel().writeAndFlush(Unpooled.copiedBuffer(printMsg, CharsetUtil.UTF_8)); } }
TcpClient
package netty.tcp; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; public class TcpClient { private static final Integer PORT = 6666; public static void main(String[] args) throws InterruptedException { // 创建一个事件循环组 EventLoopGroup eventExecutors = new NioEventLoopGroup(); try { // 创建一个启动Bootstrap(注意是Netty包下的) Bootstrap bootstrap = new Bootstrap(); // 链式设置参数 bootstrap.group(eventExecutors) // 设置线程组 .channel(NioSocketChannel.class) // 设置通道class .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); // 1. 加入一个自定义的处理器 pipeline.addLast(new MyClientHandler()); } }); System.out.println("客户端is ok..."); // 启动客户端连接服务器(ChannelFuture 是netty的异步模型) ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", PORT).sync(); // 监听关闭通道 channelFuture.channel().closeFuture().sync(); } finally { // 关闭 eventExecutors.shutdownGracefully(); } } }
MyClientHandler
package netty.tcp; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; /** * 自定义服务器端处理handler,需要继承netty定义的 ChannelInboundHandlerAdapter 类 */ public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> { /** * 通道就绪事件 * * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 循环发送十条消息 int count = 10; String msg = "client msg "; for (int i = 0; i < count; i++) { ctx.writeAndFlush(Unpooled.copiedBuffer(msg + i, CharsetUtil.UTF_8)); } } /** * 发生异常事件 * * @param ctx * @param cause * @throws Exception */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { System.out.println("从客户端 " + ctx.channel().remoteAddress() + " 读取到的消息, long: " + msg); // 回显一个消息给客户端 String msgStr = msg.toString(CharsetUtil.UTF_8); System.out.println(msgStr); } }
启动服务器端,然后启动两个客户端,查看日志:
(1) 服务器端:
服务端启动成功,监听地址: 6666 server is ok~~~~ 读取到的字节数: 120 count: 1; msgStr:client msg 0client msg 1client msg 2client msg 3client msg 4client msg 5client msg 6client msg 7client msg 8client msg 9 server is ok~~~~ 读取到的字节数: 12 count: 1; msgStr:client msg 0 读取到的字节数: 96 count: 2; msgStr:client msg 1client msg 2client msg 3client msg 4client msg 5client msg 6client msg 7client msg 8 读取到的字节数: 12 count: 3; msgStr:client msg 9
(2) 客户端1
客户端is ok... 从客户端 /127.0.0.1:6666 读取到的消息, long: PooledUnsafeDirectByteBuf(ridx: 0, widx: 137, cap: 1024) count: 1; msgStr:client msg 0client msg 1client msg 2client msg 3client msg 4client msg 5client msg 6client msg 7client msg 8client msg 9
(3) 客户端2
客户端is ok... 从客户端 /127.0.0.1:6666 读取到的消息, long: PooledUnsafeDirectByteBuf(ridx: 0, widx: 171, cap: 1024) count: 1; msgStr:client msg 0count: 2; msgStr:client msg 1client msg 2client msg 3client msg 4client msg 5client msg 6client msg 7client msg 8count: 3; msgStr:client msg 9
可以看到发生了消息错乱。
3. 解决办法
1. 加入一个消息包装类 TransferMsg
package netty.tcp; import lombok.Data; @Data public class TransferMsg { private Integer length; private byte[] msg; }
2. TransferMsgEncoder 编码器
package netty.tcp; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; public class TransferMsgEncoder extends MessageToByteEncoder<TransferMsg> { @Override protected void encode(ChannelHandlerContext ctx, TransferMsg msg, ByteBuf out) throws Exception { System.out.println("netty.tcp.TransferMsgEncoder.encode 被调用"); out.writeInt(msg.getLength()); out.writeBytes(msg.getMsg()); } }
3. TransferMsgDecoder 解码器
package netty.tcp; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ReplayingDecoder; import java.util.List; public class TransferMsgDecoder extends ReplayingDecoder<Void> { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { System.out.println("netty.tcp.TransferMsgDecoder.decode 被调用"); int count = in.readInt(); byte[] bytes = new byte[count]; in.readBytes(bytes); TransferMsg transferMsg = new TransferMsg(); transferMsg.setMsg(bytes); transferMsg.setLength(count); out.add(transferMsg); } }
4. 服务器端修改
修改MyNettyServerInitializer 加入自己的解码器
package netty.tcp; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; public class MyNettyServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { // 向管道加入处理器 ChannelPipeline pipeline = ch.pipeline(); // 1. 增加一个自定义的handler pipeline.addLast(new TransferMsgDecoder()); pipeline.addLast(new MyServerHandler()); System.out.println("server is ok~~~~"); } }
修改MyServerHandler
package netty.tcp; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; public class MyServerHandler extends SimpleChannelInboundHandler<TransferMsg> { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } @Override protected void channelRead0(ChannelHandlerContext ctx, TransferMsg msg) throws Exception { Integer length = msg.getLength(); byte[] msg1 = msg.getMsg(); String s = new String(msg1); System.out.println("读到消息,length: " + length + "\tmsg:" + s); } }
2. 客户端修改
TcpClient 加入自己的编码器
package netty.tcp; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; public class TcpClient { private static final Integer PORT = 6666; public static void main(String[] args) throws InterruptedException { // 创建一个事件循环组 EventLoopGroup eventExecutors = new NioEventLoopGroup(); try { // 创建一个启动Bootstrap(注意是Netty包下的) Bootstrap bootstrap = new Bootstrap(); // 链式设置参数 bootstrap.group(eventExecutors) // 设置线程组 .channel(NioSocketChannel.class) // 设置通道class .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); // 1. 加入一个自定义的处理器 pipeline.addLast(new TransferMsgEncoder()); pipeline.addLast(new MyClientHandler()); } }); System.out.println("客户端is ok..."); // 启动客户端连接服务器(ChannelFuture 是netty的异步模型) ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", PORT).sync(); // 监听关闭通道 channelFuture.channel().closeFuture().sync(); } finally { // 关闭 eventExecutors.shutdownGracefully(); } } }
MyClientHandler 修改发送的消息格式
package netty.tcp; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; /** * 自定义服务器端处理handler,需要继承netty定义的 ChannelInboundHandlerAdapter 类 */ public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> { /** * 通道就绪事件 * * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 循环发送十条消息 int count = 10; String msg = "client msg "; String sendMsg = null; TransferMsg transferMsg = null; for (int i = 0; i < count; i++) { transferMsg = new TransferMsg(); sendMsg = msg + i; transferMsg.setMsg(sendMsg.getBytes()); transferMsg.setLength(sendMsg.getBytes().length); ctx.writeAndFlush(transferMsg); } } /** * 发生异常事件 * * @param ctx * @param cause * @throws Exception */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { System.out.println("从客户端 " + ctx.channel().remoteAddress() + " 读取到的消息, long: " + msg); // 回显一个消息给客户端 String msgStr = msg.toString(CharsetUtil.UTF_8); System.out.println(msgStr); } }
测试: 启动一个服务器端,然后启动一个客户端,查看服务器日志如下:
服务端启动成功,监听地址: 6666 server is ok~~~~ netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 0 netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 1 netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 2 netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 3 netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 4 netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 5 netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 6 netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 7 netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 8 netty.tcp.TransferMsgDecoder.decode 被调用 读到消息,length: 12 msg:client msg 9
如果服务器向客户端返回相同的消息,在服务器端也需要加入自己的编码器;客户端加入自己的解码器。实测解决了粘包问题。
总结: Netty 解决方法:
1》 使用自定义协议 + 编解码器来解决
2》 关键就是解决服务器每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或者少读的问题,从而避免TCP的粘包、拆包
还有其他的解决办法又自定义消息的结束符,比如我们约定以"XXXXXX" 结尾,则收到消息可以判断是否以这个结尾。
参考: https://juejin.cn/post/6975109908106575903
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2018-04-07 【JDK】JDK7与JDK8环境共存与切换:先安装jdk7,配置好环境变量后再安装jdk8