Netty學習10-粘包和拆包

1 粘包拆包基本概念


TPC是一個面向流的協議。所謂流就是沒有邊界的一串數據,如同河水般連成一片,其中並沒有分界線。TCP底層並不瞭解上層業務數據的具體含義,它會根據TCP緩衝區的具體情況進行包的劃分,所以在業務上認爲,一個完整的包可能會被TCP拆成多個包發送,也有可能把多個小包封裝成一個包發送。這就是拆包和粘包的概念。

比如向對方發送信息:Good Morning Sit down please。先向對方問早,再請對方坐下。但實際情況有可能這樣:
第一次接收:Good Morning Sit
第二次接收:down please


2 解決粘包拆包的途徑


TCP是面向流的協議,消息中間沒有明顯的界限。那爲了對消息進行區分,只能依靠上層的應用協議,往往採取如下方式:

[1] 消息長度固定。累計讀取到長度總和爲定長的LEN的報文後,就認爲讀取到一個完整的消息。將計數器置位,重新開始讀取下個數據報。
[2] 將回車換行符作爲消息結束符。如FTP協議,這種方式在文本協議中應用廣泛。
[3] 將特殊的分隔符作爲消息的結束標誌。回車換行符就是一種特殊的結束符。
[4] 通過在消息頭中定義長度字段來標識消息的總長度。

Netty提供了對應的解碼器:LineBaseFrameDecoder、DelimiterBaseFrameDecoder、FixedLengthFrameDecoder等。具體示例參考《Netty權威指南》。下面分析一個粘包拆包的示例,和自定義解碼器。


3 粘包拆包實例

import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;

public class Server {
	public static void main(String[] args) {
		// 服務類
		ServerBootstrap bootstrap = new ServerBootstrap();
		// boss線程監聽端口,worker線程負責數據讀寫
		ExecutorService boss = Executors.newCachedThreadPool();
		ExecutorService worker = Executors.newCachedThreadPool();
		// 設置niosocket工廠
		bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
		// 設置管道的工廠
		bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
			@Override
			public ChannelPipeline getPipeline() throws Exception {
				ChannelPipeline pipeline = Channels.pipeline();
				pipeline.addLast("handler1", new HelloHandler());
				return pipeline;
			}
		});
		bootstrap.bind(new InetSocketAddress(10101));
		System.out.println("start!!!");
	}
}

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

public class HelloHandler extends SimpleChannelHandler {
	private int count = 1;
	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
			throws Exception {
		ChannelBuffer buffer = (ChannelBuffer) e.getMessage();
		byte[] array = buffer.array();
		System.out.println(new String(array) + "  " + count);
		count++;
	}
}

import java.net.Socket;
public class Client {
	public static void main(String[] args) throws Exception {
		Socket socket = new Socket("127.0.0.1", 10101);
		String message = "hello";
		for (int i = 0; i < 20; i++) {
			socket.getOutputStream().write(message.getBytes());
		}
		socket.close();
	}
}
輸出結果1
hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello  1

輸出結果2
hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohe 1
llohellohellohellohello 2



4 自定義處理器

在本例中採取了固定長度的方式解決粘包拆包問題,有以下幾個顯著的變化:
[1] 在Client發送時,設置了一個4字節的長度頭,該長度頭記錄了內容的長度。
[2] Server端設置了MyDecoder繼承自FrameDecoder。註釋非常清晰。
[3] 在MyDecoder中對字節數組做了處理,包裝成了String類型。所以下一個handler直接處理String類型即可。

import java.net.Socket;
import java.nio.ByteBuffer;
public class Client {
	public static void main(String[] args) throws Exception {
		Socket socket = new Socket("127.0.0.1", 10101);
		// 消息內容
		String message = "hello";
		byte[] bytes = message.getBytes();
		// 構造字節數組,長度爲(4+內容長度)
		// 其中4個字節長度字段是int爲4個字節
		ByteBuffer buffer = ByteBuffer.allocate(4 + bytes.length); 		
		// 設置長度字段(僅僅是內容的長度)
		buffer.putInt(bytes.length);
		// 設置內容
		buffer.put(bytes);
		
		byte[] array = buffer.array();
		for (int i = 0; i < 20; i++) {
			socket.getOutputStream().write(array);
		}
		socket.close();
	}
}

import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
public class Server {

	public static void main(String[] args) {
		//服務類
		ServerBootstrap bootstrap = new ServerBootstrap();	
		//boss線程監聽端口,worker線程負責數據讀寫
		ExecutorService boss = Executors.newCachedThreadPool();
		ExecutorService worker = Executors.newCachedThreadPool();
		//設置niosocket工廠
		bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
		//設置管道的工廠
		bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
			@Override
			public ChannelPipeline getPipeline() throws Exception {
				ChannelPipeline pipeline = Channels.pipeline();
				pipeline.addLast("decoder", new MyDecoder());
				pipeline.addLast("handler1", new HelloHandler());
				return pipeline;
			}
		});
		bootstrap.bind(new InetSocketAddress(10101));	
		System.out.println("start!!!");
	}
}

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
public class MyDecoder extends FrameDecoder {

	@Override
	protected Object decode(ChannelHandlerContext ctx, Channel channel,
			ChannelBuffer buffer) throws Exception {
		// 基本長度(至少要有長度頭那麼長)
		int baseLength = 4;
		if (buffer.readableBytes() > baseLength) {
			// 防止Socket攻擊
			if (buffer.readableBytes() > 2048) {
				buffer.skipBytes(buffer.readableBytes());
			}
			// 標記
			buffer.markReaderIndex();
			// 長讀取度頭
			int length = buffer.readInt();
			// 長度不夠
			if (buffer.readableBytes() < length) {
				// 還原到上述標記位置
				buffer.resetReaderIndex();
				// 緩存當前剩餘的buffer數據,等待剩下數據包到來
				return null;
			}
			// 讀數據
			byte[] bytes = new byte[length];
			buffer.readBytes(bytes);
			// 往下傳遞對象
			return new String(bytes);
		}
		// 緩存當前剩餘的buffer數據,等待剩下數據包到來
		return null;
	}

}

import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
public class HelloHandler extends SimpleChannelHandler {
	private int count = 1;
	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
			throws Exception {
		System.out.println(e.getMessage() + "  " +count);
		count++;
	}
}

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