• Netty(8):协议设计与解析


    为什么需要协议?

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

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

    例如:在网络上传输

    下雨天留客天留我不留
    

    是中文一句著名的无标点符号句子,在没有标点符号情况下,这句话有数种拆解方式,而意思却是完全不同,所以常被用作讲述标点符号的重要性

    一种解读

    下雨天留客,天留,我不留
    

    另一种解读

    下雨天,留客天,留我不?留
    

    如何设计协议呢?其实就是给网络传输的信息加上“标点符号”。但通过分隔符来断句不是很好,因为分隔符本身如果用于传输,那么必须加以区分。因此,下面一种协议较为常用

    定长字节表示内容长度 + 实际内容
    

    例如,假设一个中文字符长度为 3,按照上述协议的规则,发送信息方式如下,就不会被接收方弄错意思了

    0f下雨天留客06天留09我不留
    

    redis 协议举例

    redis协议可看,这里不展开:https://redis.com.cn/topics/protocol.html

    我们想要发送set name abc到redis,需要发送如下的数据包:

    *3
    $3
    set
    $4
    name
    $3
    abc
    
    

    java代码:

        public static void main(String[] args) {
            NioEventLoopGroup worker = new NioEventLoopGroup();
            byte[] LINE = {13, 10};
            try {
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.channel(NioSocketChannel.class);
                bootstrap.group(worker);
                bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new LoggingHandler());
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            // 会在连接 channel 建立成功后,会触发 active 事件
                            @Override
                            public void channelActive(ChannelHandlerContext ctx) {
                                set(ctx);
                            }
                            private void set(ChannelHandlerContext ctx) {
                                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("$3".getBytes());
                                buf.writeBytes(LINE);
                                buf.writeBytes("abc".getBytes());
                                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 channelFuture = bootstrap.connect("192.168.40.128", 6379).sync();
                channelFuture.channel().closeFuture().sync();
            } catch (InterruptedException e) {
            } finally {
                worker.shutdownGracefully();
            }
        }
    

    image-20220523151245678

    Http协议

    java代码:

        public static void main(String[] args) {
            NioEventLoopGroup boss = new NioEventLoopGroup(1);
            NioEventLoopGroup worker = new NioEventLoopGroup();
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.channel(NioServerSocketChannel.class);
                serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
                serverBootstrap.group(boss, worker);
                serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        ch.pipeline().addLast(new HttpServerCodec());
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
                                //返回响应
                                DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
                                String content = "<h1>hello world</h1>";
                                response.headers().setInt(CONTENT_LENGTH, content.length());
                                response.content().writeBytes(content.getBytes());
                                ctx.writeAndFlush(response);
                            }
                        });
                    }
                });
                ChannelFuture channelFuture = serverBootstrap.bind(8080);
                channelFuture.sync();
                channelFuture.channel().closeFuture().sync();
            } catch (InterruptedException e) {
            } finally {
                boss.shutdownGracefully();
                worker.shutdownGracefully();
            }
        }
    

    浏览器访问 localhost:8080:

    image-20220523162545076

    控制台输出:

    image-20220523162632503

    自定义协议要素

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

    编解码器

    根据上面的要素,设计一个登录请求消息和登录响应消息,并使用 Netty 完成收发:

    public class MessageCodec extends ByteToMessageCodec<Message> {
    
        Logger logger = LoggerFactory.getLogger(MessageCodec.class);
    
        @Override
        public 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 0 , json 1
            out.writeByte(0);
            // 4. 1 字节的指令类型
            out.writeByte(msg.getMessageType());
            // 5. 4 个字节
            out.writeInt(msg.getSequenceId());
            // 无意义,对齐填充
            out.writeByte(0xff);
            // 6. 获取内容的字节数组
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(msg);
            byte[] bytes = bos.toByteArray();
            // 7. 长度
            out.writeInt(bytes.length);
            // 8. 写入内容
            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();
            System.out.printf("%s, %s, %s, %s, %s, %s\n", magicNum, version, serializerType, messageType, sequenceId, length);
            System.out.printf("%s\n", message);
            out.add(message);
        }
    }
    

    测试:

            EmbeddedChannel channel = new EmbeddedChannel(
                    new LoggingHandler(),
                    new LengthFieldBasedFrameDecoder(
                            1024, 12, 4, 0, 0),
                    new MessageCodec()
            );
            LoginRequestMessage message = new LoginRequestMessage("zhangsan", "张三");
            ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
            new MessageCodec().encode(null, message, buf);
            channel.writeInbound(buf);
    

    测试结果:

    image-20220523175506393

    什么时候可以加 @Sharable

    • 当 handler 不保存状态时,就可以安全地在多线程下被共享
    • 但要注意对于编解码器类,不能继承 ByteToMessageCodecCombinedChannelDuplexHandler 父类,他们的构造方法对 @Sharable 有限制
    • 如果能确保编解码器不会保存状态,可以继承 MessageToMessageCodec 父类
    @Slf4j
    @ChannelHandler.Sharable
    /**
     * 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的
     */
    public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> {
        @Override
        protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception {
            ByteBuf out = ctx.alloc().buffer();
            // 1. 4 字节的魔数
            out.writeBytes(new byte[]{1, 2, 3, 4});
            // 2. 1 字节的版本,
            out.writeByte(1);
            // 3. 1 字节的序列化方式 jdk 0 , json 1
            out.writeByte(0);
            // 4. 1 字节的指令类型
            out.writeByte(msg.getMessageType());
            // 5. 4 个字节
            out.writeInt(msg.getSequenceId());
            // 无意义,对齐填充
            out.writeByte(0xff);
            // 6. 获取内容的字节数组
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(msg);
            byte[] bytes = bos.toByteArray();
            // 7. 长度
            out.writeInt(bytes.length);
            // 8. 写入内容
            out.writeBytes(bytes);
            outList.add(out);
        }
    
        @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);
        }
    }
    
  • 相关阅读:
    写的有道理的
    20199123 201920202 《网络攻防实践》第2周作业
    20199123 201920202 《网络攻防实践》第3周作业
    20199123 201920202 《网络攻防实践》第1周作业
    20199123《网络攻防实践》假期作业
    RSA,AES加解密算法的实现
    20199123 201920202 《网络攻防实践》第5周作业
    20199123 201920202 《网络攻防实践》第6周作业
    20199123 201920202 《网络攻防实践》第4周作业
    第四周课上实践验收
  • 原文地址:https://www.cnblogs.com/wwjj4811/p/16302242.html
Copyright © 2020-2023  润新知