1 TCP 粘包拆包分析
从两个角度看粘包和拆包:
1 收发角度:一个发送可能被多次接收(半包),多个发送可能被一次接收(粘包)
2 传输角度:一个发送可能占用多个传输包(半包),多个发送可能公用一个传输包(粘包)
根本原因:
TCP 协议是面向连接的、可靠的、基于字节流的传输层 通信协议,是一种流式协议,消息无边界。
2 Netty 解决 粘包拆包
1 TCP连接变成短连接,一个请求一个 短连接,效率,性能低下,不推荐。
2 固定长度,长度不好定义,会造 成空间浪费,不推荐。
3 分隔符,消息内容本身也有分 隔符时需要转义,故 需要扫描消息内容,推荐。
4 固定长度消息头+变长消息体,理论上长度有限制, 需要提前预知可能的 最大长度从而定义消 息内容占用的字节数,推荐+。
3 LengthFieldBasedFrameDecoder 解码器
防止TCP黏包、半包,可以用LengthFieldBasedFrameDecoder 解码器对发送消息进行解码,核心参数如下:
maxFrameLength: 发送的数据包最大长度
lengthFieldOffset: length域的偏移,正常情况下读 取数据从偏移为0处开始读取,如果有需要可以从其他偏移量处开始读取
lengthFieldLength: length域占用的字节数
lengthAdjustment:在length域和content域中间是 否需要填充其他字节数
initialBytesToStrip:解码后跳过的字节数 ( 解码后把 length占用的字节跳过,直接传数据包)
4 demo
4.1 服务端代码
@Slf4j
public class NettyServer {
public static void main(String[] args) {
NettyServer nettyServer = new NettyServer();
nettyServer.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();
}
}
}
/**
* @Description:
* @Author: rosh
* @Date: 2022/6/9 22:15
*/
@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));
}
}
4.2 客户端代码
@Slf4j
public class NettyClient {
public static void main(String[] args) {
NettyClient client = new NettyClient();
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();
//添加编码器
//参数,固定的header是多少
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();
}
}
}
@Slf4j
public class ClientInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 1; i <= 100; i++) {
UserInfo userInfo = new UserInfo(i, "rosh" + i);
byte[] bytes = JSON.toJSONString(userInfo).getBytes(StandardCharsets.UTF_8);
ByteBuf byteBuf = ctx.alloc().buffer().writeBytes(bytes);
ctx.channel().writeAndFlush(byteBuf);
}
super.channelActive(ctx);
}
}