Netty进阶

Netty进阶

1. 粘包与半包

粘包与半包在TCP通信中是无法避免的现象,之前在学习NIO的过程中也遇到过黏包半包问题

  • 粘包:客户端多次发送数据,服务端一次接收到了所有数据(可能也是多次,但数据与发送时不匹配)
  • 半包:客户端一次发送较长数据,服务端分多次接收到该数据

1.1 粘包现象

创建一个客户端发送10次数据给客户端

服务端代码
public class QsNServer {
    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ChannelFuture future = new ServerBootstrap()
                    .group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        }
                    })
                    .bind(8080);
            future.sync();
            ChannelFuture closeFuture = future.channel().closeFuture();
            closeFuture.sync();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
客户端代码
public class QsNClient {
    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ChannelFuture future = new Bootstrap()
                    .group(worker)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                //此方法会在建立连接后执行
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    for (int i = 0; i < 10; i++) {
                                        ByteBuf buffer = ctx.alloc().buffer();
                                        buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
                                        ctx.writeAndFlush(buffer);
                                    }
                                }
                            });
                        }
                    })
                    .connect(new InetSocketAddress("localhost", 8080));
            future.sync();
            future.channel().closeFuture().sync();;
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            worker.shutdownGracefully();
        }
    }
}
测试

执行后发现,客户端是分10次发送数据

image-20230519160103298

但是服务端在接受的时候直接一起接受了,这就是所谓的粘包

image-20230519160129832

1.2 半包现象

一次性发送160字节的数据,看看服务端接收

服务端代码
public class QsBServer {
    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ChannelFuture future = new ServerBootstrap()
                    .option(ChannelOption.SO_RCVBUF,1) //设置接收缓冲区的大小,单位是字节,默认是1024
                    .group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        }
                    })
                    .bind(8080);
            future.sync();
            ChannelFuture closeFuture = future.channel().closeFuture();
            closeFuture.sync();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
客户端代码
public class QsBClient {
    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ChannelFuture future = new Bootstrap()
                    .group(worker)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                //此方法会在建立连接后执行
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    ByteBuf buffer = ctx.alloc().buffer();
                                    for (int i = 0; i < 10; i++) {
                                        buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
                                    }
                                    //一次发送160字节
                                    ctx.writeAndFlush(buffer);
                                }
                            });
                        }
                    })
                    .connect(new InetSocketAddress("localhost", 8080));
            future.sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
        }
    }
}
测试

多次测试,发现客户端收到并不是一次性收到的,这就是半包

image-20230519161959051

1.3 现象分析

本质是因为 TCP 是流式协议,消息无边界

粘包

  • 现象,发送 abc def,接收 abcdef
  • 原因
    • 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
    • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
    • Nagle 算法:会造成粘包

半包

  • 现象,发送 abcdef,接收 abc def
  • 原因
    • 应用层:接收方 ByteBuf 小于实际发送数据量
    • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
    • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
滑动窗口
  • TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差

  • 为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值

    0051

  • 窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用

    • 图中深色的部分即要发送的数据,高亮的部分即窗口
    • 窗口内的数据才允许被发送,当应答未到达前,窗口必须停止滑动
    • 如果 1001~2000 这个段的数据 ack 回来了,窗口就可以向前滑动
    • 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收

1.4 解决方案

短链接

以粘包为例,需要发送10次数据,没发完一次就重新关闭连接,重新建立连接发送

修改客户端代码

public class Client1 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            send();
        }
    }

    public static void send(){
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ChannelFuture future = new Bootstrap()
                    .group(worker)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                //此方法会在建立连接后执行
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    ByteBuf buffer = ctx.alloc().buffer();
                                    buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
                                    ctx.writeAndFlush(buffer);
                                    //发送完就关闭channel
                                    ctx.close();
                                }
                            });
                        }
                    })
                    .connect(new InetSocketAddress("localhost", 8080));
            future.sync();
            future.channel().closeFuture().sync();;
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            worker.shutdownGracefully();
        }
    }
}

缺点

这种方法可以处理粘包,但是半包现象不太好处理

固定长度

服务端可以通过FixedLengthFrameDecoder来解码

添加服务端处理器,就可以按照自己设置的长度来解析收到的数据

image-20230519163225496

修改服务端代码

与客户端约定好,每条信息10个字节的长度

ch.pipeline().addLast(new FixedLengthFrameDecoder(10));

修改客户端代码

public class Client2 {
    public static void main(String[] args) {
        send();
    }

    public static void send() {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ChannelFuture future = new Bootstrap()
                    .group(worker)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                //此方法会在建立连接后执行
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    // 发送内容随机的数据包
                                    Random r = new Random();
                                    char c = 'a';
                                    ByteBuf buffer = ctx.alloc().buffer();
                                    for (int i = 0; i < 10; i++) {
                                        byte[] bytes = new byte[10];
                                        for (int j = 0; j < r.nextInt(10) + 1; j++) {
                                            bytes[j] = (byte) c;
                                        }
                                        c++;
                                        buffer.writeBytes(bytes);
                                    }
                                    ctx.writeAndFlush(buffer);
                                }
                            });
                        }
                    })
                    .connect(new InetSocketAddress("localhost", 8080));
            future.sync();
            future.channel().closeFuture().sync();
            ;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
        }
    }
}

测试

可以看到服务端是可以处理数据的,而且和客户端什么时候flush没有关系

image-20230519163829684

缺点

数据包的大小不好把握

  • 长度定的太大,浪费
  • 长度定的太小,对某些数据包又显得不够
固定分隔符

服务端可以通过LineBasedFrameDecoder来解码,他是根据\n来分割不同信息的。

初始化时需要设置最大长度,例如设置最大长度为1024,当读取1024个字符都没有读到\n分割符的话,就会报错。

插上一嘴,DelimiterBasedFrameDecoderLineBasedFrameDecoder基础上可以实现自定义分隔符

修改服务端代码

ch.pipeline().addLast(new LineBasedFrameDecoder(1024));

修改客户端代码

主要是修改发送信息的地方,在每条信息后面添加了\n

public class Client3 {
    public static void main(String[] args) {
        send();
    }

    public static void send() {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ChannelFuture future = new Bootstrap()
                    .group(worker)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                //此方法会在建立连接后执行
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    // 发送内容随机的数据包
                                    Random r = new Random();
                                    char c = 'a';
                                    ByteBuf buffer = ctx.alloc().buffer();
                                    for (int i = 0; i < 10; i++) {
                                        for (int j = 1; j <= r.nextInt(16)+1; j++) {
                                            buffer.writeByte((byte) c);
                                        }
                                        //ascii码10就是\n
                                        buffer.writeByte(10);
                                        c++;
                                    }
                                    ctx.writeAndFlush(buffer);
                                }
                            });
                        }
                    })
                    .connect(new InetSocketAddress("localhost", 8080));
            future.sync();
            future.channel().closeFuture().sync();
            ;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
        }
    }
}

缺点

处理字符数据比较合适,但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误

预设长度

服务端可以通过LengthFieldBasedFrameDecoder来解码,这个是目前比较流行的方法,小黄在工作中对接设备上传过来的信息时,他们的信息也是类似于这种格式

主要有以下几个关键属性

  • lengthFieldOffset:长度字段偏移量
  • lengthFieldLength:长度字段长度
  • lengthAdjustment:长度字段为基准,还有几个字节是内容
  • initialBytesToStrip:从头剥离几个字节

案例介绍

源码中有很多案例,挑几个来介绍一下,加深理解

lengthFieldOffset为0,也就是说这串字节从第0位开始记录的就是长度字段

lengthFieldLength为2,也就是说从0开始数2个字节是用来记录长度字段的

长度字段的值记录着正文的长度,例如下面记录的000C就是12个字节

lengthAdjustment为0,也就是说长度字段读完后,就开始是正文内容了

image-20230519165550042

以上设置不变

initialBytesToStrip设置为2,这是我们的过滤条件,解码时会忽略前2个字节,也就是长度字节,0的话代表不忽略,解码出来时带有长度字节信息的

image-20230519165939792

Demo

现在我们来做一个小demo,规定消息体如下

前4个字节代表消息长度

第5个字节代表版本

后面是内容

解码后要求版本+内容

  +--------+---------+----------------+      +---------+----------------+
  | Length | Version | Actual Content |----->| Version | Actual Content |
  | 12     | 1       | "HELLO, WORLD" |      | 1       | "HELLO, WORLD" |
  +--------+---------+----------------+      +---------+----------------+

代码

这次就不写客户端和服务端了,Netty为我们提供了一个比较便捷的测试类

public class Test {
    public static void main(String[] args) {
        //相当于服务端
        EmbeddedChannel channel = new EmbeddedChannel(
                new LengthFieldBasedFrameDecoder(1024,0,4,1,4),
                new LoggingHandler(LogLevel.DEBUG)
        );

        //客户端发送数据
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        convert(buffer,"hello,world");
        convert(buffer,"hi~~~");
        channel.writeInbound(buffer);
    }

    /**
     * 消息处理
     * @param buf
     * @param context 一条消息
     */
    public static void convert(ByteBuf buf,String context) {
        byte[] bytes = context.getBytes(Charset.defaultCharset());
        //int占4个字节,前四个字节表示内容的长度
        buf.writeInt(bytes.length);
        //版本号为1
        buf.writeByte(1);
        //内容
        buf.writeBytes(bytes);
    }
}

测试

最终得到了我们想要的消息格式

image-20230519171401676

2. 协议设计与解析

2.1 为什么需要协议?

TCP/IP 中消息传输基于流的方式,没有边界。

协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则

通俗来讲,就是为了通信双方可以将一串长文本转换成同样的语义。

2.2 redis 协议举例

redis是通过TCP来传输数据的,例如想实现set name YellowStar这串命令,它的协议如下

*3\r\n   #该条命令有几个词(每个命之间用回车+换行符隔开) \r\n
$3\r\n   #词的长度
set\r\n  #词
$4\r\n
name\r\n
$10\r\n
Yellowstar\r\n

通过以下代码像redis服务器发送一条指令

public class TestRedis {
    public static void main(String[] args) {
        NioEventLoopGroup group = new NioEventLoopGroup();
        byte[] line = new byte[]{13,10};
        try {
            ChannelFuture future = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LoggingHandler());
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                                // 会在连接 channel 建立成功后,会触发 active 事件
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    ByteBuf buf = ctx.alloc().buffer();
                                    buf.writeBytes("*3".getBytes());
                                    buf.writeBytes(line);
                                    buf.writeBytes("$3".getBytes());
                                    buf.writeBytes(line);
                                    buf.writeBytes("set".getBytes());
                                    buf.writeBytes(line);
                                    buf.writeBytes("$4".getBytes());
                                    buf.writeBytes(line);
                                    buf.writeBytes("name".getBytes());
                                    buf.writeBytes(line);
                                    buf.writeBytes("$10".getBytes());
                                    buf.writeBytes(line);
                                    buf.writeBytes("YellowStar".getBytes());
                                    buf.writeBytes(line);
                                    ctx.writeAndFlush(buf);
                                }
                            });
                        }
                    })
                    .connect(new InetSocketAddress("localhost", 6379)).sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            group.shutdownGracefully();
        }
    }
}

执行后代码,发现数据成功发送,并且redis响应了一个ok

image-20230522133227762

image-20230522133243809

2.3 http 协议举例

其实Netty支持很多种常见协议的,对http协议也做了支持,可以调用HttpServerCodec实现解码和编码

@Slf4j
public class TestHttp {
    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        byte[] line = new byte[]{13,10};
        try {
            ChannelFuture future = new ServerBootstrap()
                    .group(boss,worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LoggingHandler());
                            //解析http协议
                            ch.pipeline().addLast(new HttpServerCodec());
                            //响应请求
                            ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
                                @Override
                                protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
                                    log.debug("收到请求:{}",msg.getUri());
                                    //响应 第一个参数http协议版本,与发送版本一致,第二个参数响应状态码
                                    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);
                                }
                            });
                        }
                    })
                    .bind(8080).sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

使用浏览器发送http://localhost:8080/index.html,服务端会解析成以下格式,包括请求头请求体之类的

image-20230522134018389

并且浏览器也会得到响应结果

image-20230522134732107

2.4 自定义协议

要素
  • 魔数,用来在第一时间判定是否是无效数据包
  • 版本号,可以支持协议的升级
  • 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk
  • 指令类型,是登录、注册、单聊、群聊… 跟业务相关
  • 请求序号,为了双工通信,提供异步能力
  • 正文长度
  • 消息正文
约定

根据上面的要素,设计一个登录请求消息和登录响应消息

约定指的是通信双方对协议的约定,比如现在的约定如下

  • 魔数,4个字节,使用1,2,3,4
  • 版本号,1个字节,目前就是1
  • 序列化算法,1个字节,0代表jdk,1代表json
  • 指令类型,1个字节,0代表登录
  • 请求序号,4个字节,目前就是0
  • 正文长度,4个字节
  • 消息正文
编解码器

根据上面的约定,实现一个编解码器

准备Message抽象类

之后不同的消息类型统一继承该类即可

@Data
public abstract class Message implements Serializable {

    /**
     * 根据消息类型字节,获得对应的消息 class
     * @param messageType 消息类型字节
     * @return 消息 class
     */
    public static Class<? extends Message> getMessageClass(int messageType) {
        return messageClasses.get(messageType);
    }

    private int sequenceId;

    private int messageType;

    public abstract int getMessageType();

    public static final int LoginRequestMessage = 0;
    public static final int LoginResponseMessage = 1;
    public static final int ChatRequestMessage = 2;
    public static final int ChatResponseMessage = 3;
    public static final int GroupCreateRequestMessage = 4;
    public static final int GroupCreateResponseMessage = 5;
    public static final int GroupJoinRequestMessage = 6;
    public static final int GroupJoinResponseMessage = 7;
    public static final int GroupQuitRequestMessage = 8;
    public static final int GroupQuitResponseMessage = 9;
    public static final int GroupChatRequestMessage = 10;
    public static final int GroupChatResponseMessage = 11;
    public static final int GroupMembersRequestMessage = 12;
    public static final int GroupMembersResponseMessage = 13;
    public static final int PingMessage = 14;
    public static final int PongMessage = 15;
    /**
     * 请求类型 byte 值
     */
    public static final int RPC_MESSAGE_TYPE_REQUEST = 101;
    /**
     * 响应类型 byte 值
     */
    public static final int  RPC_MESSAGE_TYPE_RESPONSE = 102;

    private static final Map<Integer, Class<? extends Message>> messageClasses = new HashMap<>();

    static {
        messageClasses.put(LoginRequestMessage, LoginRequestMessage.class);
        messageClasses.put(LoginResponseMessage, LoginResponseMessage.class);
        messageClasses.put(ChatRequestMessage, ChatRequestMessage.class);
        messageClasses.put(ChatResponseMessage, ChatResponseMessage.class);
        messageClasses.put(GroupCreateRequestMessage, GroupCreateRequestMessage.class);
        messageClasses.put(GroupCreateResponseMessage, GroupCreateResponseMessage.class);
        messageClasses.put(GroupJoinRequestMessage, GroupJoinRequestMessage.class);
        messageClasses.put(GroupJoinResponseMessage, GroupJoinResponseMessage.class);
        messageClasses.put(GroupQuitRequestMessage, GroupQuitRequestMessage.class);
        messageClasses.put(GroupQuitResponseMessage, GroupQuitResponseMessage.class);
        messageClasses.put(GroupChatRequestMessage, GroupChatRequestMessage.class);
        messageClasses.put(GroupChatResponseMessage, GroupChatResponseMessage.class);
        messageClasses.put(GroupMembersRequestMessage, GroupMembersRequestMessage.class);
        messageClasses.put(GroupMembersResponseMessage, GroupMembersResponseMessage.class);
        messageClasses.put(RPC_MESSAGE_TYPE_REQUEST, RpcRequestMessage.class);
        messageClasses.put(RPC_MESSAGE_TYPE_RESPONSE, RpcResponseMessage.class);
    }
}

解码器

@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        // 1. 4个字节的魔数
        out.writeBytes(new byte[]{1,2,3,4});
        // 2. 1个字节的版本号
        out.writeByte(1);
        // 3. 1个字节的序列化算法,目前使用jdk
        out.writeByte(0);
        // 4. 1个字节的指令类型
        out.writeByte(msg.getMessageType());
        // 5. 4个字节的请求序号
        out.writeInt(msg.getSequenceId());
        // 6. 补全字节,一般来说都是2的次方
        out.writeByte(0xff);
        //序列化消息内容
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(msg);
        byte[] bytes = baos.toByteArray();
        // 7. 4个字节的正文长度
        out.writeInt(bytes.length);
        // 8. 正文
        out.writeBytes(bytes);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 1. 魔数
        int magicNumber = in.readInt();
        // 2. 版本号
        byte version = in.readByte();
        // 3. 序列化算法
        byte serializable = in.readByte();
        // 4. 指令类型
        byte messageType = in.readByte();
        // 5. 请求序号
        int sequence = in.readInt();
        // 6. 补全
        in.readByte();
        // 7. 正文长度
        int length = in.readInt();
        // 8. 正文
        byte[] bytes = new byte[length];
        in.readBytes(bytes,0,length);
        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) ois.readObject();
        log.debug("{},{},{},{},{},{}",magicNumber,version,serializable,messageType,sequence,length);
        log.debug("正文:{}",message);
        //将正文交给下一个handler
        out.add(message);
    }
}
测试

测试编码

public class TestCodec {
    public static void main(String[] args) {
        EmbeddedChannel channel = new EmbeddedChannel(
                new LoggingHandler(LogLevel.DEBUG),
                new MessageCodec()
        );
        //encode
        LoginRequestMessage message = new LoginRequestMessage("YellowStar", "123456");
        channel.writeOutbound(message);
    }
}

通过输出观察

image-20230522143514240

测试解码

public class TestCodec {
    public static void main(String[] args) throws Exception {
        EmbeddedChannel channel = new EmbeddedChannel(
                new LoggingHandler(LogLevel.DEBUG),
                new MessageCodec()
        );
        //encode
        LoginRequestMessage message = new LoginRequestMessage("YellowStar", "123456");

        //decode
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        new MessageCodec().encode(null,message,buf); //编码
        channel.writeInbound(buf);
    }
}

通过日志可以看到,消息被正常翻译过来

image-20230522144150356

解决粘包、半包
public class TestCodec {
    public static void main(String[] args) throws Exception {
        EmbeddedChannel channel = new EmbeddedChannel(
                new LoggingHandler(LogLevel.DEBUG),
            	//通过LengthFieldBasedFrameDecoder来先一步处理
                new LengthFieldBasedFrameDecoder(1024,12,4,0,0),
                new MessageCodec()
        );
        LoginRequestMessage message = new LoginRequestMessage("YellowStar", "123456");

        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        new MessageCodec().encode(null,message,buf); //编码

        //模拟半包
        //零拷贝
        ByteBuf b1 = buf.slice(0, 100);
        ByteBuf b2 = buf.slice(100, buf.readableBytes() - 100);
        buf.retain(); //让计数器+1
        channel.writeInbound(b1); // 会执行release()方法,计数器-1
        channel.writeInbound(b2);
    }
}
@Sharable
  • 当 handler 不保存状态时,就可以安全地在多线程下被共享
  • 但要注意对于编解码器类,不能继承 ByteToMessageCodec 或 CombinedChannelDuplexHandler 父类,他们的构造方法对 @Sharable 有限制
  • 如果能确保编解码器不会保存状态,可以继承 MessageToMessageCodec 父类

对于上述的解码器,不用保存状态,可以在多线程下被共享,一般都会将它提取出来,不用每次进去new一个新的,浪费内存

修改编码器

继承MessageToMessageCodec即可,MessageToMessageCodec默认接收到的是一个完整的信息

@Slf4j
@ChannelHandler.Sharable
public class MessageSharableCodec extends MessageToMessageCodec<ByteBuf,Message> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> list) throws Exception {
        ByteBuf out = ByteBufAllocator.DEFAULT.buffer();
        // 1. 4个字节的魔数
        out.writeBytes(new byte[]{1,2,3,4});
        // 2. 1个字节的版本号
        out.writeByte(1);
        // 3. 1个字节的序列化算法,目前使用jdk
        out.writeByte(0);
        // 4. 1个字节的指令类型
        out.writeByte(msg.getMessageType());
        // 5. 4个字节的请求序号
        out.writeInt(msg.getSequenceId());
        // 6. 补全字节,一般来说都是2的次方
        out.writeByte(0xff);
        //序列化消息内容
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(msg);
        byte[] bytes = baos.toByteArray();
        // 7. 4个字节的正文长度
        out.writeInt(bytes.length);
        // 8. 正文
        out.writeBytes(bytes);
        //将内容传递到下一个handler
        list.add(out);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 1. 魔数
        int magicNumber = in.readInt();
        // 2. 版本号
        byte version = in.readByte();
        // 3. 序列化算法
        byte serializable = in.readByte();
        // 4. 指令类型
        byte messageType = in.readByte();
        // 5. 请求序号
        int sequence = in.readInt();
        // 6. 补全
        in.readByte();
        // 7. 正文长度
        int length = in.readInt();
        // 8. 正文
        byte[] bytes = new byte[length];
        in.readBytes(bytes,0,length);
        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) ois.readObject();
        log.debug("{},{},{},{},{},{}",magicNumber,version,serializable,messageType,sequence,length);
        log.debug("正文:{}",message);
        //将正文交给下一个handler
        out.add(message);
    }
}

3. 聊天室案例

聊天室具体代码,过于繁杂,小黄放在gitee上,有需要的同学自行查看 点击下载

3.1 空闲等待

连接假死

原因

  • 网络设备出现故障,例如网卡,机房等,底层的 TCP 连接已经断开了,但应用程序没有感知到,仍然占用着资源。
  • 公网网络不稳定,出现丢包。如果连续出现丢包,这时现象就是客户端数据发不出去,服务端也一直收不到数据,就这么一直耗着
  • 应用程序线程阻塞,无法进行数据读写

问题

  • 假死的连接占用的资源不能自动释放
  • 向假死的连接发送数据,得到的反馈是发送超时

服务器端解决

  • 怎么判断客户端连接是否假死呢?如果能收到客户端数据,说明没有假死。因此策略就可以定为,每隔一段时间就检查这段时间内是否接收到客户端数据,没有就可以判定为连接假死
//5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(5,0,0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler(){
    // 用来触发特殊事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        IdleStateEvent event = (IdleStateEvent) evt;
        //触发读空闲事件
        if (event.state() == IdleState.READER_IDLE) {
            log.debug("已经5秒没有获取到数据了");
            ctx.channel().close();
        }
    }
});

客户端定时心跳

  • 客户端可以定时向服务器端发送数据,只要这个时间间隔小于服务器定义的空闲检测的时间间隔,那么就能防止前面提到的误判,客户端可以定义如下心跳处理器
//每隔3秒触发一次IdleState#WRITER_IDLE事件
ch.pipeline().addLast(new IdleStateHandler(0,3,0));
ch.pipeline().addLast(new ChannelDuplexHandler(){
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        IdleStateEvent event = (IdleStateEvent) evt;
        //触发空闲写事件
        if (event.state() == IdleState.WRITER_IDLE) {
            log.debug("3s 没有写数据了,发送一个心跳包");
            ctx.writeAndFlush(new PingMessage());
        }
    }
});
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值