閱讀本文建議從第一篇開始往後看
本系列文章
- Netty在Android開發中的應用實戰系列(一)——— 搭建服務端與客戶端
- Netty在Android開發中的應用實戰系列(二)——— Encoder | Decoder | Handler 的使用
- Netty在Android開發中的應用實戰系列(三)——— 心跳處理 | 斷線重連
- Netty在Android開發中的應用實戰系列(四)——— 粘包 | 拆包 處理
一、什麼粘包呢?
簡單來說就是:發送的多個包被粘到了一塊變成了一個包。
比如說:服務端連續發送了兩個包客戶端確只收到了一條包(這一個包裏其實就是兩個數據包),正常來說客戶端也應該收到兩個包。這就是所謂的粘包 客戶端遇到這種情況就需要做拆包處理了。
二、一般處理粘包的手段
- 使用固定長度的數據
- 使用特殊字符分割($、\n…)
- 其他
Netty也爲我們提供了幾個處理粘包的解碼器,如下:
DelimiterBasedFrameDecoder
基於特殊字符進行粘包拆包處理FixedLengthFrameDecoder
基於固定長度進行粘包拆包處理LengthFieldBasedFrameDecoder
基於消息頭指定消息長度進行粘包拆包處理LineBasedFrameDecoder
基於換行符( \r\n,\n)進行粘包拆包處理
三、下面通過一個示例來處理粘包拆包,這裏我使用DelimiterBasedFrameDecoder
這個解碼器然後使用$
作爲特殊分隔符
3.1 首先給服務端添加DelimiterBasedFrameDecoder
/**
* 啓動tcp服務端
*/
public void startServer() {
try {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
//分隔符
ByteBuf delimiter = Unpooled.copiedBuffer("$".getBytes());
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//解決粘包
pipeline.addLast(new DelimiterBasedFrameDecoder(65535, delimiter));
//添加發送數據編碼器
pipeline.addLast(new ServerEncoder());
//添加解碼器,對收到的數據進行解碼
pipeline.addLast(new ServerDecoder());
//添加數據處理
pipeline.addLast(new ServerHandler());
}
});
//服務器啓動輔助類配置完成後,調用 bind 方法綁定監聽端口,調用 sync 方法同步等待綁定操作完成
b.bind(PORT).sync();
handler.obtainMessage(0, "TCP 服務啓動成功 PORT = " + PORT).sendToTarget();
Log.d(TAG, "TCP 服務啓動成功 PORT = " + PORT);
} catch (Exception e) {
e.printStackTrace();
}
3.2 既然使用了特殊分隔符用來處理粘包,那麼就需要給發送的每一個數據包添加上這個$
符號;只需要在定義的Encoder
中添加即可,如下:
public class ServerEncoder extends MessageToByteEncoder<PkgDataBean> {
private static final String TAG = "ServerEncoder";
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, PkgDataBean data, ByteBuf byteBuf) throws Exception {
//根據數據包協議,生成byte數組
byte[] bytes = {0x2A, data.getCmd(), data.getDataLength()};
byte[] dataBytes = data.getData().getBytes();
//分隔符
byte[] delimiter = "$".getBytes();
//將所有數據合併成一個byte數組
byte[] all = ByteUtil.byteMergerAll(bytes, dataBytes, new byte[]{0x2A}, delimiter);
//發送數據
byteBuf.writeBytes(all);
}
}
3.3 我們寫個連續發送數據包的代碼
//獲取與客戶端的連接
List<ChannelHandlerContext> channels = ServerHandler.channels;
for (ChannelHandlerContext ctx : channels) {
for (int i = 0; i < 3; i++) {
PkgDataBean bean = new PkgDataBean();
bean.setCmd((byte) 0x05);
bean.setData("粘包的數據:" + i);
bean.setDataLength((byte) bean.getData().getBytes().length);
ctx.channel().writeAndFlush(bean);
}
}
Log.d(TAG, "服務端發送了粘包數據");
四、既然服務端已經加了DelimiterBasedFrameDecoder
解碼器,那麼客戶端也是需要同步添加的;否則是無法正常解析數據的
public void connect() {
try {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap()
// 指定channel類型
.channel(NioSocketChannel.class)
// 指定EventLoopGroup
.group(group)
// 指定Handler
.handler(new ChannelInitializer<SocketChannel>() {
//分隔符
ByteBuf delimiter = Unpooled.copiedBuffer("$".getBytes());
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new IdleStateHandler(10, 0, 0));
//解決粘包
pipeline.addLast(new DelimiterBasedFrameDecoder(65535, delimiter));
//添加發送數據編碼器
pipeline.addLast(new ClientEncoder());
//添加收到的數據解碼器
pipeline.addLast(new ClientDecoder());
//添加數據處理器
pipeline.addLast(new ClientHandler(NettyClient.this));
}
});
// 連接到服務端
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
// 添加連接狀態監聽
channelFuture.addListener(new ConnectListener(this));
//獲取連接通道
channel = channelFuture.sync().channel();
handler.obtainMessage(0, "連接成功").sendToTarget();
} catch (Exception e) {
handler.obtainMessage(0, "連接失敗").sendToTarget();
Log.e(TAG, "連接失敗:" + e.getMessage());
e.printStackTrace();
}
}
- 同樣的,客戶端發送的數據也需要在數據包的末尾寫入
$
符號。
五、現在來看看程序執行的效果
- 服務端連續發送3條數據
- 客戶端收到的數據