tcp粘包拆包问题
客户端像服务端发送消息,服务端不知道客户端每次发送消息的数据大小,服务端可能出现把一个数据包拆成两个数据包进行读取这种被称为拆包,也有可能把两个数据包当成一个数据包读取这种被称为粘包
如下图所示,客户端像服务端发送了两个数据包dataA和dataB,但服务端实际收到可能有四种情况
- 一次性读到dataA+dataB,这里就是粘包了
- 服务端读到两个数据包,分别为dataA_1和dataA_2+dataB,dataA先发送一部分 剩下一部分跟随dataB一起发送,该情况称为拆包
- 服务端读到两个数据包,分别为dataA+dataB_1和dataB_2,dataA发送的时候顺带把dataB也发送了一部分,dataB剩下一部分单独发送,和2一样也是拆包
- 服务端读到两个数据包,,分别为dataA和dataB 正常情况
解决办法:自定义数据协议,让服务器知道客户端每次发送的数据大小为多少,这样服务端就会按照客户端定义的数据大小来读取了
先定义消息协议
/** * 自定义消息协议 */ @Data @Accessors(chain = true) public class MessageProtocol { /** * 消息的长度 */ private int length; /** * 消息的内容 */ private byte[] content; }
定义自定义解码器(inboundHandler)和自定义编码器(outboundHandler)
解码器:需要继承ByteToMessageDecoder并重写decode方法,解码器是需要放在pipeline的前面,后续的自定义的逻辑inbound需放在解码器后面,读取数据顺序是 先解码后处理
ByteBuf: netty自己的buffer,类似于nio中的ByteBuffer,功能还是一样的
out: 我们看它的数据接口为List<Object>,后续通过循环它 把数据挨个向后传递,如果out.add(messageProtocol) 操作两次,那么后续的handler就会读取到两次
public class MessageDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { // 切记这里不能用in.readableBytes() 来获取接收的长度 这里返回的是ByteBuf中数组可读的长度writerIndex - readerIndex 但不代表实际数据传来的长度 猜想:ByteBuf初始化默认writerIndex就很大 int length = in.readInt(); byte[] content = new byte[length]; in.readBytes(content); MessageProtocol messageProtocol = new MessageProtocol().setLength(length).setContent(content); out.add(messageProtocol); } }
编码器:需要继承MessageToByteEncoder<T>指定数据类型枚举 并重写encode方法, 前面讲过出站是从后向前执行的,所以我们需要把编码器放到自定义handler的前面 这样才能进行编码
public class MessageEncoder extends MessageToByteEncoder<MessageProtocol> { @Override protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception { out.writeInt(msg.getLength()); out.writeBytes(msg.getContent()); } }
解码器和编码器的添加顺序,先添加解码器 再添加编码器 我们自定义处理的handler放在后面
.handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new MessageDecoder()) .addLast(new MessageEncoder()) .addLast(new ClientChannelHandler()); } });
下面通过一个案例来演示一下
需求:分别启动一个服务端和客户端,客户端连接到服务端后 像服务端发送消息,服务端进行读取后回写一个UUID给客户端
服务端:
public class TcpServer { public static void main(String[] args) { // 用来处理连接事件 EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 用来处理具体io事件 默认线程数为CPU核心*2 EventLoopGroup workGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap() .group(bossGroup, workGroup) // 加入事件组 .channel(NioServerSocketChannel.class) // 指定服务端通道类型 .option(ChannelOption.SO_BACKLOG, 100) // 设置线程队列得到的连接数 .childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态 .handler(new LoggingHandler(LogLevel.INFO)) // 给ServerSocketChannel用的 对应bossgroup .childHandler(new ChannelInitializer<SocketChannel>() { // 对应workgroup @Override protected void initChannel(SocketChannel ch) throws Exception { // 给SocketChannel添加处理器 ch.pipeline().addLast(new MessageDecoder()) .addLast(new MessageEncoder()) .addLast(new ServerChannelHandler()); } }); // 绑定端口不进行阻塞 这里通过异步来操作 会返回一个异步执行结果 ChannelFuture cf = serverBootstrap.bind(8080).sync(); // 监听关闭事件 cf.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } }
服务端handler:
public class ServerChannelHandler extends SimpleChannelInboundHandler<MessageProtocol> { @Override protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception { System.out.println(String.format("服务器收到消息,长度:%d,msg:%s", msg.getLength(), new String(msg.getContent()))); // 回写给客户端 UUID uuid = UUID.randomUUID(); byte[] bytes = uuid.toString().getBytes(); MessageProtocol response = new MessageProtocol().setLength(bytes.length).setContent(bytes); ctx.writeAndFlush(response); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
客户端:
public class TcpClient { public static void main(String[] args) { EventLoopGroup clientGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap().group(clientGroup) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new MessageDecoder()) .addLast(new MessageEncoder()) .addLast(new ClientChannelHandler()); } }); ChannelFuture cf = bootstrap.connect("127.0.0.1", 8080).sync(); cf.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { clientGroup.shutdownGracefully(); } } }
客户端handler:
public class ClientChannelHandler extends SimpleChannelInboundHandler<MessageProtocol> { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { for (int i = 0; i < 5 ; i++) { String msg = "你好啊,服务器,我正在向你发送消息:"+i; MessageProtocol messageProtocol = new MessageProtocol().setLength(msg.getBytes().length).setContent(msg.getBytes()); ctx.writeAndFlush(messageProtocol); } } @Override protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception { System.out.println(String.format("客户端收到消息,长度:%d,msg:%s", msg.getLength(), new String(msg.getContent()))); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
服务端收到的消息:
客户端收到的消息:
下面再给大家演示下不采用自定义消息协议的情况
服务端:一共分四次接受
客户端:分一次接受