
前言
TCP是面向連接的,服務端和客戶端通過socket進行資料傳輸,發送端為了更有效的發送資料,通常會使用Nagle演算法把多個資料塊合并成一個大的資料塊,這樣做雖然提高了效率,但是接收端就很難識別完整的資料包了(TCP無訊息保護邊界),可能會出現粘包拆包的問題,
粘包拆包理解
下面我用一個圖來帶大家理解什么是粘包和拆包

解釋一下
- 第一次傳輸沒有問題,資料1和資料2沒有粘合,也沒有拆分
- 第二次傳輸,資料1和資料2粘在一起傳輸了,出現了粘包
- 第三次傳輸,資料2被分為了2部分,資料2_1 第一份和資料1粘在一起,資料2_2第二份單獨傳輸,這里即出現了拆包也出現了粘包
粘包拆包代碼演示
這里寫一個簡單案例來演示粘包拆包,客戶端發送10個資料包,觀察服務端是否做了10次讀取,如果不是,就出現粘包或者拆包的情況,這里我們使用byte型別來傳輸案例如下,
第一步:撰寫Netty服務端
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加handler
pipeline.addLast(new ServerHandler());
}
});
try {
ChannelFuture sync = bootstrap.bind(3000).sync();
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
第二步:撰寫服務端handler
public class ServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
//服務端接收次數
private int num = 0;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("接收訊息,次數 = "+ num++);
//接收資料
byte[] bytes = new byte[msg.readableBytes()];
//把資料讀到bytes中
msg.readBytes(bytes);
System.out.println(new String(bytes, CharsetUtil.UTF_8));
}
}
這里定義了一個num來記錄服務端資料讀取次數,
第三步:定義Netty客戶端
public static void main(String[] args) {
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ClientHandler());
}
});
ChannelFuture sync = null;
try {
sync = bootstrap.connect("127.0.0.1", 3000).sync();
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
eventLoopGroup.shutdownGracefully();
}
}
第四步:定義客戶端的Handler
public class ClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//發送10個資料塊
for (int i = 0; i < 10; i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer("資料塊"+i+";", CharsetUtil.UTF_8));
}
}
}
這里回圈了10次,我發送了10個資料塊
第五步:測驗,啟動服務端和客戶端,觀察控制臺

問題比較明顯,客戶端發送了10次資料,服務端做了5次接收,第3次4次5次都出現了粘包的情況,
定義編碼器解決粘包拆包問題
要解決粘包拆包的問題就要明確資料邊界,尷尬的是面向流的通信是沒有訊息保護邊界的,所以我們需要自定義傳輸協議來確定訊息的邊界,說的再直白一點就是我們如果能夠明確服務端每次讀取訊息的長度,那就不會出現粘包拆包問題了,
如果要做到該效果,那么就需要自定義訊息協議和編碼解碼器,我們先來處理客戶端,
第一步:定義協議 , 指定訊息長度和內容
//定義訊息協議
public class MsgProtocol {
//內容長度
private int len;
//內容
private byte[] data;
public MsgProtocol(int len , byte[] data){
this.len = len;
this.data = data;
}
public MsgProtocol(){}
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
}
第二步:客戶端的handler發送MsgProtocol物件
public class ClientHandler extends SimpleChannelInboundHandler<MsgProtocol> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//發送10個資料塊
for (int i = 0; i < 10; i++) {
String data = "資料塊"+i;
byte[] bytes = data.getBytes(CharsetUtil.UTF_8);
//長度
int len = bytes.length;
//構建一個MsgProtocol,并寫去
ctx.writeAndFlush(new MsgProtocol(len,bytes));
}
}
}
第三步:繼承MessageToByteEncoder,自定義編碼器 ,把訊息的長度和內容寫出去
//定義直接的編碼器:MessageToByteEncoder 把Messsage轉換成 byte
public class MessageEncoder extends MessageToByteEncoder<MsgProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MsgProtocol msg, ByteBuf out) throws Exception {
//這里需要把內容的長度寫給服務端
out.writeInt(msg.getLen());
//把內容寫給服務端
out.writeBytes(msg.getData());
}
}
第四步:客戶端指定編碼器
public static void main(String[] args) {
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入自定義的編碼器
pipeline.addLast(new MessageEncoder());
pipeline.addLast(new ClientHandler());
}
});
ChannelFuture sync = null;
try {
sync = bootstrap.connect("127.0.0.1", 3000).sync();
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
eventLoopGroup.shutdownGracefully();
}
}
客戶端的作業完成了,接下來我們處理服務端
第一步:撰寫解碼器,需要把byte資料封裝成MsgProtocol
//定義解碼器,拿到資料長度和內容轉換成MsgProtocol,交給handler處理
public class MessageDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//拿到資料的長度
int len = in.readInt();
//拿到資料的內容
byte[] bytes = new byte[len];
in.readBytes(bytes);
//把解碼后的資料交給下一個handler
out.add(new MsgProtocol(len,bytes));
}
}
ReplayingDecoder就是對ByteToMessageDecoder的 擴展和簡化
第二步:服務端handler,這里接收的是MsgProtocol訊息物件
public class ServerHandler extends SimpleChannelInboundHandler<MsgProtocol> {
//服務端接收次數
private int num = 0;
@Override
protected void channelRead0(ChannelHandlerContext ctx, MsgProtocol msg) throws Exception {
System.out.println("接收訊息,次數 = "+ num++);
//接收資料
System.out.println(new String(msg.getData(), CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.channel().close();
}
}
第三步:服務端指定解碼器
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加解碼器
pipeline.addLast(new MessageDecoder());
pipeline.addLast(new ServerHandler());
}
});
try {
ChannelFuture sync = bootstrap.bind(3000).sync();
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
到這里服務端撰寫完成,接下來依次啟動服務端和客戶端進行測驗,效果如下

可以看到,客戶端發送了10次,服務器接收了10次,沒有出現粘包拆包的情況了,所以問題的關鍵就是服務端解碼器中需要明確訊息的長度,就能夠明確每次訊息讀取的邊界,就不會出問題了,
好了文章結束,喜歡就給個好評吧
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/377316.html
標籤:其他
