本章节主要介绍粘包半包的解决方法、协议的设计、序列化知识;同时通过实现聊天室案例将这些知识点串联起来。

3.1 粘包半包

  • 粘包半包现象
  • 粘包:多条数据粘连,一次发送给服务器
  • 半包:一条完整消息从某个点断开,发送给服务器的消息不完整

演示代码

//服务器演示代码
public void start(){
    //声明工作线程及主线程
    NioEventLoopGroup boss = new NioEventLoopGroup(1);
    NioEventLoopGroup worker = new NioEventLoopGroup();
    try {
        //启动配置
        ServerBootstrap server = new ServerBootstrap();
        server.channel(NioServerSocketChannel.class); //非阻塞
        //调整系统的接收缓冲区
        server.option(ChannelOption.SO_RCVBUF,10); //设置接收缓冲区
        server.group(boss,worker);
        server.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel sc) throws Exception {
                sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
            }
        });
        ChannelFuture future = server.bind(8080).sync();
        future.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.error("服务器启动错误:{}",e.getMessage());
    } finally {
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }
}
//客户端演示代码
static void send(){
    NioEventLoopGroup worker = new NioEventLoopGroup();
    try {
        Bootstrap client = new Bootstrap();
        client.channel(NioSocketChannel.class);
        client.group(worker);
        client.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel sc) throws Exception {
                sc.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        for (int i = 0; i < 10; i++) {
                            ByteBuf buf = ctx.alloc().buffer(16);
                            buf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
                            ctx.writeAndFlush(buf);
                        }
					}
                });
            }
        });
        ChannelFuture future = client.connect("127.0.0.1", 8080).sync();
        future.channel().closeFuture().sync();
    }catch (Exception e){
        log.error("客户端错误:{}",e.getMessage());
    }finally {
        worker.shutdownGracefully();
    }
}

日志说明

| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
±-------±------------------------------------------------±---------------+
 |00000000| 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 |…|
 |00000010| 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 |…|
 |00000020| 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 |…|
 |00000030| 06 07 |… |
 ±-------±------------------------------------------------±---------------+

通过日志可以看出,服务器前三次接收的属于粘包现象,最后一次属于半包现象。

  • 粘包半包的解决方案
  • 短链接
//客户端
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    ByteBuf buf = ctx.alloc().buffer(16);
    buf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
    ctx.writeAndFlush(buf);
    ctx.channel().close(); //发完消息接断开连接,可以解决粘包,不能处理半包
}
//服务器,模拟半包现象
//调整系统的接收缓冲区(滑动窗口)
//server.option(ChannelOption.SO_RCVBUF,10); //设置接收缓冲区
//调整netty的接收缓冲区(ByteBuf)
server.childOption(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16));
  • 定长解码器
//服务器端
protected void initChannel(SocketChannel sc) throws Exception {
    //添加定长解吗器,位于最上端
    sc.pipeline().addLast(new FixedLengthFrameDecoder(10)); 
    sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
//客户端,fill10Bytes不足的补全10个字节
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    ByteBuf buf = ctx.alloc().buffer();
    char c = '0';
    Random r = new Random();
    for (int i = 0; i < 10; i++) {
        byte[] bytes = fill10Bytes(c,r.nextInt(10) + 1);
        c++;
        buf.writeBytes(bytes);
    }
    ctx.writeAndFlush(buf);
}
//fill10Bytes()方法代码
public static byte[] fill10Bytes(char c ,int len){
    byte[] bytes = new byte[10];
    Arrays.fill(bytes,(byte) '_');
    for (int i = 0; i < len; i++) {
        bytes[i] = (byte)c;
    }
    System.out.println("内容:" + new String(bytes));
    return bytes;
}
  • 行解码器
//服务器代码
protected void initChannel(SocketChannel sc) throws Exception {
    sc.pipeline().addLast(new LineBasedFrameDecoder(1024)); //添加行解吗器
    sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
//客户端代码,makeString给消息添加分隔符‘\n’
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    ByteBuf buf = ctx.alloc().buffer();
    char c = '0';
    Random r = new Random();
    for (int i = 0; i < 10; i++) {
        byte[] bytes = makeString(c,r.nextInt(256) + 1);
        c++;
        buf.writeBytes(bytes);
    }
    ctx.writeAndFlush(buf);
}
//makeString()方法代码
public static byte[] makeString(char c ,int len){
    StringBuilder sb = new StringBuilder(len + 2);
    for (int i = 0; i < len; i++) {
        sb.append(c);
    }
    sb.append("\n");
    return sb.toString().getBytes();
}
  • LTC解码器
public static void main(String[] args) {
        EmbeddedChannel channel = new EmbeddedChannel(
                new LengthFieldBasedFrameDecoder(1024,0,4,1,4),
                new LoggingHandler(LogLevel.DEBUG)
        );
        //4个字节的内容长度,实际长度
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        send(buf,"hello, world");
        send(buf,"Hi");
        channel.writeInbound(buf);

    }

    private static void send(ByteBuf buf,String content){
        byte[] bytes = content.getBytes(); //实际内容
        int len = bytes.length; //实际内容长度
        buf.writeInt(len); //写入内容长度
        buf.writeByte(1); //写入任意数据
        buf.writeBytes(bytes); //写入实际内容
    }

注解:

  • maxFrameLength:发送的数据帧最大长度
  • lengthFieldOffset:定义长度域位于发送的字节数组中的下标
  • lengthFieldLength:用于描述定义的长度域的长度
  • lengthAdjustment:自长度域以后几个字节为内容域
  • initialBytesToStrip:接收到的发送数据包,去除前initialBytesToStrip位

3.2 协议的制定及解析

  • Redis
final byte[] LINE = {13,10};
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
    Bootstrap client = new Bootstrap();
    client.channel(NioSocketChannel.class);
    client.group(worker);
    client.handler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel sc) throws Exception {
            sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
            sc.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                @Override
                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                    //set name zhangsan
                    ByteBuf buf = ctx.alloc().buffer();
                    buf.writeBytes("*3".getBytes()); //命令数组长度
                    buf.writeBytes(LINE); //回车换行
                    buf.writeBytes("$3".getBytes()); //命令字段长度 set
                    buf.writeBytes(LINE);
                    buf.writeBytes("set".getBytes()); //指令 => set
                    buf.writeBytes(LINE);
                    buf.writeBytes("$4".getBytes()); //key字段长度 name
                    buf.writeBytes(LINE);
                    buf.writeBytes("name".getBytes()); //key => name
                    buf.writeBytes(LINE);
                    buf.writeBytes("$8".getBytes()); //value字段长度 zhangsan
                    buf.writeBytes(LINE);
                    buf.writeBytes("zhangsan".getBytes()); //value => zhangsan
                    buf.writeBytes(LINE);
                    ctx.writeAndFlush(buf); //写入并发送
                }

                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                    ByteBuf buf = (ByteBuf) msg;
                    System.out.println(buf.toString(Charset.defaultCharset()));
                }
            });
        }
    });
    ChannelFuture future = client.connect("127.0.0.1", 6379).sync();
    future.channel().closeFuture().sync();
}catch (Exception e){
    log.error("客户端错误:{}",e.getMessage());
}finally {
    worker.shutdownGracefully();
}
  • Http
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
    ServerBootstrap client = new ServerBootstrap();
    client.channel(NioServerSocketChannel.class);
    client.group(boss,worker);
    client.childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel sc) throws Exception {
            sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
            sc.pipeline().addLast(new HttpServerCodec());
            sc.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>(){

                @Override
                protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
                    //获取请求
                    log.info(msg.uri());
                    //返回响应
                    DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
                    byte[] bytes = "<h1>Hello, World!</h1>".getBytes();
                    response.headers().setInt(CONTENT_LENGTH,bytes.length);
                    response.content().writeBytes(bytes);
                    //写回响应
                    ctx.writeAndFlush(response);
                }
            });
        }
    });
    ChannelFuture future = client.bind("127.0.0.1", 8080).sync();
    future.channel().closeFuture().sync();
}catch (Exception e){
    log.error("客户端错误:{}",e.getMessage());
}finally {
    boss.shutdownGracefully();
    worker.shutdownGracefully();
}
  • 自定义协议
  • 要素
  • 魔数:用来在第一时间判定是否是无效的数据包
  • 版本号:可以支持协议的升级
  • 序列化算法:消息正文到底采用哪种序列化方式,可以由此扩展,例如JSON\protobuf\jdk
  • 指令类型:是登录、注册…与业务相关
  • 请求序号:为了双工通信,提供异步能力
  • 正文长度:传递数据的长度
  • 消息正文:传递的数据
  • 编码
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
    //4字节的魔数
    out.writeBytes(new byte[]{1,2,3,4});
    //1字节的版本
    out.writeByte(1);
    //1字节的序列化方式0:jdk;1:json
    out.writeByte(0);
    //1字节的指令类型
    out.writeByte(msg.getMessageType());
    //4字节序列号
    out.writeByte(msg.getSequenceId());
    //对齐填充
    out.writeByte(0xff);
    //获取字节的内容数组
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(msg);
    byte[] bytes = bos.toByteArray();
    //内容长度
    out.writeInt(bytes.length);
    //内容写入
    out.writeBytes(bytes);
}
  • 解码
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    int magicNum = in.readInt();
    byte version = in.readByte();
    byte serializerType = in.readByte();
    byte messageType = in.readByte();
    int sequenceId = in.readInt();
    in.readByte();
    int length = in.readInt();
    byte[] bytes = new byte[length];
    in.readBytes(bytes,0,length);
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
    Message message = (Message) ois.readObject();
    log.debug("{}, {}, {}, {}, {}, {}", magicNum, version, serializerType, messageType, sequenceId, length);
    log.debug("{}", message);
    out.add(message);
}
  • 测试
EmbeddedChannel channel = new EmbeddedChannel(
    //解决粘包半包问题
    new LengthFieldBasedFrameDecoder(1024,12,4,0,0),
    new LoggingHandler(LogLevel.DEBUG),
    new MessageCodec()
);

LoginRequestMessage request =
    new LoginRequestMessage("zhangsan","123","张三");
//出站
channel.writeOutbound(request);

ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null,request,buf);

//模拟半包现象
ByteBuf s1 = buf.slice(0, 100);
ByteBuf s2 = buf.slice(100,buf.readableBytes() - 100);
//入站
s1.retain(); //防止内存释放
channel.writeInbound(s1);
channel.writeInbound(s2);