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();
}
}
輸出結果1hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 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++;
}
}