【Netty4.x】Netty TCP粘包/拆包問題的解決辦法(二)
阿新 • • 發佈:2019-01-26
一、什麼是TCP粘包/拆包
如圖所示,假如客戶端分別傳送兩個資料包D1和D2給服務端,由於服務端一次讀取到的位元組數是不確定的,故可能存在以下4中情況:
- 第一種情況:Server端分別讀取到D1和D2,沒有產生粘包和拆包的情況。
- 第二種情況:Server端一次接收到兩個資料包,D1和D2粘合在一起,被稱為TCP粘包。
- 第三種情況:Server端分2次讀取到2個數據包,第一次讀取到D1包和D2包的部分內容D2_1,第二次讀取到D2包的剩餘內容,被稱為TCP拆包。
- 第四中情況:Server端分2次讀取到2個數據包,第一次讀取到D1包的部分內容D1_1 ,第二次讀取到D1包的剩餘內容D1_2和D2包的整包。
二、重現TCP粘包
2.1 程式碼示例(伺服器端)
修改上一篇【Netty4.X】Unity客戶端與Netty伺服器的網路通訊(一)的ServerHandler類程式碼。在類中申明一個計數常量count,當每讀到一條訊息後,就count++,然後傳送應答訊息給客戶端,程式碼如下:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf)msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req,"UTF-8").substring(0, req.length - System.getProperty("line.separator").length()); count++; System.out.println("body"+body+";"+ ++count); String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new Date(System.currentTimeMillis()).toString():"BAD ORDER"; ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); ctx.writeAndFlush(resp); }
2.2 程式碼示例(客戶端)
修改HttpClient的send()方法,當客戶端與伺服器鏈路建立成功之後,迴圈傳送100條訊息類程式碼。
public void Send()
{
if(client == null)
{
start ();
}
byte[] buffer = Encoding.UTF8.GetBytes("userName:"+userName.text+" password"+password.text);
for(int i = 0;i < 100;i++)
{
client.Send(buffer);
}
}
控制檯(伺服器): +------------------------------------------------------------------+ 七月 07, 2016 8:09:35 下午 com.game.lll.net.HttpServer main 資訊: 服務已啟動... ad4ea569進來了 bodyuserName:aaa password:bbb ...此處省略36條 userName:aaa password:b;count:1 userName:aaa password:bbb ...此處省略36條 userName:aaa password;count:2 userName:aaa password:bbb ...此處省略22條 userName:aaa password:bbb;count:3 +------------------------------------------------------------------+
2.3 控制檯(客戶端)
按照設計初衷,客戶端應該收到100條AD ORDER訊息,但實際上只收到了一條。
2.4 粘包問題的解決辦法
粘包的解決辦法有很多,可以歸納如下。
- 訊息定長,例如每個報文的大小為固定長度200位元組,如果不夠,空位補空格。
- 在包尾增加回車換行符進行分割,例如FTP協議。
- 將訊息分為訊息頭和訊息體,訊息頭中包含訊息長度的欄位,通常設計思路為訊息頭的第一個欄位使用int32來表示訊息的總長度
在本案例中,我使用的是第2個解決辦法在包尾增加回車換行符進行分割。
2.5 程式碼修改(伺服器端)
1新建一個類ServerChannelHandler繼承於ChannelInitializer,重點程式碼在26,27行,在原來的ServerHandler之前新增了兩個解碼器:LineBasedFrameDecoder和StringDecoder。程式碼如下
package com.game.lll.net;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class ServerChannelHandler extends ChannelInitializer<SocketChannel>{
public static void main(String[] args) throws Exception {
int port = 8844;
if(args!=null&&args.length>0)
{
try {
port = Integer.valueOf(args[0]);
} catch (Exception e) {
// TODO: handle exception
}
}
System.out.println(port);
new HttpServer().bind(port);
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ServerHandler());
}
}
2修改原來的HttpServer類,修改程式碼如下:
package com.game.lll.net;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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 HttpServer {
private static Log log = LogFactory.getLog(HttpServer.class);
public void bind(int port) throws Exception {
log.info("伺服器已啟動");
////配置服務端的NIO執行緒組
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ServerChannelHandler());
}
}).option(ChannelOption.SO_BACKLOG, 128) //最大客戶端連線數為128
.childOption(ChannelOption.SO_KEEPALIVE, true);
//繫結埠,同步等待成功
ChannelFuture f = b.bind(port).sync();
//等待服務端監聽埠關閉
f.channel().closeFuture().sync();
} finally {
//優雅退出,釋放執行緒池資源
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
3修改ServerHandler類,程式碼如下:
package com.game.lll.net;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class ServerHandler extends ChannelInboundHandlerAdapter{
private static Log log = LogFactory.getLog(ServerHandler.class);
private int count = 0;
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
super.handlerAdded(ctx);
System.out.println(ctx.channel().id()+"進來了");
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
super.handlerRemoved(ctx);
System.out.println(ctx.channel().id()+"離開了");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
String body = (String)msg;
System.out.println("body"+body+";count:"+ ++count);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)?new Date(System.currentTimeMillis()).toString():"BAD ORDER";
currentTime = currentTime+System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
// TODO Auto-generated method stub
ctx.close();
}
}
直接看第33行,修改前後程式碼比較。修改前:
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
修改後:
String body = (String)msg;
2.6 程式碼修改(客戶端端)
byte[] buffer = Encoding.UTF8.GetBytes("userName:"+userName.text+" password:"+password.text+"\r\n");
在每一條訊息尾巴後新增“\r\n’”控制檯(伺服器): +------------------------------------------------------------------+ 8844 七月 07, 2016 8:37:58 下午 com.game.lll.net.HttpServer bind 資訊: 伺服器已啟動 04d575ff進來了 bodyuserName:aaa password:bbb;count:1 此處省略很多條...... bodyuserName:aaa password:bbb;count:99 bodyuserName:aaa password:bbb;count:100 +------------------------------------------------------------------+
本章參考書籍
<<Netty權威指南(第2版)>>
若有錯誤之處,請多多諒解並歡迎批評指正。 本部落格中未標明轉載的文章歸作者小毛驢所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。