zoukankan      html  css  js  c++  java
  • java架构之路-(netty专题)netty的编解码(出入战)与粘包拆包

    上次回归:

      上次博客我们主要说了netty的基本使用,都是一些固定的模式去写的,我们只需要关注我们的拦截器怎么去写就可以了,然后我们用我们的基础示例,改造了一个简单的聊天室程序,可以看到内部加了一个StringEncoder和StringDecoder,这个就是用来编解码我们字符串的,这次我们就来说说这个编解码。

    编码&解码:

      上次我们写的那个简单的聊天室程序大家还记得吧,内部加了两个类似拦截器的玩意。

    ch.pipeline().addLast(new StringEncoder());
    ch.pipeline().addLast(new StringDecoder());

      一个是编码的,一个是解码的,也是借着这个东西,来个大家说一下我们的ChannelPipline,上次只是简单了说了一下我们的ChannelPipline内部存放了ChannelHandler,而且是双向链表的结构来存储的,我们这次来细化一下我们这个拦截器是怎么工作的。

     内部大概是这个样子的,每次放入的时候有顺序的放置,暂时先说有顺序,后面我会详细解释这个什么时候顺序生效,记住是由由头开始放置,这里涉及到两个概念就是入站和出站。

       就是说我们从客户端发送数据到服务端,叫做出站,会经过一系列的ChannelOutboundHandler,可以方便记忆为一系列的出站拦截器,我们想出站,就要经过出站拦截器。

       反之我们的入站就是和出站相对应的,是由服务端发送过来的数据,经由我们的一系列ChannelInboundHandler,到达我们的客户端,其实出站入站你站在客户端的角度来看就很好理解了,我们客户端想发出去数据,就是出站,想进来数据(接收数据),就是入站,出站会经过out拦截器,入站会经过in拦截器。切记,是双向的,客户端和服务端不是共有一个ChannelPipline,而且这个出站和入站都是相对的,可能还是有一点抽象,我们来拿着我们聊天室的例子来看一下。

      我们需要先明确我们的StringEncoder和StringDecoder是ChannelInboundHandler还是ChannelOutboundHandler。

     

      StringEncoder是ChannelOutboundHandler,StringDecoder是ChannelInboundHandler,这回我们按照我们的代码画一下图。先弄一个客户端发送消息的。

       简单解释一下,我们的服务端和客户端都有自己的ChannelPipline,我们的客户端要发送消息,相当于客户端是出站操作,我们要发送,数据外流,显然是数据要出去,出站操作啊, 出站要经过Encoder然后是我们自己的Clienthandler,走你,进入网络传输,对于我们的服务端来说,要接收数据,数据要进来,一定是入站操作啊,经过我们的Decoder,然后经过我们自己的Serverhandler,到达我们的服务端。

       客户端往外发送消息,客户端是出站操作,经过Encoder,然后经过我们ServerHandler,进入网络,我们客户端是入站操作,经过Decoder,经过我们的ClientHandler,到达我们的服务端。这样说应该就理解了吧,你可以自己在decode和encode方法上打断点,自己调试一下,后面我会说源码,自己也是先熟悉一下源码。出站一定是从尾到头,入站一定是从头到尾,别问我为什么,我自己写了测试类,测试一下午了.....

      我们可以看到Handler是按照顺序执行的,这个顺序只是对于相同类型的Handlery有效果的,像我们的Decoder和Encoder,一个是入站的Handler,一个是出站的Handler,他俩谁在前,谁在后无所谓的。

    粘包&拆包:

       粘包和拆包比上面那个出入战好理解很多很多,我们先来看一段代码。

    package com.xiaocai.packing;
    
    import io.netty.bootstrap.Bootstrap;
    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.Unpooled;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioSocketChannel;
    import io.netty.util.CharsetUtil;
    
    public class NettyClient {
        public static void main(String[] args) throws Exception {
            EventLoopGroup group = new NioEventLoopGroup();// 开启工作线程组
            try {
                Bootstrap bootstrap = new Bootstrap(); //创建一个和服务端相对应的server
                bootstrap.group(group) //设置线程组
                        .channel(NioSocketChannel.class) //使用NioSocketChannel作为客户端的通道实现
                        .handler(new ChannelInitializer<SocketChannel>() {//设置回调函数
                            @Override
                            protected void initChannel(SocketChannel ch) {
    
                            }
                        });
    
                ChannelFuture cf = bootstrap.connect("127.0.0.1", 9000).sync();//启动客户端去连接服务器端
                //对通道关闭进行监听
                System.out.println("netty client start。。准备开始发送数据");
                for (int i = 0; i < 2000; i++) {
                    ByteBuf buf = Unpooled.copiedBuffer("hello,xiaocaiJAVA!".getBytes(CharsetUtil.UTF_8));
                    cf.channel().writeAndFlush(buf);
                }
                System.out.println("发送数据完毕");
                cf.channel().closeFuture().sync();
            } finally {
                group.shutdownGracefully();//关闭线程组
            }
        }
    }

      就是什么意思呢?我们建立一个客户端连接,然后我们多次向我们的服务端发送消息,理论上我们每次收到的消息都应该是hello,xiaocaiJAVA!,我们来看一下结论。

       我们可以看到,有部分是正常的,有一部分是hello,xiaocaiJAVA!hello,xiaocaiJAVA!有的还是o,什么什么,这个明显错误的,也就是我们的粘包拆包,为什么会出现这个呢?netty收到我们的消息不是马上发送出去,大概会等待一个瞬间,然后再发送我们的消息,在等待的瞬间再次进来的消息,他会一次性的发送出去,但是netty自身并不知道我们的消息该从何位置截断,所以就出现了我们看到的粘包拆包问题,我们来看一下解决方法。

       我们每次可以把数据发过去,而且把数据的长度带过去就OK了,然后客户端每次优先判断一下数据的长度就可以了,看一下我这的解决方案。

    package com.xiaocai.packing;
    
    /**
     * 自定义协议包
     */
    public class MyMessageProtocol {
    
        //定义一次发送包体长度
        private int len;
        //一次发送包体内容
        private byte[] content;
    
        public int getLen() {
            return len;
        }
    
        public void setLen(int len) {
            this.len = len;
        }
    
        public byte[] getContent() {
            return content;
        }
    
        public void setContent(byte[] content) {
            this.content = content;
        }
    }
    package com.xiaocai.packing;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.MessageToByteEncoder;
    
    public class MyMessageEncoder extends MessageToByteEncoder<MyMessageProtocol> {
        @Override
        protected void encode(ChannelHandlerContext ctx, MyMessageProtocol msg, ByteBuf out) throws Exception {
            out.writeInt(msg.getLen());
            out.writeBytes(msg.getContent());
        }
    }
    package com.xiaocai.packing;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.ByteToMessageDecoder;
    
    import java.util.List;
    
    public class MyMessageDecoder extends ByteToMessageDecoder {
    
        int length = 0;
    
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
            if(in.readableBytes() >= 4) {
                if (length == 0){
                    length = in.readInt();
                }
                if (in.readableBytes() < length) {
                    System.out.println("当前可读数据不够,继续等待。。");
                    return;
                }
                byte[] content = new byte[length];
                if (in.readableBytes() >= length){
                    in.readBytes(content);
    
                    //封装成MyMessageProtocol对象,传递到下一个handler业务处理
                    MyMessageProtocol messageProtocol = new MyMessageProtocol();
                    messageProtocol.setLen(length);
                    messageProtocol.setContent(content);
                    out.add(messageProtocol);
                }
                length = 0;
            }
        }
    }
    package com.xiaocai.packing;
    
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.util.CharsetUtil;
    
    public class PackingServerHandler extends SimpleChannelInboundHandler<MyMessageProtocol> {
    
        private int count;
    
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, MyMessageProtocol msg) throws Exception {
            System.out.println("====服务端接收到消息如下====");
            System.out.println("长度=" + msg.getLen());
            System.out.println("内容=" + new String(msg.getContent(), CharsetUtil.UTF_8));
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }
    package com.xiaocai.packing;
    
    import io.netty.bootstrap.Bootstrap;
    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.Unpooled;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioSocketChannel;
    import io.netty.util.CharsetUtil;
    
    public class PackingClient {
        public static void main(String[] args) throws Exception {
            EventLoopGroup group = new NioEventLoopGroup();
            try {
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.group(group)
                        .channel(NioSocketChannel.class)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) {
                                ch.pipeline().addLast(new MyMessageEncoder());
                            }
                        });
    
                ChannelFuture cf = bootstrap.connect("127.0.0.1", 9000).sync();
    
                for(int i = 0; i< 200; i++) {
                    String msg = "你好,小菜JAVA!";
                    //创建协议包对象
                    MyMessageProtocol messageProtocol = new MyMessageProtocol();
                    messageProtocol.setLen(msg.getBytes(CharsetUtil.UTF_8).length);
                    messageProtocol.setContent(msg.getBytes(CharsetUtil.UTF_8));
    
                    cf.channel().writeAndFlush(messageProtocol);
                }
    
                cf.channel().closeFuture().sync();
            } finally {
                group.shutdownGracefully();
            }
        }
    }

      简单来说就是我们封装一个协议包,包含我们的内容和长度,我们要客户端传消息之前,信息进行处理,给予长度,然后我客户端接收数据时优先判断长度,长度不够继续等待,这样就解决了我们的拆包粘包问题,有的人还会提出用什么特殊符号的方法,也是可行的,但是你的数据中一定不要包含那个特殊符号,而且每次来一个新的开发人员,都要了解你们的特殊符号协议,还是比较麻烦的。

    总结:

      这次我们主要说了ChannelPipline内部的结构和addList时的放置顺序,netty的入战出战,是相对的,出站走out拦截器,入站走in拦截器,入站一定是从头到尾的,出站一定是从尾到头的,切记~!!!

     

    最进弄了一个公众号,小菜技术,欢迎大家的加入

     

  • 相关阅读:
    Execution Contexts (执行上下文)
    OOP—ECMAScript实现详解
    requireJS入门
    SqlServer 傲娇的表变量
    CSharp进阶 引用类型引发的血案
    CSharp进阶 都是请求惹的祸
    z-index问题
    js中事件(自定义事件)
    做了个后末日朋克风的梦
    昨晚的梦
  • 原文地址:https://www.cnblogs.com/cxiaocai/p/12320550.html
Copyright © 2011-2022 走看看