Netty TCP粘包拆包
- 熟悉tcp编程的可能都知道,无论是服务器端还是客户端,当我们读取或者发送数据的时候,都需要考虑TCP底层的粘包/拆包机制。
- TCP是一个“流”协议,所谓流就是没有界限的遗传数据。大家可以想象下如果河里的水就好比数据,他们是连成一片的,没有分界线,TCP底层并不了解上层的业务数据具体的含义,它会根据TCP缓冲区的实际情况进行包的划分,也就是说,在业务上,我们一个完整的包可能会被TCP分成多个包进行发送,也可能把多个小包封装成一个大的数据包发送出去,这就是所谓的TCP粘包、拆包问题。
- 分析TCP粘包、拆包问题的产生原因:
- 1 应用程序write写入的字节大小大于套接口发送缓冲区的大小
- 2 进行MSS大小的TCP分段
- 3 以太网帧的payload大于MTU进行IP分片
TCP拆包、粘包解决方案
根据业界主流协议,有三种方案
- 消息定长,例如每个报文的大小固定为200个字节,如果不够,空位补空格;如果大于这个长度,就发多个。
- 在包尾部增加特殊字符进行分割,例如加回车等
- 消息分为消息头和消息体,在消息头中包含表示消息总长度的字段,然后进行业务处理
一般采用第三种消息头和消息体模式。 netty 为我们提供了前两种
NETTY解决TCP拆包粘包问题
netty 为我们引入两个类,两种方法解决TCP拆包粘包问题
- 分隔符类 DelimiterBasedFrameDecoder(自定义分隔符)
- FixedLengthFrameDecoder(定长)
一 . 自定义分隔符
目录结构:Server 使用 ServerHandler , Client 使用 ClientHandler
在 initChannel 中 添加分隔符定义, 这里自定义分隔符 $_
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 设置自定义分隔符。特殊字符应该用 ByteBuf 转换
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
//设置分隔符,发送的最大长度是32*1024 这个应该和SO_SNDBUF设置的一样
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(32*1024,buf));
//设置字符串形式的解码。<传过来的数据直接转码成字符串>
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ServerHandler()); //1
}
});
完整代码:
Server :
public static void main(String[] args) throws InterruptedException {
//1. 创建两个线程组: 一个用于进行网络连接接受的 另一个用于我们的实际处理(网络通信的读写)
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
//2 通过辅助类去构造server/client
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class) // NIO 通道
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) // 连接超时时间
.option(ChannelOption.SO_BACKLOG, 1024) // sync 和 accept队列加起来=backlog 即1024
.option(ChannelOption.SO_KEEPALIVE,false) // 是否是长连接
.childOption(ChannelOption.TCP_NODELAY, true) //禁用Nagel算法,禁止小的请求包的合并
.childOption(ChannelOption.SO_RCVBUF, 1024 * 32) //接受区的缓存大小 1024byte * 32
.childOption(ChannelOption.SO_SNDBUF, 1024 * 32) //发送区的缓存大小 1024byte * 32
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
/**
* 底层其实是一个责任链模式
*/
// 设置特殊分隔符 ,特殊字符应该用 ByteBuf 转换
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
//设置分隔符,发送的最大长度是32*1024 这个应该和SO_SNDBUF设置的一样
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024*32,buf));
//设置字符串形式的解码。<传过来的数据直接转码成字符串>
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ServerHandler()); //1
/**
*
可以加好多个,一个一个执行下去。对数据一层一层的解析过滤
ch.pipeline().addLast(new ServerHandler()); //2
ch.pipeline().addLast(new ServerHandler()); //3
ch.pipeline().addLast(new ServerHandler()); //4
*/
}
});
//服务器端绑定端口并启动服务
ChannelFuture cf = bootstrap.bind(8765).sync();
//使用channel级别的监听close端口 阻塞的方式
cf.channel().closeFuture().sync();
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
ServerHandler :
注意 : server 返回消息给 客户端时也要加上特殊分隔符 &_
public class ServerHandler extends ChannelInboundHandlerAdapter {
//ServerHandler implements ChannelHandler 一般不实现 ChannelHandler 这个太顶层了
// 通道激活
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.err.println("server channel active..");
}
/**
* 真正的数据最终会走到这个方法进行处理
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 读取客户端的数据
// 因为服务端设置了字符串解码: ch.pipeline().addLast(new StringDecoder());。<传过来的数据直接转码成字符串>
// 所以可以直接把数据强转成String类型
String request = (String) msg;
System.err.println("Server收到消息: " + request);
//返回响应数据, 【记得加上 $_ 分隔符】
String responseBody = "Server返回响应数据: " + request + "$_";
ctx.writeAndFlush(Unpooled.copiedBuffer(responseBody.getBytes()));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.err.println(" exceptionCaught ");
ctx.fireExceptionCaught(cause);
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.err.println(" channelReadComplete : 数据读取完毕");
ctx.fireChannelReadComplete();
}
client :
如果这个 32*1024 设置的过小,就会接收不到那一条数据。 假设传过来的数据长度是10字节, 这是设置了8,那么这条数据就接受不到。
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(32*1024,buf));
public static void main(String[] args) throws InterruptedException {
//1. 创建两个线程组: 只需要一个线程组用于我们的实际处理(网络通信的读写)
EventLoopGroup workGroup = new NioEventLoopGroup();
//2 通过辅助类去构造server/client
Bootstrap b = new Bootstrap();
b.group(workGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
.option(ChannelOption.SO_RCVBUF, 1024 * 32)
.option(ChannelOption.SO_SNDBUF, 1024 * 32)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
//设置分隔符,一次性接受的最大长度是32*1024.这里是Client, 这个应该和SO_RCVBUF设置的一样。
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(32*1024,buf));
//设置字符串形式的解码。<传过来的数据直接转码成字符串>
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ClientHandler()); //1
}
});
//服务器端绑定端口并启动服务
//使用channel级别的监听close端口 阻塞的方式
ChannelFuture cf = b.connect("127.0.0.1", 8765).syncUninterruptibly();
// 开始写数据
// writeAndFlush 写入数据到缓冲并冲刷到channel
// Unpooled.copiedBuffer 把字节数组变成buff对象
for (int i = 1; i <= 10; i++) {
/**
* "hello" 占5个字节;“i” 0~9 占1个字节,10就占两个字节;"$_" 占两个字节; 总计8个字节~9个字节
* 所以,如果设置最长为8,那么最后一条9个字节长度的数据就接受不到
* ch.pipeline().addLast(new DelimiterBasedFrameDecoder(8,buf));
中文占两个字节
*/
//cf.channel().writeAndFlush(Unpooled.copiedBuffer(("hello netty 呀 ! "+i+" # $_ ").getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer(("hello "+i+"$_ ").getBytes()));
}
cf.channel().closeFuture().sync();
workGroup.shutdownGracefully();
}
clientHandler :
public class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.err.println("client channel active..");
}
/**
* 真正的数据最终会走到这个方法进行处理
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
String request = (String)msg;
System.out.println(" Client 接受到消息 : "+request );
// Response resp = (Response)msg;
// System.err.println("Client: " + resp.getId() + "," + resp.getName() + "," + resp.getResponseMessage());
} finally {
ReferenceCountUtil.release(msg);
//为什么服务端不需要做这一步释放的动作,因为服务端 writeAndFlush 的时候就释放了
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
【注意】
//设置分隔符,发送的最大长度是32*1024 这个应该和SO_SNDBUF设置的一样
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024*32,buf));
这个长度我们要预估下,大概有多长。
server :
假设我们这边1000条数据,990条数据量都是在 32 * 1024 左右大小。 少数极个别的 数据量特别大,可能有 34*1024 。那么这个值 就设置 32 * 1024 就可以了。那几条大数据,可以让客户端那边做好 两次 flush 处理 ,把数据压过来。
二 . 定长
// 设置定长字符串接收 5个字节长度
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
Client :
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
// 设置定长字符串接收
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
// 设置字符串形式的解码
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ServerHandler());
}
});
ClientHandler :
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String response = (String)msg;
System.out.println("Client: " + response);
}
Server :
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
// 设置定长字符串接收
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
// 设置字符串形式的解码
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ServerHandler());
}
});
ServerHandler :
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String request = (String)msg;
System.out.println("Server :" + msg);
String response = request ;
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
}
测试下:
首先看下 Client 的发送数据的代码
1 . 设置的定长是 5个字节(服务端设置的接受数据定长也是5个字节)
2. 发送的数据如下 aaaaabbbbb ccccccc c cc
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture cf = b.connect("127.0.0.1", 8765).sync();
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("aaaaabbbbb".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("ccccccc12".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("c4 5cc".getBytes()));
// 等待客户端端口关闭
cf.channel().closeFuture().sync();
group.shutdownGracefully();
最终服务端收到数据如下: 会自动把数据按照5个字节长度 分隔
Server :aaaaa
Server :bbbbb
Server :ccccc
Server :cc12c
Server :4 5cc