Netty粘包拆包問題解決方案
TCP黏包拆包
TCP是一個流協議,就是沒有界限的一長串二進位制資料。TCP作為傳輸層協議並不不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行資料包的劃分,所以在業務上認為是一個完整的包,可能會被TCP拆分成多個包進行傳送,也有可能把多個小的包封裝成一個大的資料包傳送,這就是所謂的TCP粘包和拆包問題。
怎麼解決?
- • 訊息定長度,傳輸的資料大小固定長度,例如每段的長度固定為100位元組,如果不夠空位補空格
- • 在資料包尾部新增特殊分隔符,比如下劃線,中劃線等
- • 將訊息分為訊息頭和訊息體,訊息頭中包含表示資訊的總長度
Netty提供了多個解碼器,可以進行分包的操作,分別是:
- • LineBasedFrameDecoder (回車換行分包)
- • DelimiterBasedFrameDecoder(特殊分隔符分包)
- • FixedLengthFrameDecoder(固定長度報文來分包)
- • LengthFieldBasedFrameDecoder(自定義長度來分包)
製造粘包和拆包問題
為了驗證我們的解碼器能夠解決這種粘包和拆包帶來的問題,首先我們就製造一個這樣的問題,以此用來做對比。
服務端:
public static void main(String[] args) { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup,workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast("decoder",new StringDecoder()); ch.pipeline().addLast("encoder",new StringEncoder()); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx,Object msg) { System.err.println("server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好" ); } }); } }) .option(ChannelOption.SO_BACKLOG,128) .childOption(ChannelOption.SO_KEEPALIVE,true); try { ChannelFuture f = bootstrap.bind(2222).sync(); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } }
客戶端我們傳送一個比較長的字串,如果服務端收到的訊息是一條,那麼就是對的,如果是多條,那麼就有問題了。
public static void main(String[] args) { EventLoopGroup workerGroup = new NioEventLoopGroup(); Channel channel = null; try { Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioSocketChannel.class); b.option(ChannelOption.SO_KEEPALIVE,true); b.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast("decoder",new StringDecoder()); ch.pipeline().addLast("encoder",new StringEncoder()); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx,Object msg) { System.err.println("client:" + msg.toString()); } }); } }); ChannelFuture f = b.connect("127.0.0.1",2222).sync(); channel = f.channel(); StringBuilder msg = new StringBuilder(); for (int i = 0; i < 100; i++) { msg.append("hello yinjihuan"); } channel.writeAndFlush(msg); } catch(Exception e) { e.printStackTrace(); } }
首先啟動服務端,然後再啟動客戶端,通過控制檯可以看到服務接收的資料分成了2次,這就是我們要解決的問題。
server:hello yinjihuanhello....
server:o yinjihuanhello...
LineBasedFrameDecoder
用LineBasedFrameDecoder 來解決需要在傳送的資料結尾加上回車換行符,這樣LineBasedFrameDecoder 才知道這段資料有沒有讀取完整。
改造服務端程式碼,只需加上LineBasedFrameDecoder 解碼器即可,建構函式的引數是資料包的最大長度。
public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LineBasedFrameDecoder(10240)); ch.pipeline().addLast("decoder",new StringDecoder()); ch.pipeline().addLast("encoder",new StringEncoder()); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx,Object msg) { System.err.println("server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好"); } }); }
改造客戶端傳送程式碼,再資料後面加上回車換行符
ChannelFuture f = b.connect("127.0.0.1",2222).sync(); channel = f.channel(); StringBuilder msg = new StringBuilder(); for (int i = 0; i < 100; i++) { msg.append("hello yinjihuan"); } channel.writeAndFlush(msg + System.getProperty("line.separator"));
DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder和LineBasedFrameDecoder差不多,DelimiterBasedFrameDecoder可以自己定義需要分割的符號,比如下劃線,中劃線等等。
改造服務端程式碼,只需加上DelimiterBasedFrameDecoder解碼器即可,建構函式的引數是資料包的最大長度。我們用下劃線來分割。
public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new DelimiterBasedFrameDecoder(10240,Unpooled.copiedBuffer("_".getBytes()))); ch.pipeline().addLast("decoder",Object msg) { System.err.println("server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好"); } }); }
改造客戶端傳送程式碼,再資料後面加上下劃線
ChannelFuture f = b.connect("127.0.0.1",2222).sync(); channel = f.channel(); StringBuilder msg = new StringBuilder(); for (int i = 0; i < 100; i++) { msg.append("hello yinjihuan"); } channel.writeAndFlush(msg + "_");
FixedLengthFrameDecoder
FixedLengthFrameDecoder是按固定的資料長度來進行解碼的,也就是說你客戶端傳送的每條訊息的長度是固定的,下面我們看看怎麼使用。
服務端還是一樣,增加FixedLengthFrameDecoder解碼器即可。
public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new FixedLengthFrameDecoder(1500)); ch.pipeline().addLast("decoder",Object msg) { System.err.println("server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好"); } }); }
客戶端,msg輸出的長度就是1500
ChannelFuture f = b.connect("127.0.0.1",2222).sync(); channel = f.channel(); StringBuilder msg = new StringBuilder(); for (int i = 0; i < 100; i++) { msg.append("hello yinjihuan"); } System.out.println(msg.length()); channel.writeAndFlush(msg);
服務端程式碼:
public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast("frameDecoder",new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,4,4)); ch.pipeline().addLast("frameEncoder",new LengthFieldPrepender(4)); ch.pipeline().addLast("decoder",Object msg) { System.err.println("server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好"); } }); }
客戶端,直接傳送就行
ChannelFuture f = b.connect("127.0.0.1",2222).sync(); channel = f.channel();![](https://s4.51cto.com/images/blog/202008/04/fb05cdb6bd8458bd1006a127ff9d12dc.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) StringBuilder msg = new StringBuilder(); for (int i = 0; i < 100; i++) { msg.append("hello yinjihuan"); } channel.writeAndFlush(msg);
原始碼參考:https://github.com/yinjihuan/netty-im
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。