由于TCP底层无法理解上层的应用数据,所以TCP底层无法保证数据包不会被拆分和重组,该问题只能通过上层应用协议栈设计来解决。
解决TCP拆包粘包的根本原因,就是找出消息的边界。业界主流的方案有如下几个:
- 消息定长,譬如每个报文规定长度为100个字节,如果不够,空位用空格或其余字符代替。
- 指定分隔符,在包尾添加如换行符进行分割。
- 将消息分为消息头和消息体,消息头中包含标示数据总长度的字段,根据长度读取数据。
- 更复杂的应用层协议。
以上是TCP粘包/拆包的基础知识,下面用实际案例来看看netty是如何解决TCP粘包拆包问题。
1. 固定长度-FixedLengthFrameDecoder
消息长度固定,累计读取到长度总和为定长Len的报文后,就认为读取到了一条完整的消息。
注意:FixedLengthFrameDecoder并没有提供一个对应的编码器,因为接收方只需要根据字节数进行判断即可,发送方无需编码。
例如:我们规定每个报文的大小为固定长度 5个字节,表示一个有效报文,如果不够,空位补空格;
优点是实现比较简单,缺点是造成资源浪费,因为此方式不推荐使用。
服务端开发
@Slf4j
public class FixedLengthServer {
public static void main(String[] args) {
EventLoopGroup boos = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boos, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new FixedLengthFrameDecoder(5))
.addLast(new StringDecoder())
.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("server:{}", msg);
}
});
}
});
ChannelFuture channelFuture = bootstrap.bind(8080);
try {
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boos.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
客户端开发
@Slf4j
public class FixeLengthClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
//将发送的内容encode编码
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel();
for (int i = 0; i < 10; i++) {
channel.writeAndFlush("hello222");
}
}
}
运行结果
指定分隔符-DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder提供了多个构造方法,比如下面两个
其中:
- maxFrameLength:
- 指定消息的最大长度,如果消息长度超过指定的长度并且没有找到分隔符,则会抛异常。
- 如果消息长度小于指定的长度并且没有找到分隔符,会缓存收到的消息,直到接收到分隔符。
- 同时存在多个分隔符时,优先匹配长度最短的分隔符,如果一样长,则哪个先出现,匹配哪个。
- stripDelimiter:
- 解码后的消息是否去除分隔符。
- delimiter
- 自定义的分隔符
优点是实现简单,缺点是需要对整个包消息进行扫描(在数据中寻找分隔符),因为效率不高。
服务端开发
/**
* 指定分隔符解决粘包粘包
*/
@Slf4j
public class DelimiterServer {
public static void main(String[] args) {
EventLoopGroup boos = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boos, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("$_".getBytes())))
.addLast(new StringDecoder())
.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("server:{}", msg);
}
});
}
});
ChannelFuture channelFuture = bootstrap.bind(8080);
try {
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boos.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
客户端开发
@Slf4j
public class DelimiterClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
//将发送的内容encode编码
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel();
for (int i = 0; i < 10; i++) {
channel.writeAndFlush("hello$_");
}
}
}
运行结果
指定长度-LengthFieldBasedFrameDecoder
简单来说就是在消息中增加数据长度域,当然还可以增加其它信息,该方式类似于http协议,如http协议中header的Content-Length属性。
LengthFieldBasedFrameDecoder有多种配置参数的方式,常见于私有客户端和服务器协议。
参数说明
LengthFieldBasedFrameDecoder 类里面主要有以下这几个参数:
-
maxFrameLength:本次能接收的最大的数据长度
-
lengthFieldOffset:设置的长度域的偏移量,长度域在数据包的起始位置
-
lengthFieldLength:长度域的长度
-
lengthAdjustment:数据包的偏移量,可以理解为长度域和content区间的数据长度
-
initialBytesToStrip:需要跳过的字节数
lengthFieldLength
这里将 lengthFieldLength 设置为2,说明用2个字节(0x000C转成十进制等于12)表示消息内容的总长度(Content)
lengthAdjustment
这里将 initialBytesToStrip 设置为2,代表解码后跳过消息中的前两个字节(长度域)
lengthFieldOffset
长度域 lengthFieldLength 长为3个字节(0x00000C),lengthFieldOffset 偏移量为2(因为前两个字节给Header1 占了)
initialBytesToStrip
长度域 lengthFieldLength 为两个字节,initialBytesToStrip设置为2,解码后跳过两个字节,即解码后的数据不包含长度域
lengthAdjustment
lengthAdjustment 设置为2,表示 Length 和 Content 中间还有两个字节的Header1
服务端开发
@Slf4j
public class LengthFieldServer {
public static void main(String[] args) {
LengthFieldServer lengthFieldServer = new LengthFieldServer();
lengthFieldServer.start(8888);
}
private void start(int port) {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
//给boss添加handler
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
//发送的数据包最大长度为1024*64
//length域的偏移,正常情况下读 取数据从偏移为0处开始读取,如果有需要可以从其他偏移量处开始读取
//length域占用的字节数
//在length域和content域中间是 否需要填充其他字节数
//解码后跳过的字节数 ( 解码后把 length占用的字节跳过,直接传数据包)
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 64, 0, 4, 0, 4));
pipeline.addLast(new MySimpleChannelInboundHandler());
}
});
//绑定端口
ChannelFuture future = serverBootstrap.bind(port).sync();
//监听端口的关闭
future.channel().closeFuture().sync();
} catch (Exception e) {
log.error("netty server error ,{}", e.getMessage(), e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
服务端handle
@Slf4j
public class MySimpleChannelInboundHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count = 0;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
count++;
log.info("服务端第:{} 次收到消息,消息内容为:{}", count, msg.toString(StandardCharsets.UTF_8));
}
}
客户端handle
@Slf4j
public class ClientInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 1; i <= 100; i++) {
ByteBuf byteBuf = ctx.alloc().buffer().writeBytes((i + "").getBytes());
ctx.channel().writeAndFlush(byteBuf);
}
super.channelActive(ctx);
}
}
客户端开发
@Slf4j
public class LengthFieldClient {
public static void main(String[] args) {
LengthFieldClient client = new LengthFieldClient();
client.start("127.0.0.1", 8888);
}
public void start(String host, int port) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
//添加编码器
//参数,长度域
pipeline.addLast(new LengthFieldPrepender(4));
//添加客户端channel对应的handler
pipeline.addLast(new ClientInboundHandler());
}
});
//连接远程启动
ChannelFuture future = bootstrap.connect(host, port).sync();
//监听通道关闭
Channel channel = future.channel();
channel.closeFuture().sync();
} catch (Exception e) {
log.error("netty client error ,msg={}", e.getMessage());
} finally {
//优雅关闭
group.shutdownGracefully();
}
}
}
结果
客户端发送了100条消息,服务端均接收到。