Netty中使用MessagePack時的TCP粘包問題與解決方案
[toc]
Netty中使用MessagePack時的TCP粘包問題與解決方案
透過下面的例項程式碼來演示在Netty中使用MessagPack時會出現的TCP粘包問題,為了學習的連貫性,參考了《Netty權威指南》第7章中的程式碼,但是需要注意的是,書中並沒有提供完整程式碼,提供的程式碼都是片段性的,所以我根據自己的理解把服務端的程式碼和客戶端的程式碼寫了出來,可以作為參考。
仍然需要注意的是,我使用的是Netty 4.x的版本。
另外我在程式程式碼中寫了非常詳細的註釋,所以這裡不再進行更多的說明。
在使用MessagePack時的TCP粘包問題
編碼器與解碼器
MsgpackEncoder.java
package cn.xpleaf.msgpack;import org.msgpack.MessagePack;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.MessageToByteEncoder;/** * MsgpackEncoder繼承自Netty中的MessageToByteEncoder類, * 並重寫抽象方法encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) * 它負責將Object型別的POJO物件編碼為byte陣列,然後寫入到ByteBuf中 * @author yeyonghao * */public class MsgpackEncoder extends MessageToByteEncoder
MsgpackDecoder.java
package cn.xpleaf.msgpack;import java.util.List;import org.msgpack.MessagePack;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ByteToMessageDecoder;import io.netty.handler.codec.MessageToMessageDecoder;/** * MsgpackDecoder繼承自Netty中的MessageToMessageDecoder類, * 並重寫抽象方法decode(ChannelHandlerContext ctx, ByteBuf msg, List
服務端
EchoServer.java
package cn.xpleaf.echo;import cn.demo.simple.MsgPackDecode;import cn.xpleaf.msgpack.MsgpackDecoder;import cn.xpleaf.msgpack.MsgpackEncoder;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;public class EchoServer { public void bind(int port) throws Exception { // 配置服務端的NIO執行緒組 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { // 新增MesspagePack解碼器 ch.pipeline().addLast("msgpack decoder", new MsgPackDecode()); // 新增MessagePack編碼器 ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder()); // 新增業務處理handler ch.pipeline().addLast(new EchoServerHandler()); } }); // 繫結埠,同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服務端監聽埠關閉 f.channel().closeFuture().sync(); } finally { // 優雅退出,釋放執行緒池資源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if(args != null && args.length > 0) { try { port = Integer.valueOf(port); } catch (NumberFormatException e) { // TODO: handle exception } } new EchoServer().bind(port); }}
EchoServerHandler.java
package cn.xpleaf.echo;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;public class EchoServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("Server receive the msgpack message : " + msg); ctx.write(msg); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // 發生異常,關閉鏈路 ctx.close(); }}
客戶端
EchoClient.java
package cn.xpleaf.echo;import cn.demo.simple.MsgPackDecode;import cn.xpleaf.msgpack.MsgpackDecoder;import cn.xpleaf.msgpack.MsgpackEncoder;import io.netty.bootstrap.Bootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;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 EchoClient { public void connect(String host, int port, int sendNumber) throws Exception { // 配置客戶端NIO執行緒組 EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) // 設定TCP連線超時時間 .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) .handler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { // 新增MesspagePack解碼器 ch.pipeline().addLast("msgpack decoder", new MsgPackDecode()); // 新增MessagePack編碼器 ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder()); // 新增業務處理handler ch.pipeline().addLast(new EchoClientHandler(sendNumber)); } }); // 發起非同步連線操作 ChannelFuture f = b.connect(host, port).sync(); // 等待客戶端鏈路關閉 f.channel().closeFuture().sync(); } finally { // 優雅退出,釋放NIO執行緒組 group.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if(args != null && args.length > 0) { try { port = Integer.valueOf(port); } catch (NumberFormatException e) { // 採用預設值 } } int sendNumber = 1000; new EchoClient().connect("localhost", port, sendNumber); }}
EchoClientHander.java
package cn.xpleaf.echo;import cn.xpleaf.pojo.User;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerAdapter;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;public class EchoClientHandler extends ChannelInboundHandlerAdapter { // sendNumber為寫入傳送緩衝區的物件數量 private int sendNumber; public EchoClientHandler(int sendNumber) { this.sendNumber = sendNumber; } /** * 構建長度為userNum的User物件陣列 * @param userNum * @return */ private User[] getUserArray(int userNum) { User[] users = new User[userNum]; User user = null; for(int i = 0; i " + i); user.setAge(i); users[i] = user; } return users; } @Override public void channelActive(ChannelHandlerContext ctx) { User[] users = getUserArray(sendNumber); for (User user : users) { ctx.writeAndFlush(user); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("Client receive the msgpack message : " + msg); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); }}
POJO
User.java
package cn.xpleaf.pojo;import org.msgpack.annotation.Message;@Messagepublic class User { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { 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
package cn.xpleaf.echo02;import cn.demo.simple.MsgPackDecode;import cn.xpleaf.msgpack.MsgpackDecoder;import cn.xpleaf.msgpack.MsgpackEncoder;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.codec.LengthFieldBasedFrameDecoder;import io.netty.handler.codec.LengthFieldPrepender;public class EchoServer { public void bind(int port) throws Exception { // 配置服務端的NIO執行緒組 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { // 新增長度欄位解碼器 // 在MessagePack解碼器之前增加LengthFieldBasedFrameDecoder,用於處理半包訊息 // 它會解析訊息頭部的長度欄位資訊,這樣後面的MsgpackDecoder接收到的永遠是整包訊息 ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2)); // 新增MesspagePack解碼器 ch.pipeline().addLast("msgpack decoder", new MsgPackDecode()); // 新增長度欄位編碼器 // 在MessagePack編碼器之前增加LengthFieldPrepender,它將在ByteBuf之前增加2個位元組的訊息長度欄位 ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2)); // 新增MessagePack編碼器 ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder()); // 新增業務處理handler ch.pipeline().addLast(new EchoServerHandler()); } }); // 繫結埠,同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服務端監聽埠關閉 f.channel().closeFuture().sync(); } finally { // 優雅退出,釋放執行緒池資源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if(args != null && args.length > 0) { try { port = Integer.valueOf(port); } catch (NumberFormatException e) { // TODO: handle exception } } new EchoServer().bind(port); }}
EchoClient.java
package cn.xpleaf.echo02;import cn.demo.simple.MsgPackDecode;import cn.xpleaf.msgpack.MsgpackDecoder;import cn.xpleaf.msgpack.MsgpackEncoder;import io.netty.bootstrap.Bootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import io.netty.handler.codec.LengthFieldBasedFrameDecoder;import io.netty.handler.codec.LengthFieldPrepender;public class EchoClient { public void connect(String host, int port, int sendNumber) throws Exception { // 配置客戶端NIO執行緒組 EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) // 設定TCP連線超時時間 .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) .handler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { // 新增長度欄位解碼器 // 在MessagePack解碼器之前增加LengthFieldBasedFrameDecoder,用於處理半包訊息 // 它會解析訊息頭部的長度欄位資訊,這樣後面的MsgpackDecoder接收到的永遠是整包訊息 ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2)); // 新增MesspagePack解碼器 ch.pipeline().addLast("msgpack decoder", new MsgPackDecode()); // 新增長度欄位編碼器 // 在MessagePack編碼器之前增加LengthFieldPrepender,它將在ByteBuf之前增加2個位元組的訊息長度欄位 ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2)); // 新增MessagePack編碼器 ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder()); // 新增業務處理handler ch.pipeline().addLast(new EchoClientHandler(sendNumber)); } }); // 發起非同步連線操作 ChannelFuture f = b.connect(host, port).sync(); // 等待客戶端鏈路關閉 f.channel().closeFuture().sync(); } finally { // 優雅退出,釋放NIO執行緒組 group.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if(args != null && args.length > 0) { try { port = Integer.valueOf(port); } catch (NumberFormatException e) { // 採用預設值 } } int sendNumber = 1000; new EchoClient().connect("localhost", port, sendNumber); }}
測試
可以將EchoClient.java
中sendNumber
設定為1000或更大,此時服務端和客戶端的輸出結果跟預期的都是一樣的。
測試結果為,服務端和客戶端都會列印1000行的資訊(假設sendNumber為1000),這裡不再給出執行結果。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2730/viewspace-2813877/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- TCP 粘包 - 拆包問題及解決方案TCP
- Netty2:粘包/拆包問題與使用LineBasedFrameDecoder的解決方案Netty
- Netty解決粘包和拆包問題的四種方案Netty
- Netty解決半包(TCP粘包/拆包導致)讀寫問題NettyTCP
- Netty粘包&半包解決方案Netty
- netty 解決粘包 和 分包的問題Netty
- Netty入門系列(2) --使用Netty解決粘包和拆包問題Netty
- 深入學習Netty(5)——Netty是如何解決TCP粘包/拆包問題的?NettyTCP
- TCP粘包拆包問題TCP
- Go TCP 粘包問題GoTCP
- TCP協議粘包問題詳解TCP協議
- Netty拾遺(七)——粘包與拆包問題Netty
- 結合RPC框架通訊談 netty如何解決TCP粘包問題RPC框架NettyTCP
- 計算機網路 - TCP粘包、拆包以及解決方案計算機網路TCP
- Netty - 粘包與拆包Netty
- Netty原始碼學習6——netty編碼解碼器&粘包半包問題的解決Netty原始碼
- 粘包拆包及解決方案
- Netty(三) 什麼是 TCP 拆、粘包?如何解決?NettyTCP
- java nio解決半包 粘包問題Java
- Netty 中的粘包和拆包Netty
- Netty--粘包與分包Netty
- Netty如何解決粘包拆包?(二)Netty
- TCP 粘包拆包TCP
- 粘包問題
- 粘包問題原因和解決方法
- 25. Socket與粘包問題
- socket的半包,粘包與分包的問題
- tcp中的粘包、半包的處理方法TCP
- java nio訊息半包、粘包解決方案Java
- TCP的粘包拆包技術TCP
- TCP通訊處理粘包詳解TCP
- 從零開始netty學習筆記之TCP粘包和拆包Netty筆記TCP
- Java 8 的日期與時間問題解決方案Java
- Socket 粘包和分包問題
- 詳說tcp粘包和半包TCP
- Netty Protobuf處理粘包分析Netty
- Lumen 中使用 jwt 時多 guard 的問題解決方案JWT
- Swoole - TCP流資料邊界問題解決方案TCP