粘包和拆包是什么?
TCP協議是一種字節流協議,沒有記錄邊界,我們在接收消息的時候,不能人為接收到的數據包就是一個整包消息
當客戶端向服務器端發送多個消息數據的時候,TCP協議可能將多個消息數據合並成一個數據包進行發送,這就是粘包
當客戶端向服務器端發送的消息過大的時候,tcp協議可能將一個數據包拆成多個數據包來進行發送,這就是拆包
以下一netty為例,展示一下tcp粘包和拆包的例子:
ServerBusinessHanler:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
import java.util.UUID;
public class ServerBusinessHanler extends SimpleChannelInboundHandler<ByteBuf> {
int count = 0;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] bytes = new byte[msg.readableBytes()];
msg.readBytes(bytes);
String message = new String(bytes, Charset.forName("UTF-8"));
System.out.println("從客戶端收到的字符串:" + message);
System.out.println("服務端接收到的請求數:" + (++count));
ctx.writeAndFlush(Unpooled.copiedBuffer(UUID.randomUUID().toString(),Charset.forName("UTF-8")) );
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getStackTrace());
ctx.channel().close();
}
}
Server:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
public class Server {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ServerBusinessHanler());
}
});
serverBootstrap.bind(new InetSocketAddress(8899)).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
ClientBusinessHandler:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
public class ClientBusinessHandler extends SimpleChannelInboundHandler<ByteBuf> {
int count = 1;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] bytes = new byte[msg.readableBytes()];
msg.readBytes(bytes);
System.out.println("從服務器端接收到的信息: " + new String(bytes, Charset.forName("UTF-8")));
System.out.println("從服務器端讀到的請求的個數: " + count++);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer("中國移動".getBytes()));
}
ctx.writeAndFlush(Unpooled.copiedBuffer("jiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyiping".getBytes()));
}
}
Client:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class Client {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventExecutors = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventExecutors)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ClientBusinessHandler());
}
});
Channel channel = bootstrap.connect("127.0.0.1", 8899).channel();
channel.closeFuture().sync();
}
}
分別運行服務器端和客戶端,我們看到服務器端的輸出:
相應的,服務器端也只給客戶端了兩個返回,而不是我們期待的11個
我們從客戶端傳入的十個 "中國移動"和第二次發送的長的字符串的前一部分發生了粘包,而第二個長字符串的后一部分則被拆分到第二個數據包里了
粘包和拆包的情況都出現了
那怎么解決粘包和拆包的問題呢? 一種方法是在字符串中使用特定的分隔符來分隔,另外一種方法是,我們在發送數據包的時候在前邊附加上數據包的長度
netty為我們提供了多種的解碼器,比如定長解碼器和基於分隔符的解碼器等,這里,我么使用 LengthFieldBasedFrameDecoder這個解碼器來解決粘包和拆包的問題,關於這個解碼器的詳細信息,可以參考這個類的java doc文檔,上邊講述的很詳細,簡單來說,就是我們需要在請求中添加一些關於數據字段長度的信息(以下簡稱length),LengthFieldBasedFrameDecoder的構造方法中,設置一下關於length所占的字節數和要跳過的字節數等信息(我們只需要讀取數據內容的話,可以跳過length的信息) 這樣的話,我們從客戶端發送的每個數據包,都可以正確地解析
首先,我們增加一個編碼器,為數據包附件上lenght:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
//本類作用,加上四個字節的消息頭(int類型),表示數據包長度
public class LengthEncoder extends MessageToByteEncoder<ByteBuf> {
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
int length = msg.readableBytes();
byte[] bytes = new byte[length];
msg.readBytes(bytes);
out.writeInt(length);
out.writeBytes(bytes);
}
}
然后,將我們的編碼器和對應的 LengthFieldBasedFrameDecoder添加到服務器端和客戶端的ChannelInitializer里邊去
修改之后的服務器端代碼:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import java.net.InetSocketAddress;
public class Server {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(4096,0,4,0,4));
ch.pipeline().addLast(new LengthEncoder());
ch.pipeline().addLast(new ServerBusinessHanler());
}
});
serverBootstrap.bind(new InetSocketAddress(8899)).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
new LengthFieldBasedFrameDecoder(4096,0,4,0,4));的意思是:使用LengthFieldBasedFrameDecoder的最大的數據包大小是4096個,其中length占四個字節,讀取的時候跳過4個字節(也就是最終解碼的時候,忽略掉length字段,只返回真正的數據內容)
修改之后的客戶端代碼:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
public class Client {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventExecutors = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventExecutors)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(4096,0,4,0,4));
ch.pipeline().addLast(new LengthEncoder());
ch.pipeline().addLast(new ClientBusinessHandler());
}
});
Channel channel = bootstrap.connect("127.0.0.1", 8899).channel();
channel.closeFuture().sync();
}
}
分別運行服務器端和客戶端之后我們得到的結果:
服務器端輸出:
客戶端輸出:
這樣就解決了粘包和拆包導致的問題