Netty读书笔记4: 编码器解码器详解

1. 编码器解码器的引入

通常情况下,当我们得到ByteBuf情况下,我们如果得解码得到我们想要的消息,通常情况下是如下这样处理

 @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception{
        ByteBuf byteBuf = (ByteBuf)msg;
        byte [] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        String s = new String(bytes, Charset.forName("UTF-8"));
    }

这样的一个弊端是,每次InBoundHandler的channelRead读取数据时候,都要读到一个数组中,然后转化成我们想要的数据.这样太过于麻烦了.如果我们直接将msg转化成我们想要的类型,这就非常的快捷.这就是解码器的目的.

2. DelimiterBasedFrameDecoder解码器

DelimiterBasedFrameDecoder解码器用来解码以自定义特殊字符串结尾的消息,例如以"#"结尾的消息,每遇到一个以"#"字符串,则触发channelRead()读取数据.

服务端代码

public class TimeServer {
    public void bind(int port)throws Exception{
        /* 配置服务端的NIO线程组 */
        // NioEventLoopGroup类 是个线程组,包含一组NIO线程,用于网络事件的处理
        // (实际上它就是Reactor线程组)。
        // 创建的2个线程组,1个是服务端接收客户端的连接,另一个是进行SocketChannel的
        // 网络读写
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup WorkerGroup = new NioEventLoopGroup();

        try {
            // ServerBootstrap 类,是启动NIO服务器的辅助启动类
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup,WorkerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,1024)
                    .childHandler(new ChildChannelHandler());

            // 绑定端口,同步等待成功
            ChannelFuture f= b.bind(port).sync();

            // 等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        }finally {
            // 释放线程池资源
            bossGroup.shutdownGracefully();
            WorkerGroup.shutdownGracefully();
        }
    }

    private class ChildChannelHandler extends ChannelInitializer<SocketChannel>{
        @Override
        protected  void initChannel(SocketChannel arg0)throws Exception{
            
            // 消息用 _#_ 作为分隔符,加入到DelimiterBasedFrameDecoder中,第一个参数表示单个消息的最大长度,当达到该
            // 长度后仍然没有查到分隔符,就抛出TooLongFrameException异常,防止由于异常码流缺失分隔符导致的内存溢出
            ByteBuf delimiter = Unpooled.copiedBuffer("_#_".getBytes());
            arg0.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));

            arg0.pipeline().addLast(new StringDecoder());
            arg0.pipeline().addLast(new TimeServerHandler());
        }
    }

    public static void main(String[]args)throws Exception{
        int port = 8080;
        if(args!=null && args.length>0){
            try {
                port = Integer.valueOf(args[0]);
            }
            catch (NumberFormatException ex){}
        }
        new TimeServer().bind(port);
    }
}

  • 以上定义了DelimiterBasedFrameDecoder,并且添加到Pipeline中.
  • 同样引入了StringDecoder编码器,这个编码器的作用是把读到的内容作为整个字符串,不分开.
  • 以上过程中需要注意DelimiterBasedFrameDecoder,StringDecoder加入到 PipeLine中的顺序.
    先加入DelimiterBasedFrameDecoder,后加入StringDecoder.表明是以"#"作为结束符号,然后StringDecoder把这个结束的字符串作为读取成功的.

服务端Handler

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    private int counter=0;

  

  // 当客户端和服务端建立tcp成功之后,Netty的NIO线程会调用channelActive
  // 发送查询时间的指令给服务端。
  // 调用ChannelHandlerContext的writeAndFlush方法,将请求消息发送给服务端
  // 当服务端应答时,channelRead方法被调用
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception{

        String body = (String)msg;
        System.out.println(body+";the counter is :"+ (++counter));

        ByteBuf resp = Unpooled.copiedBuffer(("hello"+"_#_").getBytes());
        ctx.writeAndFlush(resp);   //写入操作

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
        cause.printStackTrace();
        ctx.close();
    }
}
  • 这里服务端传给客户端同样采用以"#"作为分隔符号,然后读取每个字符串.

客户端代码

public class TimeClient {
    public void connect(String host,int port)throws Exception{
        // 配置服务端的NIO线程组
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            // Bootstrap 类,是启动NIO服务器的辅助启动类
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY,true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception{
                             ByteBuf delimiter = Unpooled.copiedBuffer("_#_".getBytes());
                            // 增加 DelimiterBasedFrameDecoder编码器
                            ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new TimeClientHandler());
                        }
                    });

            // 发起异步连接操作
            ChannelFuture f= b.connect(host,port).sync();

            // 等待客服端链路关闭
            f.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[]args)throws Exception{
        int port = 8080;
        if(args!=null && args.length>0){
            try {
                port = Integer.valueOf(args[0]);
            }
            catch (NumberFormatException ex){}
        }
        new TimeClient().connect("127.0.0.1",port);
    }
}
  • 客户端的逻辑和服务端差不多

客户端Handler

public class TimeClientHandler extends ChannelInboundHandlerAdapter {



    private int counter;
    private final String echo_req = "aaaa_#_aa_#_";


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
           throws Exception{
        String body = (String)msg;
        System.out.println( body+";the countor is : "+ ++counter);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx){
        // 当客户端和服务端建立tcp成功之后,Netty的NIO线程会调用channelActive
        // 发送查询时间的指令给服务端。
        // 调用ChannelHandlerContext的writeAndFlush方法,将请求消息发送给服务端
        // 当服务端应答时,channelRead方法被调用

            ctx.writeAndFlush(Unpooled.copiedBuffer(echo_req.getBytes()));

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
        System.out.println("message from:"+cause.getMessage());
        ctx.close();
    }
}

客户端的Handler与服务端的Handler差不多.
客户端运行结果如图


image.png

客户端运行结果如图


image.png

结果分析,客户端首先发动"aaaa"字符串(以"#"分隔),然后服务端发动hello给客户端.接着客户端发送剩下的"aa"给服务端,服务端发动"hello"给客户端.

  1. 编码器,解码器的编程示范
    接下来实现的是一个把一个字节转化int的编解码器.
    解码器实现如下
public class ByteToIntegerDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {


        if(in.readableBytes()>=4){
            int n = in.readInt();
            System.out.println("decode meg is "+n);
            out.add(n);
        }
    }
}

  • 注意最后需要List中去
    编码器实现如下
public class IntegerToByteEncoder  extends MessageToByteEncoder<Integer>{

    @Override
    protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {
        System.out.println("encode message is msg"+msg);
        out.writeInt(msg);
    }
}
  • 上面的编码器实现的是把int转化成字节码.
总结:

通过以上的方式,我们能够很快定义一个编码器和解码器,来处理传输过程中的数据转换.对于Tcp的粘包问题,将在下面讲到.
上面的代码github地址:https://github.com/maskwang520/nettyinaction

参考文章:

netty之分隔符和定长解码器解决之道
Netty (四) 分隔符和定长解码器的使用

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容