• Netty学习笔记(三) 自定义编码器


    编写一个网络应用程序需要实现某种编解码器,编解码器的作用就是讲原始字节数据与自定义的消息对象进行互转。网络中都是以字节码的数据形式来传输数据的,服务器编码数据后发送到客户端,客户端需要对数据进行解码,因为编解码器由两部分组成:

    • Decoder(解码器)
    • Encoder(编码器)

    解码器负责处理“入站”数据,编码器负责处理“出站”数据。编码器和解码器的结构很简单,消息被编码后解码后会自动通过ReferenceCountUtil.release(message)释放。

    需要补充说明的是,Netty中有两个方向的数据流

    • 入站(ChannelInboundHandler):从远程主机到用户应用程序则是“入站(inbound)”

    • 出站(ChannelOutboundHandler):从用户应用程序到远程主机则是“出站(outbound)”

    今天我们主要学习编码器,也就是Encoder

    实现逻辑

    完成一个编码器的编写主要是实现一个抽象类MessageToMessageEncoder,其中我们需要重写方法是

        /**
         * Encode from one message to an other. This method will be called for each written message that can be handled
         * by this encoder.
         *
         * @param ctx           the {@link ChannelHandlerContext} which this {@link MessageToMessageEncoder} belongs to
         * @param msg           the message to encode to an other one
         * @param out           the {@link List} into which the encoded msg should be added
         *                      needs to do some kind of aggragation
         * @throws Exception    is thrown if an error accour
         */
        protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
    

    其中泛型参数I表示我们需要接收的参数类型,如你需要将ByteBuf类型转换为Date类型,那么泛型I就是ByteBuf(事实上当实现ByteBuf编码为其他类型的时候是不需要使用MessageToMessageEncoder,Netty提供了ByteToMessageCodec,其本质也是实现了MessageToMessageEncoder)

    代码编写

    需求说明

    客户端发过来一个数字(ByteBuf),我们将此类型转换为数字,获取当前时间加上此数字的时间后返回客户端,具体逻辑如下:

    编码器的编写

    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.MessageToMessageDecoder;
    import io.netty.util.CharsetUtil;
    
    import java.time.LocalDateTime;
    import java.util.List;
    
    public class TimeEncoder extends MessageToMessageDecoder<ByteBuf> {
    
      @Override
      protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        //将ByteBuf转换为String,注意此处,我是Mac OS,数据结尾是
    ,如果是其他类型的OS,此处可能需要调整
        String dataStr = msg.toString(CharsetUtil.UTF_8).replace("
    ","");
        //将String转换为Integer
        Integer dataInteger = Integer.valueOf(dataStr);
        //获取当前时间N小时后的数据
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime dataLocalDatetime = now.plusHours(dataInteger);
        out.add(dataLocalDatetime);
      }
    }
    
    

    服务端处理代码

    此处的服务端HandleAdapter和前面两个章节的HandleAdapter有所区别的是:其继承了SimpleChannelInboundHandler<I> 并且传递了一个泛型参数,这里需要说明一下,SimpleChannelInboundHandler是ChannelInboundHandler一个子类,他能够自动帮我们处理一些数据,在ChannelInboundHandler中,我们使用channel方法来接收数据,那么在SimpleChannelInboundHandler中我们使用protected abstract void messageReceived(ChannelHandlerContext ctx, I msg) throws Exception;来接收客户端的参数,可以看到的是,其参数中自动的实现了我们需要处理的泛型I msg,另外看一下SimpleChannelInboundHandler中channelRead方法的实现代码

       @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            boolean release = true;
            try {
    			//acceptInboundMessage() 检查参数的类型是否和设定的泛型是否匹配
    			//可以看到匹配的话,会进行强制类型转换并调用messageReceived方法
    			//否则的话,不执行,也就是说,这里的泛型一定要和编码器转换的结果类型一致,否则将接收不到参数
    			//当前如果你需要自己转换,那么你也可以和ChannelInBoundHandleAdapter一样,重写channelRead方法
    			
                if (acceptInboundMessage(msg)) {
                    @SuppressWarnings("unchecked")
                    I imsg = (I) msg;
                    messageReceived(ctx, imsg);
                } else {
                    release = false;
                    ctx.fireChannelRead(msg);
                }
            } finally {
                if (autoRelease && release) {
                    ReferenceCountUtil.release(msg);
                }
            }
        }
    
    

    那么继续实现我们的HandleAdapter,代码非常简单,这里不再赘述。需要注意的是,我们这里没有做解码器,也就是说入站的时候需要ByteBuf类型的数据,因此使用channel.writeAndFlush(Object)的时候,需要的就是ByteBuf类型的数据类型(当然如果pipeline中添加了StringDecoder解码器,那么你就可以直接使用字符串类型的数据了)

    import io.netty.buffer.Unpooled;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    
    import java.nio.charset.Charset;
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    
    public class TimeServerChannelHandleAdapter extends SimpleChannelInboundHandler<LocalDateTime> {
    
      @Override
      public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("添加了新的连接信息:id = " + ctx.channel().id());
      }
    
      @Override
      protected void messageReceived(ChannelHandlerContext ctx, LocalDateTime msg) throws Exception {
        // 转换时间格式
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String dateFormat = msg.format(dateTimeFormatter);
        System.out.println("接收到参数:" + dateFormat);
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer(dateFormat, Charset.defaultCharset()));
      }
    }
    
    

    服务端启动代码

    服务前启动代码和以前的代码非常类似,只需要在pipeline添加上适配的编码器即可,当然需要注意顺序(这个知识点以后我在仔细的阐述)

    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.*;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    
    public class TimeServer {
    
      public void start() throws Exception {
        EventLoopGroup boosGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();
    
        try {
          ServerBootstrap bootstrap = new ServerBootstrap();
          bootstrap
              .group(boosGroup, workGroup)
              .channel(NioServerSocketChannel.class)
              .option(ChannelOption.SO_BACKLOG, 128)
              .childHandler(
                  new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                      ChannelPipeline pipeline = ch.pipeline();
                      //设置编码
                      pipeline.addLast(new TimeEncoder());
                      //设置服务处理
                      pipeline.addLast(new TimeServerChannelHandleAdapter());
                    }
                  });
    
          ChannelFuture sync = bootstrap.bind(9998).sync();
          System.out.println("Netty Server start with 9998 port");
          sync.channel().closeFuture().sync();
        } finally {
          workGroup.shutdownGracefully();
          boosGroup.shutdownGracefully();
        }
      }
    
      public static void main(String[] args) throws Exception {
        TimeServer server = new TimeServer();
        server.start();
      }
    }
    
    

    效果展示

    这里为了不写太多的代码,防止造成知识的不理解,迷惑,这里我们使用telnet命令来测试数据,

    启动服务器端

    运行TimeServer代码的中的main方法即可

    使用Telnet发送数据

    telnet的命令格式是

    usage: telnet [-l user] [-a] [-s src_addr] host-name [port] 
    

    可以看到大部分参数都是可选的,只有主机名称必填

    发送数据的效果

    继续在telnet中发送一个数据5,我们分别看下服务端的打印的数据和telnet接收到的数据

    服务端打印的数据如下:

    telnet端打印的接收到的数据

    总结

    至此,一个简单的编码器就完成,我们总结一下步骤

    • 继承MessageToMessageDecoder抽象类,实现decode()方法
    • 配置HandleAdapter 实现channelRead或者messageReceived方法
    • 配置服务启动类,配置ChannelPipeline,添加编码器和HandleAdapter
    • 编写客户端或者使用telnet或者其他手段测试
  • 相关阅读:
    快速创建一个 Servlet 项目(1)
    快速创建一个 Servlet 项目(2)
    多级派生情况下派生类的构造函数
    最近看了点C++,分享一下我的进度吧!
    进程同步&进程间通信
    multiprocess模块
    进程
    网络编程之socket
    网络通信原理
    网络通信的流程 | 初始socket
  • 原文地址:https://www.cnblogs.com/zhoutao825638/p/10382163.html
Copyright © 2020-2023  润新知