通过下面的实例代码来演示在Netty中使用MessagPack时会出现的TCP粘包问题,为了学习的连贯性,参考了《Netty权威指南》第7章中的代码,但是需要注意的是,书中并没有提供完整代码,提供的代码都是片段性的,所以我根据自己的理解把服务端的代码和客户端的代码写了出来,可以作为参考。
仍然需要注意的是,我使用的是Netty 4.x的版本。
另外我在程序代码中写了非常详细的注释,所以这里不再进行更多的说明。
在使用MessagePack时的TCP粘包问题
编码器与解码器
MsgpackEncoder.java
packagecn.xpleaf.msgpack;importorg.msgpack.MessagePack;importio.netty.buffer.ByteBuf;importio.netty.channel.ChannelHandlerContext;importio.netty.handler.codec.MessageToByteEncoder;/**
* MsgpackEncoder继承自Netty中的MessageToByteEncoder类,
* 并重写抽象方法encode(ChannelHandlerContext ctx, Object msg, ByteBuf out)
* 它负责将Object类型的POJO对象编码为byte数组,然后写入到ByteBuf中
* @author yeyonghao
*
*/publicclassMsgpackEncoderextendsMessageToByteEncoder{@Overrideprotectedvoid encode(ChannelHandlerContextctx,Objectmsg,ByteBufout)throwsException{// 创建MessagePack对象MessagePackmsgpack =newMessagePack();// 将对象编码为MessagePack格式的字节数组byte[] raw = msgpack.write(msg);// 将字节数组写入到ByteBuf中out.writeBytes(raw); }}
MsgpackDecoder.java
package cn.xpleaf.msgpack;importjava.util.List;importorg.msgpack.MessagePack;importio.netty.buffer.ByteBuf;importio.netty.channel.ChannelHandlerContext;importio.netty.handler.codec.ByteToMessageDecoder;importio.netty.handler.codec.MessageToMessageDecoder;/*** MsgpackDecoder继承自Netty中的MessageToMessageDecoder类,* 并重写抽象方法decode(ChannelHandlerContext ctx, ByteBuf msg, Listout)* 首先从数据报msg(数据类型取决于继承MessageToMessageDecoder时填写的泛型类型)中获取需要解码的byte数组* 然后调用MessagePack的read方法将其反序列化(解码)为Object对象* 将解码后的对象加入到解码列表out中,这样就完成了MessagePack的解码操作* @author yeyonghao**/publicclassMsgpackDecoderextendsMessageToMessageDecoder{@Overrideprotectedvoiddecode(ChannelHandlerContext ctx, ByteBuf msg,List out) throws Exception {// 从数据报msg中(这里的数据类型为ByteBuf,因为Netty的通信基于ByteBuf对象)finalbyte[] array;finalintlength = msg.readableBytes(); array =newbyte[length];/**
* 这里使用的是ByteBuf的getBytes方法来将ByteBuf对象转换为字节数组,前面是使用readBytes,直接传入一个接收的字节数组参数即可
* 这里的参数比较多,第一个参数是index,关于readerIndex,说明如下:
* ByteBuf是通过readerIndex跟writerIndex两个位置指针来协助缓冲区的读写操作的,具体原理等到Netty源码分析时再详细学习一下
* 第二个参数是接收的字节数组
* 第三个参数是dstIndex the first index of the destination
* 第四个参数是length the number of bytes to transfer
*/msg.getBytes(msg.readerIndex(), array,0, length);// 创建一个MessagePack对象MessagePack msgpack =newMessagePack();// 解码并添加到解码列表out中out.add(msgpack.read(array)); }}
服务端
EchoServer.java
packagecn.xpleaf.echo;importcn.demo.simple.MsgPackDecode;importcn.xpleaf.msgpack.MsgpackDecoder;importcn.xpleaf.msgpack.MsgpackEncoder;importio.netty.bootstrap.ServerBootstrap;importio.netty.channel.ChannelFuture;importio.netty.channel.ChannelInitializer;importio.netty.channel.ChannelOption;importio.netty.channel.EventLoopGroup;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.SocketChannel;importio.netty.channel.socket.nio.NioServerSocketChannel;publicclassEchoServer{publicvoidbind(intport)throwsException{// 配置服务端的NIO线程组EventLoopGroup bossGroup =newNioEventLoopGroup(); EventLoopGroup workerGroup =newNioEventLoopGroup();try{ ServerBootstrap b =newServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG,1024) .childHandler(newChannelInitializer() {@OverrideprotectedvoidinitChannel(SocketChannel ch)throwsException{// 添加MesspagePack解码器ch.pipeline().addLast("msgpack decoder",newMsgPackDecode());// 添加MessagePack编码器ch.pipeline().addLast("msgpack encoder",newMsgpackEncoder());// 添加业务处理handlerch.pipeline().addLast(newEchoServerHandler()); } });// 绑定端口,同步等待成功ChannelFuture f = b.bind(port).sync();// 等待服务端监听端口关闭f.channel().closeFuture().sync(); }finally{// 优雅退出,释放线程池资源bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }publicstaticvoidmain(String[] args)throwsException{intport =8080;if(args !=null&& args.length >0) {try{ port = Integer.valueOf(port); }catch(NumberFormatException e) {//TODO:handle exception} }newEchoServer().bind(port); }}
EchoServerHandler.java
packagecn.xpleaf.echo;importio.netty.channel.ChannelHandlerContext;importio.netty.channel.ChannelInboundHandlerAdapter;publicclassEchoServerHandlerextendsChannelInboundHandlerAdapter{@OverridepublicvoidchannelRead(ChannelHandlerContext ctx, Object msg)throwsException{ System.out.println("Server receive the msgpack message : "+ msg); ctx.write(msg); }@OverridepublicvoidchannelReadComplete(ChannelHandlerContext ctx)throwsException{ ctx.flush(); }@OverridepublicvoidexceptionCaught(ChannelHandlerContext ctx, Throwable cause){// 发生异常,关闭链路ctx.close(); }}
客户端
EchoClient.java
packagecn.xpleaf.echo;importcn.demo.simple.MsgPackDecode;importcn.xpleaf.msgpack.MsgpackDecoder;importcn.xpleaf.msgpack.MsgpackEncoder;importio.netty.bootstrap.Bootstrap;importio.netty.channel.ChannelFuture;importio.netty.channel.ChannelInitializer;importio.netty.channel.ChannelOption;importio.netty.channel.EventLoopGroup;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.SocketChannel;importio.netty.channel.socket.nio.NioSocketChannel;publicclassEchoClient{publicvoidconnect(String host,intport,intsendNumber)throwsException{// 配置客户端NIO线程组EventLoopGroup group =newNioEventLoopGroup();try{ Bootstrap b =newBootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY,true)// 设置TCP连接超时时间.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,3000) .handler(newChannelInitializer() {@OverrideprotectedvoidinitChannel(SocketChannel ch)throwsException{// 添加MesspagePack解码器ch.pipeline().addLast("msgpack decoder",newMsgPackDecode());// 添加MessagePack编码器ch.pipeline().addLast("msgpack encoder",newMsgpackEncoder());// 添加业务处理handlerch.pipeline().addLast(newEchoClientHandler(sendNumber)); } });// 发起异步连接操作ChannelFuture f = b.connect(host, port).sync();// 等待客户端链路关闭f.channel().closeFuture().sync(); }finally{// 优雅退出,释放NIO线程组group.shutdownGracefully(); } }publicstaticvoidmain(String[] args)throwsException{intport =8080;if(args !=null&& args.length >0) {try{ port = Integer.valueOf(port); }catch(NumberFormatException e) {// 采用默认值} }intsendNumber =1000;newEchoClient().connect("localhost", port, sendNumber); }}
EchoClientHander.java
packagecn.xpleaf.echo;importcn.xpleaf.pojo.User;importio.netty.buffer.ByteBuf;importio.netty.buffer.Unpooled;importio.netty.channel.ChannelHandlerAdapter;importio.netty.channel.ChannelHandlerContext;importio.netty.channel.ChannelInboundHandlerAdapter;publicclassEchoClientHandlerextendsChannelInboundHandlerAdapter{// sendNumber为写入发送缓冲区的对象数量privateintsendNumber;publicEchoClientHandler(intsendNumber){this.sendNumber = sendNumber; }/** * 构建长度为userNum的User对象数组 *@paramuserNum *@return*/privateUser[] getUserArray(intuserNum) { User[] users =newUser[userNum]; User user =null;for(inti =0; i < userNum; i++) { user =newUser(); user.setName("ABCDEFG --->"+ i); user.setAge(i); users[i] = user; }returnusers; }@OverridepublicvoidchannelActive(ChannelHandlerContext ctx){ User[] users = getUserArray(sendNumber);for(User user : users) { ctx.writeAndFlush(user); } }@OverridepublicvoidchannelRead(ChannelHandlerContext ctx, Object msg)throwsException{ System.out.println("Client receive the msgpack message : "+ msg); }@OverridepublicvoidchannelReadComplete(ChannelHandlerContext ctx)throwsException{ ctx.flush(); }@OverridepublicvoidexceptionCaught(ChannelHandlerContext ctx, Throwable cause)throwsException{ ctx.close(); }}
POJO
User.java
packagecn.xpleaf.pojo;importorg.msgpack.annotation.Message;@MessagepublicclassUser{privateString name;privateintage;publicStringgetName(){returnname; }publicvoidsetName(String name){this.name = name; }publicintgetAge(){returnage; }publicvoidsetAge(intage){this.age = age; }@OverridepublicStringtoString(){return"User [name="+ name +", age="+ age +"]"; }}
测试
当EchoClient.java中的sendNumber为1时,服务端和客户端都是正常工作的,此时,服务端和客户端的输出分别如下:
服务端:
Server receive the msgpack message : ["ABCDEFG--->0",0]
客户端:
Client receive the msgpack message : ["ABCDEFG--->0",0]
但是当sendNumber数字很大时,就不能正常工作了,比如可以设置为1000,此时输出结果如下:
服务端:
Server receive the msgpack message : ["ABCDEFG --->0",0]Server receive the msgpack message : ["ABCDEFG --->1",1]Server receive the msgpack message : ["ABCDEFG --->3",3]...省略输出...Server receive the msgpack message : ["ABCDEFG --->146",146]Server receive the msgpack message : 70Server receive the msgpack message : ["ABCDEFG --->156",156]Server receive the msgpack message : ["ABCDEFG --->157",157]...省略输出...
客户端:
Client receive the msgpack message : ["ABCDEFG --->0",0]Client receive the msgpack message : 62Client receive the msgpack message : 68
显然运行结果跟预期的不太一样,这是因为出现了TCP粘包问题。
粘包问题解决方案
在前面代码的基础上,只需要对EchoServer.java和EchoClient.java中的代码进行修改即可。
EchoServer.java
packagecn.xpleaf.echo02;importcn.demo.simple.MsgPackDecode;importcn.xpleaf.msgpack.MsgpackDecoder;importcn.xpleaf.msgpack.MsgpackEncoder;importio.netty.bootstrap.ServerBootstrap;importio.netty.channel.ChannelFuture;importio.netty.channel.ChannelInitializer;importio.netty.channel.ChannelOption;importio.netty.channel.EventLoopGroup;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.SocketChannel;importio.netty.channel.socket.nio.NioServerSocketChannel;importio.netty.handler.codec.LengthFieldBasedFrameDecoder;importio.netty.handler.codec.LengthFieldPrepender;publicclassEchoServer{publicvoidbind(intport)throwsException{// 配置服务端的NIO线程组EventLoopGroup bossGroup =newNioEventLoopGroup(); EventLoopGroup workerGroup =newNioEventLoopGroup();try{ ServerBootstrap b =newServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG,1024) .childHandler(newChannelInitializer() {@OverrideprotectedvoidinitChannel(SocketChannel ch)throwsException{// 添加长度字段解码器// 在MessagePack解码器之前增加LengthFieldBasedFrameDecoder,用于处理半包消息// 它会解析消息头部的长度字段信息,这样后面的MsgpackDecoder接收到的永远是整包消息ch.pipeline().addLast("frameDecoder",newLengthFieldBasedFrameDecoder(65535,0,2,0,2));// 添加MesspagePack解码器ch.pipeline().addLast("msgpack decoder",newMsgPackDecode());// 添加长度字段编码器// 在MessagePack编码器之前增加LengthFieldPrepender,它将在ByteBuf之前增加2个字节的消息长度字段ch.pipeline().addLast("frameEncoder",newLengthFieldPrepender(2));// 添加MessagePack编码器ch.pipeline().addLast("msgpack encoder",newMsgpackEncoder());// 添加业务处理handlerch.pipeline().addLast(newEchoServerHandler()); } });// 绑定端口,同步等待成功ChannelFuture f = b.bind(port).sync();// 等待服务端监听端口关闭f.channel().closeFuture().sync(); }finally{// 优雅退出,释放线程池资源bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }publicstaticvoidmain(String[] args)throwsException{intport =8080;if(args !=null&& args.length >0) {try{ port = Integer.valueOf(port); }catch(NumberFormatException e) {//TODO:handle exception} }newEchoServer().bind(port); }}
EchoClient.java
packagecn.xpleaf.echo02;importcn.demo.simple.MsgPackDecode;importcn.xpleaf.msgpack.MsgpackDecoder;importcn.xpleaf.msgpack.MsgpackEncoder;importio.netty.bootstrap.Bootstrap;importio.netty.channel.ChannelFuture;importio.netty.channel.ChannelInitializer;importio.netty.channel.ChannelOption;importio.netty.channel.EventLoopGroup;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.SocketChannel;importio.netty.channel.socket.nio.NioSocketChannel;importio.netty.handler.codec.LengthFieldBasedFrameDecoder;importio.netty.handler.codec.LengthFieldPrepender;publicclassEchoClient{publicvoidconnect(String host,intport,intsendNumber)throwsException{// 配置客户端NIO线程组EventLoopGroup group =newNioEventLoopGroup();try{ Bootstrap b =newBootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY,true)// 设置TCP连接超时时间.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,3000) .handler(newChannelInitializer() {@OverrideprotectedvoidinitChannel(SocketChannel ch)throwsException{// 添加长度字段解码器// 在MessagePack解码器之前增加LengthFieldBasedFrameDecoder,用于处理半包消息// 它会解析消息头部的长度字段信息,这样后面的MsgpackDecoder接收到的永远是整包消息ch.pipeline().addLast("frameDecoder",newLengthFieldBasedFrameDecoder(65535,0,2,0,2));// 添加MesspagePack解码器ch.pipeline().addLast("msgpack decoder",newMsgPackDecode());// 添加长度字段编码器// 在MessagePack编码器之前增加LengthFieldPrepender,它将在ByteBuf之前增加2个字节的消息长度字段ch.pipeline().addLast("frameEncoder",newLengthFieldPrepender(2));// 添加MessagePack编码器ch.pipeline().addLast("msgpack encoder",newMsgpackEncoder());// 添加业务处理handlerch.pipeline().addLast(newEchoClientHandler(sendNumber)); } });// 发起异步连接操作ChannelFuture f = b.connect(host, port).sync();// 等待客户端链路关闭f.channel().closeFuture().sync(); }finally{// 优雅退出,释放NIO线程组group.shutdownGracefully(); } }publicstaticvoidmain(String[] args)throwsException{intport =8080;if(args !=null&& args.length >0) {try{ port = Integer.valueOf(port); }catch(NumberFormatException e) {// 采用默认值} }intsendNumber =1000;newEchoClient().connect("localhost", port, sendNumber); }}
测试
可以将EchoClient.java中sendNumber设置为1000或更大,此时服务端和客户端的输出结果跟预期的都是一样的。
测试结果为,服务端和客户端都会打印1000行的信息(假设sendNumber为1000),这里不再给出运行结果。
如果你也想在IT行业拿高薪,可以参加我们的训练营课程,选择最适合自己的课程学习,技术大牛亲授,7个月后,进入名企拿高薪。我们的课程内容有:Java工程化、高性能及分布式、高性能、深入浅出。高架构。性能调优、Spring,MyBatis,Netty源码分析和大数据等多个知识点。如果你想拿高薪的,想学习的,想就业前景好的,想跟别人竞争能取得优势的,想进阿里面试但担心面试不过的,你都可以来,群号为: 171662117
注:加群要求
1、具有1-5工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加。
2、在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加。
3、如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的,可以加。
4、觉得自己很牛B,一般需求都能搞定。但是所学的知识点没有系统化,很难在技术领域继续突破的可以加。
5.阿里Java高级大牛直播讲解知识点,分享知识,多年工作经验的梳理和总结,带着大家全面、科学地建立自己的技术体系和技术认知!
6.小号或者小白之类加群一律不给过,谢谢。
目标已经有了,下面就看行动了!记住:学习永远是自己的事情,你不学时间也不会多,你学了有时候却能够使用自己学到的知识换得更多自由自在的美好时光!时间是生命的基本组成部分,也是万物存在的根本尺度,我们的时间在那里我们的生活就在那里!我们价值也将在那里提升或消弭!Java程序员,加油吧