netty学习06-编解码介绍

1. 编码和解码器

在netty中编码器和解码器,其本质也是ChannelHandler的一种实现。

1.1 半包粘包问题

产生原因:TCP/IP协议是面向流的协议。

当客户端向服务器端发送数据时,会把数据划分为一个一个的包进行发送,服务器端收到数据时,也是一个一个的包,那么服务器端怎么知道一个请求包含几个包呢?即服务器端怎么把一个一个包组装成一个完整请求呢?

1.2 粘包/拆包解决思路

不断从TCP缓冲区中读取数据,每次读取完都需要判断是否是一个完整的请求数据。

  • 若当前读取的数据不足以拼接成一个完整的请求数据,那就保留该数据,继续从tcp缓冲区中读取,直到得到一个完整的请求数据。
  • 若当前读到的数据加上已经读取的数据足够拼接成一个请求数据,那就将已经读取的数据拼接上本次读取的数据,构成一个完整的请求传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接。

如何判断一个完整的业务数据包呢?

  • 定长:客户端和服务器协商一个请求为N个字节。
  • 分隔符:客户端和服务器端协商以一个分隔符作为标记,表示一个完整的业务数据包
  • 基于长度的变长包:客户端和服务端协商,在请求的最前面用N个字节表示这个请求涉及到的数据长度,后面再传具体数据

1.3 netty常见的编解码器

常用的解码器如下:

  • LineBasedFrameDecoder:回车换行解码器,即遇到一个回车换行符,表示一个请求数据的结束。
  • DelimiterBasedFrameDecoder:分隔符解码器
  • FixedLengthFrameDecoder:固定长度解码器
  • LengthFieldBasedFrameDecoder:变长解码器(常用)

对应的常见编码器如下:

  • LineEncoder:回车换行编码器,99%的时候没用,理由和分隔符编码器一样
  • 分隔符编码器netty未提供,因为你只需要在发送消息时,将分隔符写入到消息末尾即可
  • 固定长度编码器netty未提供,因为你在写入消息时,只能写入N个字符,超过N个将会变成多个业务消息,少于N个则需要补齐。
  • LengthFieldPrepender:变长编码器(常用)

接下来我们就详细介绍变长编解码器。

1.4 netty常见编解码类继承关系

从上图可以看出,编解码器都是channelHandler,解码器是入站处理器,编码器为出站处理器。

编码器分为MessageToByteEncoder和MessageToMessageEncoder两大类,解码器分为ByteToMessageDecoder和MessageToMessageDecoder两大类。

2. 变长编解码器

2.1 变长编解码器的使用

注意:pipeline上的handler是有顺序要求的哟。尤其是编码器与编码器之间,解码器与解码器之间。即入站handler与入站handler之间,出站handler与出站handler之间。入站handler和出站handler之间则没有顺序要求。

2.2 变长解码器

LengthFieldBasedFrameDecoder详解:

构造函数:

public LengthFieldBasedFrameDecoder(int maxFrameLength,int lengthFieldOffset, int lengthFieldLength,int lengthAdjustment, int initialBytesToStrip)
  • maxFrameLength:发送的数据包最大长度;
  • lengthFieldOffset:长度域偏移量,指的是长度域位于整个数据包字节数组中的下标;
  • lengthFieldLength:长度域的自己的字节数长度;
  • lengthAdjustment:长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。
  • initialBytesToStrip:丢弃的起始字节数。丢弃处于有效数据前面的字节数量。比如前面有4个节点的长度域,则它的值为4。

一般用法为:new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4),表示消息最大长度为Integer的最大值,长度域在消息的最前面,长度域占4个字节,实际消息在整个字节数组的第四个位置开始。

2.3 变长编码器

LengthFieldPrepender编码器详解:

构造函数:

LengthFieldPrepender(int lengthFieldLength)

  • lengthFieldLength:长度域所占字节数,和解码器中的lengthFieldLength对应。

一般用法:new LengthFieldPrepender(4),表示将消息字节数组的长度先写在消息前面,并占4个字节。

2.4 流程解析

根据2.1小结的源码进行。

3. 自定义协议

3.1 编写协议类

public class MyProtocol {

    private Integer header;
    private Integer length;
    private byte[] content;

    public Integer getHeader() {
        return header;
    }

    public void setHeader(Integer header) {
        this.header = header;
    }

    public Integer getLength() {
        return length;
    }

    public void setLength(Integer length) {
        this.length = length;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }
}

3.2 编码器

public class MyEncoder extends MessageToByteEncoder<MyProtocol> {

    @Override
    protected void encode(ChannelHandlerContext ctx, MyProtocol msg, ByteBuf out) throws Exception {
        out.writeInt(msg.getHeader());
        out.writeInt(msg.getLength());
        out.writeBytes(msg.getContent());
    }
}

3.3 解码器

public class MyDecoder extends ReplayingDecoder<MyProtocol> {

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

        int header=in.readInt();
        int length=in.readInt();
        byte[] content=new byte[length];
        in.readBytes(content);

        MyProtocol myProtocol =new MyProtocol();
        myProtocol.setHeader(header);
        myProtocol.setContent(content);
        myProtocol.setLength(length);
        out.add(myProtocol);
    }
}

为什么继承ReplayingDecoder而不是ByteToMessageDecoder?

ReplayingDecoder是一个特殊的ByteToMessageDecoder ,可以在阻塞的i/o模式下实现非阻塞的解码。
ReplayingDecoder 和ByteToMessageDecoder 最大的不同就是继承ReplayingDecoder 并实现decode()和decodeLast()方法时,你不需要手动判断接收到的字节数,你可以认为所有字节都已经接收到;如果直接继承ByteToMessageDecoder,则需要你手动判断已经接收到了多少字节。

3.4 服务器端

public class MyServer {

    public static void main(String[] args) {
        //负责接收客户端的连接请求
        EventLoopGroup boosGroup = new NioEventLoopGroup(1);
        //负责接收客户端读写请求
        EventLoopGroup workerGroup = new NioEventLoopGroup(1);

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boosGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler())
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new MyDecoder());
                            pipeline.addLast(new MyEncoder());
                            pipeline.addLast(new MyServerHandler1());
                        }
                    });

            ChannelFuture channelFuture = bootstrap.bind(9999).sync();
            System.out.println("系统启动成功!!!");
            channelFuture.channel().closeFuture().sync();
            System.out.println("系统执行完成!!!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

public class MyServerHandler1 extends SimpleChannelInboundHandler<MyProtocol> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyProtocol msg) throws Exception {
        System.out.println("服务端接收到的数据:");
        System.out.println("版本:"+ msg.getHeader());
        System.out.println("长度:"+msg.getLength());
        System.out.println("内容:"+new String(msg.getContent(),"UTF-8"));

        MyProtocol myProtocol =new MyProtocol();
        myProtocol.setContent("你好,客户端".getBytes("UTF-8"));
        myProtocol.setLength(myProtocol.getContent().length);
        myProtocol.setHeader(1);

        ctx.writeAndFlush(myProtocol);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

3.5 客户端

public class MyClient {

    public static void main(String[] args) {
        EventLoopGroup group=new NioEventLoopGroup();
        try {
            Bootstrap boot = new Bootstrap();
            boot.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline=ch.pipeline();

                            pipeline.addLast(new MyDecoder());
                            pipeline.addLast(new MyEncoder());

                            pipeline.addLast(new MyClientHandler());
                        }
                    });

            ChannelFuture channelFuture = boot.connect("localhost", 9999).sync();
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            group.shutdownGracefully();
        }
    }
}

public class MyClientHandler extends SimpleChannelInboundHandler<MyProtocol> {
    /**
     *
     * @param ctx 上下文请求对象
     * @param msg 表示服务端发来的消息
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyProtocol msg) throws Exception {
        System.out.println("客户端收到的消息:");
        System.out.println("版本:"+ msg.getHeader());
        System.out.println("长度:"+msg.getLength());
        System.out.println("内容:"+new String(msg.getContent(),"UTF-8"));
    }


    /**
     * 如果没有这个方法,Client并不会主动发消息给Server
     * 那么Server的channelRead0无法触发,导致Client的channelRead0也无法触发
     * 这个channelActive可以让Client连接后,发送一条消息
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        MyProtocol myProtocol =new MyProtocol();
        myProtocol.setHeader(1);
        myProtocol.setContent("你好,服务器".getBytes("UTF-8"));
        myProtocol.setLength(myProtocol.getContent().length);

        ctx.writeAndFlush(myProtocol);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

 

 

 

参考博客:https://blog.csdn.net/qq_37909508/article/category/8983741/5

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值