Netty的TCP粘包/拆包

目錄

 

1 TCP粘包/拆包

TCP粘包/拆包問題說明

TCP粘包/拆包發生的原因

粘包問題的解決策略

2 Netty遇到粘包和拆包

3 Netty解決粘包和拆包


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());
			
		}
	}

最後 運行服務端,接着客戶端

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章