目錄
1 TCP粘包/拆包
TCP是一個“流”協議,所謂流就是沒有邊界的一串字符串,其間沒有分界線。下面就用一個列子來說明
TCP粘包/拆包問題說明
假設客戶端分別發送了兩個數據包D1和D2給服務端,由於服務端一次讀取到的字節數是不確定的,故可能存在以下4種情況。
(1)服務端分兩次讀取到了兩個獨立的數據包,分別是D1和D2,沒有粘包和拆包;
(2)服務端一次接收到了兩個數據包,D1和D2粘合在一起,被稱爲TCP粘包;
(3)服務端分兩次讀取到了兩個數據包,第一次讀取到了完整的D1包和D2包的部分內容,第二次讀取到了D2包的剩餘內容,這被稱爲TCP拆包;
(4)服務端分兩次讀取到了兩個數據包,第一次讀取到了D1包的部分內容D1_1,第二次讀取到了D1包的剩餘內容D1_2和D2包的整包。
如果此時服務端TCP接收滑窗非常小,而數據包D1和D2比較大,很有可能會發生第五種可能,即服務端分多次才能將D1和D2包接收完全,期間發生多次拆包。
TCP粘包/拆包發生的原因
問題產生的原因有三個,分別如下。
(1)應用程序write寫入的字節大小大於套接口發送緩衝區大小;
(2)進行MSS大小的TCP分段;
(3)以太網幀的payload大於MTU進行IP分片。
粘包問題的解決策略
由於底層的TCP無法理解上層的業務數據,所以在底層是無法保證數據包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案,可以歸納如下。
(1)消息定長,例如每個報文的大小爲固定長度200字節,如果不夠,空位補空格;
(2)在包尾增加回車換行符進行分割,例如FTP協議;
(3)將消息分爲消息頭和消息體,消息頭中包含表示消息總長度(或者消息體長度)的字段,通常設計思路爲消息頭的第一個字段使用int32來表示消息的總長度;
(4)更復雜的應用層協議。
2 Netty遇到粘包和拆包
在前面一章(Netty的HelloWord)中客戶端只發送了一次請求給服務端,如果發送多次請求而且沒有進行TCP粘包和拆包的處理,會發生什麼樣的情況。
我們把上一章的例子的TimeClientHandler和TimeServerHandler進行修改
TimeClientHandler類 把發送消息加上一個換行符 然後發送一百次
req = ("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes();
System.out.println("從服務端得到的時間:"+body+" counter:"+ ++counter);
public class TimeClientHandler extends ChannelHandlerAdapter {
private static final Logger logger = Logger.getLogger(TimeClientHandler.class.getName());
private byte[] req;
private int counter = 0;
public TimeClientHandler() {
req = ("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message;
for(int i = 0;i<100;i++) {
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] time = new byte[buf.readableBytes()];
buf.readBytes(time);
String body = new String(time,"utf-8");
System.out.println("從服務端得到的時間:"+body+" counter:"+ ++counter);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.warning(cause.getMessage());
ctx.close();
}
}
TimeServerHandler類 服務端獲取信息
String body = new String(req,"utf-8").substring(0,req.length-(System.getProperty("line.separator").length()));
System.out.println("從客戶端發送的消息:"+body+" counter:"+ ++counter);
public class TimeServerHandler extends ChannelHandlerAdapter {
private int counter = 0;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
//System.getProperty("line.separator") 獲取回車換行符
String body = new String(req,"utf-8").substring(0,req.length-(System.getProperty("line.separator").length()));
System.out.println("從客戶端發送的消息:"+body+" counter:"+ ++counter);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
測試 先運行服務端,在運行客戶端
結果會發現並沒有我們想象的那樣,客戶端發送一百條請求,服務端會接受一百條請求並且打印這些請求,然後在響應客戶端一百條響應,客戶端打印一百條來自服務端響應的消息
出現這種的原因在於消息在TCP中發生了粘包和拆包的問題
3 Netty解決粘包和拆包
在Netty中解決粘包和拆包問題需要使用解碼器(Decoder),在Netty中提供了許多的解碼器,比如LineBasedFrameDecoder,回車符解碼器,FixedLengthFrameDecoder固定長度解碼器。這些解碼器的父類是ByteToMessageDecoder
在ByteToMessageDecoder中定義了一個decode的方法,該方法定義瞭解碼規則,不同的解碼器實現ByteToMessageDecoder類重寫decode方法編寫解碼規則,我們也可以自己定義自己的解碼器,只要重寫decode方法就可以。
我們以LineBasedFrameDecoder解碼器來說明其流程
還是以前面的例子來介紹其流程(我們前面的例子在消息的結尾都加上了回車換行符)
首先我們在TimeServer類中ChlidChannelHandler 方法中在初始化Channel的時候加上LineBasedFrameDecoder(1024)其中1024表示這個數據包的大小,也就是編碼規則(我們這裏表示的是換行符)之間的消息大小,如果數據包大小超過就會報異常
我們還加上了StringDecoder()解碼器,將消息變成String類型。這樣我們就可以在TimeServerHandler()中不需要講消息轉變成String類型可以直接使用。
private class ChlidChannelHandler extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
/**
* LineBasedFrameDecoder() 換行符解碼器
* StringDecoder()將接收到的對象轉換成String對象
*/
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeServerHandler());
}
}
我們點開LineBasedFrameDecoder類或者StringDecoder類就可以看到decode方法中編寫的解碼規則。並且將解碼後的消息放在一個list集合中,該消息會傳到給下一個解碼器或者處理器(下面代碼爲StringDecoder解碼器中的decode方法)
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
out.add(msg.toString(charset));
}
然後我們在TimeCilent類中加入LineBasedFrameDecoder類和StringDecoder類解碼器。
private class ChlidChannelHandler extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
/**
* LineBasedFrameDecoder() 換行符解碼器
* StringDecoder()將接收到的對象轉換成String對象
*/
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeServerHandler());
}
}
最後 運行服務端,接着客戶端