Netty框架使用总结

概念

Netty是一个提供异步事件驱动的网络应用程序框架和工具,用于快速开发维护的高性能和高伸缩的客户端和服务端,Netty是一个NIO客户端服务器框架。

Netty相关类与应用场景

ByteBuf

ByteBuf是一种零个或多个字节(8bit)组成的随机且顺序访问序列。这个接口为一个或多个基元字节数组(byte[])和NIO缓冲区提供了一个抽象视图。

ByteBuf是使用Netty开发常用的基本数据结构。Netty也存在堆内存与直接内存的区别:

  • 堆内存:用于对象的创建和存储,以及进行编码、解码等操作。特点是内存分配和回收速度较快,如果用于Socket的IO处理则需要做一次额外的内存复制。
  • 直接内存:主要用于传输数据,直接内存可以直接与Socket进行交互而不需要额外的内存复制,其内存分配和回收的速度则较慢于堆内存。

ByteBuf创建实例有以下几种方式:

  1. 通过ByteBufAllocator接口
  2. 通过AbstractByteBufAllocator
  3. 通过PooledByteBufAllocator
  4. 通过UnpooledByteBufAllocator
  5. 通过Unpooled工具类
  6. 通过Netty的Channel#alloc方法

上述类与接口关系如下:

图中的类都实现了ByteBufAllocator接口,接口有个静态常量DEFAULT是引用了 ByteBufUtil.DEFAULT_ALLOCATOR,在ByteBufUtil类中实现如下图:

根据平台环境来选择Allocator的默认类型,当系统不存在io.netty.allocator.type该属性的时候,安卓平台默认为unpooled,其他则为pooled,也就是选择UnpooledByteBufAllocatorUnpooledByteBufAllocator 提供的默认实现的其中一种。Netty提供的Unpooled工具类内部则也是使用了UnpooledByteBufAllocator类的实现。

这里根据类的名称,可以看出Netty对于内存的分配进行了池化非池化的处理,两者的区别如下:

  • 池化的ByteBuf可以减少内存分配和释放的开销,提高效率,但会造成更多的内存碎片。
  • 非池化的ByteBuf则相反,它没有内存池的限制,但需要更多的管理开销。

内存碎片又衍生出了一个CompositeByteBuf类,一种组合式ByteBuf实现。将多个缓冲区显示为单个合并缓冲区的虚拟缓冲区,避免多次分配和释放小块内存的开销,提高处理效率。

创建ByteBuf实例的代码如下:

java 复制代码
/**
 * 创建 ByteBuf 几种方式
 */
private static void generateByteBuf() {
    // 通过 Unpooled
    // Unpooled#buffer() 默认返回 heapBuffer
    ByteBuf heapBuffer = Unpooled.buffer();
    ByteBuf directBuffer = Unpooled.directBuffer();
    ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(new byte[128], new byte[256]);
    ByteBuf copiedBuffer = Unpooled.copiedBuffer(ByteBuffer.allocate(128));
    ByteBuf copiedBuffer1 = Unpooled.copiedBuffer(new byte[256],new byte[256]);


    // 通过实现ByteBufAllocator相关类
    ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
    ByteBuf abstractBuf = AbstractByteBufAllocator.DEFAULT.buffer();
    ByteBuf poolBuf = PooledByteBufAllocator.DEFAULT.buffer();
    ByteBuf unPooledBuf = UnpooledByteBufAllocator.DEFAULT.buffer();

    System.out.println("heapBuffer:"+heapBuffer);
    System.out.println("directBuffer:"+directBuffer);
    System.out.println("wrappedBuffer:"+wrappedBuffer);
    System.out.println(wrappedBuffer.maxCapacity());
    System.out.println("copiedBuffer:"+copiedBuffer);
    System.out.println("copiedBuffer maxCapacity:"+copiedBuffer.maxCapacity());
    System.out.println("copiedBuffer1:"+copiedBuffer1);
    System.out.println("copiedBuffer1 maxCapacity:"+copiedBuffer1.maxCapacity());
    System.out.println("buffer:"+buffer);
    System.out.println("abstractBuf:"+abstractBuf);
    System.out.println("poolBuf:"+poolBuf);
    System.out.println("unPooledBuf:"+unPooledBuf);

}

上述是一些创建ByteBuf实例部分API,创建ByteBuf实例,如果不设置capacity参数则默认为256,maxCapacity默认为Integer.MAX_VALUEUnpooled#copiedBuffer,Unpooled#copyInt则会设置数据的长度为maxCapacitymaxCapacity限制ByteBuf增长的最大容量。

CompositeByteBuf(ridx: 0, widx: 384, cap: 384, components=2)可知有两个ByteBuf组合,不过最大components为16,如果超过16个则使用16个,下图为创建ByteBuf的默认参数变量。

释放ByteBuf引用

ByteBuf是一个引用计数对象,必须通过release()方法显式释放,处理程序有责任释放传递给处理程序的任何引用计数对象。ByteBuf不释放会造成内存泄漏问题。

释放方式有以下几种:。

  • ByteBuf.release():释放一个refCnt引用计数,如果refCnt=0,调用该方法异常。
  • ReferenceCountUtil.release(Object msg): 在第一个方式操作前判断是否引用计数对象再调用。
  • ReferenceCountUtil.saveRelease(Object msg): 在第二个方式增加了trycatch来捕获异常。

可以使用ByteBufUtil.isAccessible(ByteBuf buffer)方法先判断ByteBuf是否还可以访问,再执行release()释放引用计数。 同样的可以使用ByteBuf.retain()来增加引用计数。

java 复制代码
private static void referenceRelease() {
    ByteBuf buffer = Unpooled.buffer();
    // 增加引用计数
    buffer.retain();
    buffer.retain(2);
    ReferenceCountUtil.retain(buffer);
    ReferenceCountUtil.retain(buffer,2);
    // 释放引用计数
    ReferenceCountUtil.release(buffer,3);
    ReferenceCountUtil.safeRelease(buffer,3);

    buffer.release();
    ReferenceCountUtil.safeRelease(buffer);
    System.out.println("safeRelease 执行通过");

    boolean accessible = ByteBufUtil.isAccessible(buffer);
    if (accessible) {
        System.out.println("buffer 可释放");
        ReferenceCountUtil.release(buffer);
    }else {
        System.out.println("buffer 没有可释放");
    }

    ReferenceCountUtil.release(buffer);
    System.out.println("不会执行该代码");

}

ByteBuf部分API

ByteBuf API 描述
copy() 复制一个全新的ByteBuf副本
readableBytes() 返回ByteBuf当前可读的字节数
isReadable() 当前ByteBuf是否可读
readInt() 读取一个整型(4字节),并修改readerIndex
readBytes(byte[] bytes) ByteBuf中数据写到bytes数组中,并修改readerIndex
writeBytes(byte[] bytes) 将bytes数据写入到ByteBuf中,并修改writerIndex
readerIndex() 返回当前readerIndex
writerIndex() 返回当前writerIndex
markReaderIndex() 标记当前readerIndex
markWriterIndex() 标记当前writerIndex
resetReaderIndex() 重置readerIndex到上次markReaderIndex()标记处,默认0
resetWriterIndex() 重置writerIndex到上次markWriterIndex()标记处,默认0
setIndex(int readerIndex,int writerIndex) 设置readerIndex、writerIndex
clear() 等于setIndex(0,0)
retain() 增加ByteBuf refCnt一个引用计数,refCnt>=1才可以增加,否则抛出异常
retain(int increment) 增加ByteBuf refCnt指定数量的引用计数,refCnt>=1才可以增加,否则抛出异常
release() 释放ByteBuf refCnt一个引用计数
release(int decrement) 释放ByteBuf refCnt指定数量引用计数
refCnt 返回当前引用计数
nioBuffer() 转换成NIO的ByteBuffer
slice() 返回此缓冲区的子区域的切片并设置maxCapacity(),修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们保持单独的索引和标记
duplicate() 返回一个共享此缓冲区的整个区域的缓冲区。修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们保持单独的索引和标记
java 复制代码
/**
 * ByteBuf、ByteBufUtil、Unpooled
 * 一些相关API
 */
private static void byteBufApi() {
    // Unpooled 申请 ByteBuf 并存入指定类型的数据int
    // 类似 API long,float,boolean,short,double
    ByteBuf intBuf = Unpooled.copyInt(1, 5, 5, 2134, 5454, 2345);
    // Unpooled 申请 ByteBuf 并存入指定类型的数据,byte,char,可多个数组
    // 这样的类型会限制 maxCapacity
    ByteBuf charBuffer = Unpooled.copiedBuffer(new char[]{'a', 'b', 'c', 'd'}, StandardCharsets.UTF_8);
    ByteBuf byteBuffer = Unpooled.copiedBuffer("SilverGravel".getBytes(StandardCharsets.UTF_8));

    /*数据复制*/
    // 复制是全新的ByteBuf
    ByteBuf copyBuf = byteBuffer.copy();

    /*数据读写*/
    // 读取整型
    // 可读字节,整型4位字节 4*6= 24
    System.out.print("\n************数据读写************\n");
    System.out.println("intBuf可读字节:" + intBuf.readableBytes());
    System.out.print("读取intBuf数据:" + intBuf.readInt());
    while (intBuf.isReadable()) {
        System.out.print("," + intBuf.readInt());
    }
    byte[] bytes = new byte[byteBuffer.readableBytes()];
    byteBuffer.readBytes(bytes);
    System.out.println("\nbyteBuffer数据:" + new String(bytes, StandardCharsets.UTF_8));
    // 此时readerIndex已经变更,同时writerIndex也会变更
    System.out.printf("byteBuffer readerIndex: %d,writerIndex: %d\n",
            byteBuffer.readerIndex(), byteBuffer.writerIndex());


    byte[] copyBytes = ByteBufUtil.getBytes(copyBuf);
    System.out.println("copyBuf:" + new String(copyBytes, StandardCharsets.UTF_8));

    // 写数据
    // 可以字节、InputStream、FileChannel
    // ByteBuf、ScatteringByteChannel
    ByteBuf operationBuf = Unpooled.buffer();
    operationBuf.writeBytes("Silver".getBytes(StandardCharsets.UTF_8));
    byte[] bytes2 = ByteBufUtil.getBytes(operationBuf);
    System.out.println("\noperationBuf:" + new String(bytes2, StandardCharsets.UTF_8));
    System.out.printf("operationBuf readerIndex: %d,writerIndex: %d\n",
            operationBuf.readerIndex(), operationBuf.writerIndex());


    operationBuf.writeBytes("Gravel".getBytes(StandardCharsets.UTF_8));
    byte[] bytes3 = new byte[operationBuf.readableBytes()];
    operationBuf.readBytes(bytes3);
    System.out.println("operationBuf:" + new String(bytes3, StandardCharsets.UTF_8));
    System.out.printf("operationBuf readerIndex: %d,writerIndex: %d\n",
            operationBuf.readerIndex(), operationBuf.writerIndex());
    operationBuf.resetReaderIndex();

    operationBuf.writeBytes("Operation".getBytes(StandardCharsets.UTF_8));
    byte[] bytes4 = new byte[operationBuf.readableBytes()];
    operationBuf.readBytes(bytes4);
    System.out.println("operationBuf:" + new String(bytes4, StandardCharsets.UTF_8));
    System.out.printf("operationBuf readerIndex: %d,writerIndex: %d\n"
            , operationBuf.readerIndex(), operationBuf.writerIndex());

    operationBuf.writeBytes("ByteBuf".getBytes(StandardCharsets.UTF_8));
    byte[] bytes5 = ByteBufUtil.getBytes(operationBuf);
    System.out.println("operationBuf:" + new String(bytes5, StandardCharsets.UTF_8));
    System.out.printf("operationBuf readerIndex: %d,writerIndex: %d\n",
            operationBuf.readerIndex(), operationBuf.writerIndex());
    System.out.println("由上述可知,ByteBuf#readBytes()会修改ReaderIndex和WriterIndex" +
            ",而ByteBufUtil#getBytes()则不会");

    /*重置 ReaderIndex WriterIndex*/
    System.out.print("\n************重置 ReaderIndex WriterIndex************\n");
    ByteBuf resetBuf = Unpooled.buffer();
    resetBuf.writeBytes("1".getBytes(StandardCharsets.UTF_8));
    System.out.printf("\nresetBuf当前 readerIndex:%d,writerIndex:%d\n",
            resetBuf.readerIndex(), resetBuf.writerIndex());
    // 标记当前读位置
    resetBuf.markReaderIndex();
    System.out.println("resetBuf当前标记readerIndex:" + resetBuf.readerIndex());
    resetBuf.writeBytes("2".getBytes(StandardCharsets.UTF_8));
    System.out.printf("resetBuf当前 readerIndex:%d,writerIndex:%d\n",
            resetBuf.readerIndex(), resetBuf.writerIndex());

    // 读取数据
    byte[] resetBytes = new byte[resetBuf.readableBytes()];
    resetBuf.readBytes(resetBytes);
    System.out.printf("resetBuf读取数据:%s, 当前 readerIndex:%d,writerIndex:%d\n",
            new String(resetBytes), resetBuf.readerIndex(), resetBuf.writerIndex());

    resetBuf.writeBytes("3".getBytes(StandardCharsets.UTF_8));
    System.out.printf("resetBuf 当前 readerIndex:%d,writerIndex:%d\n",
            resetBuf.readerIndex(), resetBuf.writerIndex());
    System.out.printf("resetBuf当前readerIndex:%d,可读字节:%d\n"
            , resetBuf.readerIndex(), resetBuf.readableBytes());
    // 重置readerIndex到上个 markReaderIndex
    resetBuf.resetReaderIndex();
    System.out.printf("resetBuf重置readerIndex之后,readerIndex:%d,可读字节:%d\n"
            , resetBuf.readerIndex(), resetBuf.readableBytes());

    // 标记当前 WriterIndex
    resetBuf.markWriterIndex();
    System.out.printf("resetBuf标记之后 readerIndex:%d, writerIndex:%d\n"
            , resetBuf.writerIndex(), resetBuf.writerIndex());

    resetBuf.writeBytes("4".getBytes(StandardCharsets.UTF_8));
    resetBytes = new byte[resetBuf.readableBytes()];
    resetBuf.readBytes(resetBytes);
    System.out.printf("resetBuf读取数据:%s, 当前 readerIndex:%d,writerIndex:%d\n",
            new String(resetBytes), resetBuf.readerIndex(), resetBuf.writerIndex());

    // 如果 WriterIndex > ReaderIndex 则抛出异常
    // 这里再重置一下 readerIndex
    resetBuf.resetReaderIndex();
    resetBuf.resetWriterIndex();
    System.out.printf("resetBuf重置读写索引之后,readerIndex:%d,writerIndex:%d\n"
            , resetBuf.readerIndex(), resetBuf.writerIndex());

    // 等于 ByteBuf#setIndex(0,0)
    resetBuf.clear();
    System.out.printf("resetBuf#clear之后,readerIndex:%d,writerIndex:%d\n"
            , resetBuf.readerIndex(), resetBuf.writerIndex());


    // 0 <= readerIndex <= writerIndex <= capacity
    resetBuf.setIndex(1, 3);
    System.out.printf("resetBuf设置读写索引之后,readerIndex:%d,writerIndex:%d\n"
            , resetBuf.readerIndex(), resetBuf.writerIndex());



    /* 增加或释放 ByteBuf refCnt引用计数*/
    System.out.println("\n************增加或释放 ByteBuf refCnt引用计数************");
    ByteBuf refBuf = resetBuf.copy();
    refBuf.retain();
    refBuf.retain(3);
    // 1+1+3 = 5
    System.out.println("\nrefBuf增加引用计数后:" + refBuf.refCnt());
    refBuf.release();
    refBuf.release(3);
    System.out.println("refBuf释放引用计数后:" + refBuf.refCnt());

    /*转成Java NIO ByteBuffer*/
    System.out.println("\n************转成Java NIO ByteBuffer");
    ByteBuffer nioBuffer = resetBuf.nioBuffer();

    int remaining = nioBuffer.remaining();
    byte[] nioBytes = new byte[remaining];
    nioBuffer.get(nioBytes);
    System.out.println(new String(nioBytes));


    /*slice ByteBuf*/
    System.out.println("\ns************lice、duplicate ByteBuf************");
    ByteBuf sliceBuf = Unpooled.buffer();
    sliceBuf.writeBytes("SliceBuf".getBytes(StandardCharsets.UTF_8));
    ByteBuf slice1 = sliceBuf.slice();
    // 进行数据读取
    sliceBuf.readBytes(new byte[3]);
    System.out.printf("sliceBuf writerIndex:%d,readerIndex:%d,maxCapacity:%d\n"
            ,sliceBuf.writerIndex(),sliceBuf.readerIndex(),sliceBuf.maxCapacity());
    System.out.printf("slice1 writerIndex:%d,readerIndex:%d,maxCapacity:%d\n"
            ,slice1.writerIndex(),slice1.readerIndex(),slice1.maxCapacity());
    ByteBuf slice2 = sliceBuf.slice(1, 3);
    System.out.printf("slice2 writerIndex:%d,readerIndex:%d,maxCapacity:%d\n"
            ,slice2.writerIndex(),slice2.readerIndex(),slice2.maxCapacity());

    ByteBuf duplicate = sliceBuf.duplicate();
    System.out.printf("duplicate writerIndex:%d,readerIndex:%d,maxCapacity:%d\n"
            ,duplicate.writerIndex(),duplicate.readerIndex(),duplicate.maxCapacity());

}
java 复制代码
 byte[] bytes = new byte[byteBuffer.readableBytes()];
 byteBuffer.readBytes(bytes);
java 复制代码
byte[] bytes = ByteBufUtil.getBytes(byteBuffer);

使用ByteBuf.readBytes(bytes)会修改ByteBuf的readerIndex和writerIndex,而ByteBufUtil.getBytes(byteBuffer)则不会修改ByteBuf的readerIndex和writerIndex。

引导类ServerBootstrap、 Bootstrap

ServerBootstrap是一个搭建服务器端程序的助手类,Boostrap 则是一个搭建客户端程序的助手类。

服务端客户端代码示例如下:

java 复制代码
public class Server {
    public static void main(String[] args) {
        Server server = new Server();
        server.init();
    }

    private void init() {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline()
                                .addLast(new IdleStateHandler(20, 20, 20))
                                // 自定义处理器
                                .addLast(new ServerHandler());
                    }
                });
        try {
            ChannelFuture future = serverBootstrap.bind(9000).sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
java 复制代码
public class Client {
    public static void main(String[] args) {
        Client client = new Client();
        client.init();
    }

    private void init() {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline()
                                .addLast(new IdleStateHandler(10, 10, 10))
                                // 自定义处理器
                                .addLast(new ClientHandler());
                    }
                });

        try {
            ChannelFuture future = bootstrap.connect("localhost", 9000).sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            workerGroup.shutdownGracefully();
        }
    }
}

对比ServerBootstrapBootstrap构建的参数可以发现它们的异同。

  • group: 搭建服务端程序需要传入两个EventLoopGroup,一个称为boss,用于接受传入的连接,另一个称为worker,一旦boos接受连接并将连接注册到worker,就处理该连接的流量。
  • channel:对于TCP程序,服务端使用的是NioServerSocketChannel,客户端使用的是NioSocketChannel。同理如果是Sctp,服务端使用NioSctpServerChannel,客户端则是NioSctpChannel。UDP则都是NioDatagramChannel
  • optionchildOptionoption用于NioServerSocketChannel接受的连接配置相关参数,客户端同理,用于NioSocketChannel。服务端还有一个childOption方法,用于父ServerChannel连接的Channels。对于TCP/IP Server,可以使用参数ChannelOption.SO_KEEPALIVE用于是否保持长连接,默认是false。
  • handler:用于为请求提供服务的ChannelHandler。即自身请求。
  • childHandler:设置用于为Channel请求提供服务的ChannelHandler。即他人请求。
  • bind:服务端需要绑定一个端口用于接受客户端的连接。
  • connect:客户端需要连接服务器地址端口。
  • sync:Netty的IO操作是异步的,如果想操作执行完成之后在执行之后的代码,使用该方法异步转同步。

对于多个处理类,可以实现ChannelInitializer<SocketChannel>()接口,调用SocketChannelpipeline方法添加处理类,这里采用的责任链模式。

ChannelHandler接口

ChannelHandler是一个处理I/O事件或截获I/O操作,并将其转发到其ChannelPipeline中的下一个处理程序。其本身没有提供太多方法,所以通常需要实现其子类型。

以下是与ChannelHandler接口相关的部分类、接口关系图:

就像java中的InputStreamOutputStream一样,ChannelInboundHandler只处理入站数据,ChannelOutboundHandler只处理出站数据,也就是说如果没有在SocketChannelpipeline中添加相关出站类,那么接下来在使用ChannelHandlerContext#ChannelFuture writeAndFlush(Object msg)类似方法时候,都不能将数据输出到远端的主机

通常情况下,通过继承ChannelInboundHandlerAdapter类并重写相关方法来处理数据。其相关方法如下图:

其中ChannelHandlerContext关于fire前缀的方法表示将跳转到下一个处理器对应的相同名称的方法。

ChannelInboundHandlerAdapterChannelOutboundHandlerAdapter,ChannelDuplexHandler中msg默认的基本类型是ByteBuf。而SimpleChannelInboundHandler只允许显式处理特定类型的消息,如下面为一个只处理String类型的实现:

java 复制代码
public class StringHandler extends
              SimpleChannelInboundHandler<String> {
 
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String message)
              throws Exception {
        System.out.println(message);
    }
}

管道对于数据的入站和出站是分开的,顺序如下:

ps: Netty提供了LoggingHandler处理类用于打印相关入站出站数据,其默认日志等级为DEBUG,如果与Spring Boot集成可输出到控制台,需要设置LoggingHandler等级为INFO以上或者设置Spring Boot的日志输出级别为DEBUG.

编码、解码、拆包、粘包

对于TCP/IP是基于流的传输,接收到的数据存储到Socket的缓存区中,该缓存区是基于字节队列而不是数据包队列,即使将两条消息作为两个独立的数据包进行发送,操作系统也只会将它们视为一堆字节,而不是数据包。那么就不能保证接收数据的正确性。

以数据包为定义,而接收数据的形式为字节队列,那么可能出现以下几种情况:

第一种为正常情况:有两个数据包AB、CD分别发送。

第二种称为粘包:本该两个数据包分别发送,现在则是合成一个数据包一起发送。

第三、四、五种称为拆包:即数据AB、CD本该整体发送,而在传输过程中则AB被拆成了A、B发送,CD被拆成了C、D发送。

为了得到第一种情况,同时避免其他的异常产生,通信的双方需要协商数据按照的指定格式进行处理。 发送方将原始数据处理之后再发送给接收方,这个过程称为编码 。接收方接收发送的数据并按照规定的格式将数据还原成原始数据的过程,称为解码 。 所以一般情况下,不直接继承ChannelOutboundHandlerAdapter类用于输出数据, 而是在ChannelInitializer实现中按照合理的顺序添加合适的编解码处理器,再使用自定义的处理器来处理经过解码后的数据。

实现一个编码,解码处理器,有以下部分类,它们的关系如下:

  • MessageToMessageEncoder<I>:将一个消息编码成另一个消息
  • MessageToMessageDecoder<I>:将一个消息解码成另一个消息
  • ByteToMessageDecoder:类似流的方式将字节从一个ByteBuf解码到另一个Message类型。
  • MessageToByteEncoder<I>:以类似流的方式将消息从一条消息编码到ByteBuf

根据业务需要,继承上述相关类并重写decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)encode(ChannelHandlerContext ctx, I msg, ByteBuf out)方法。

Netty提供了一些协议编解码的相关实现,下面为其中的一部分:

HTTP协议:

  • HttpRequestEncoderHttpRequestDecoder
  • HttpResponseEncoderHttpResponseDecoder
  • HttpServerCodec相当于(HttpRequestDecoder+HttpResponseEncoder)、HttpClientCodec相当于(HttpResponseDecoder+HttpRequestEncoder)
  • HttpObjectAggregator:将HttpMessage及其后续的HttpContents聚合为一个单独的FullHttpRequest或FullHttpResponse。

Websocket协议:WebSocketServerProtocolHandlerWebSocketClientProtocolHandler

其他编码器:

  • DelimiterBasedFrameDecoder:通过一个或多个分隔符来分割接收到的字节。
  • FixedLengthFrameDecoder:通过固定的长度来分割字节。
  • LengthFieldBasedFrame:通过消息中长度字段的值动态拆分接收到的ByteBuf。对应的Netty提供了LengthFieldPrepender提供了偏移位0的长度字段的支持,长度允许1,2,3,4,8。
  • LineBasedFrameDecoder:用于将接收到的ByteBuf在行结尾处进行拆分,可同时处理"\n"和"\r\n"。

具体使用可以参考Netty 源码中的注释。

超时空闲处理

在一些情况下,通信的双方的其中一方断开连接,而另一方没有及时的探测到,从而导致某些业务出现问题,如连接断开重连。为了判断连接是否有效,一个做法就是监测连接对应的管道是否有空闲,也就是Channel是否有数据流量。

检测管道是否空闲,可以使用Netty实现的IdleStateHandler

IdleStateHandler提供了三个构造方法用于创建判断管道空闲策略,如果readerIdleTime设置为0,则表示禁用,其他两个参数同理。

同时,后续需要一个ChannelHandlerAdapter类重写userEventTriggered(ChannelHandlerContext ctx, Object evt)方法,如下:

java 复制代码
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    // 处理空闲事件 JDK17 写法
    if (evt instanceof IdleStateEvent event) {
        if (event.state() == IdleState.READER_IDLE) {
            ctx.close();
        }
    }
}

IdleStateHandler类有两个子类:ReadTimeoutHandlerWriteTimeoutHandler,两者分别只启用了读空闲监测、写空闲监测。如果使用子类的实例,而不是IdleStateHandler实例,可以不用重写userEventTriggered方法。子类有相关实现如ReadTimeoutHandler中的方法:

使用了上述的相关类,万一程序有段时间没有数据流量就会导致连接关闭,没有流量也是属于一种正常现象,所以需要定义协议用于防止出现上述情况,也就是心跳检测。比如客户端发送一个'ping'字符串,服务端接收到之后就给客户端发送一个'pong'字符串,客户端接收之后继续发送'ping',如此反复。

完整代码示例

java-skill-learn/netty-study at main · DawnSilverGravel/java-skill-learn (github.com)

参考文档

Netty 官方文档4.x

Netty 学习手册

相关推荐
救救孩子把1 分钟前
Java基础之IO流
java·开发语言
小菜yh3 分钟前
关于Redis
java·数据库·spring boot·redis·spring·缓存
宇卿.9 分钟前
Java键盘输入语句
java·开发语言
浅念同学9 分钟前
算法.图论-并查集上
java·算法·图论
立志成为coding大牛的菜鸟.22 分钟前
力扣1143-最长公共子序列(Java详细题解)
java·算法·leetcode
鱼跃鹰飞23 分钟前
Leetcode面试经典150题-130.被围绕的区域
java·算法·leetcode·面试·职场和发展·深度优先
爱上语文2 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people2 小时前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
qmx_073 小时前
HTB-Jerry(tomcat war文件、msfvenom)
java·web安全·网络安全·tomcat
为风而战3 小时前
IIS+Ngnix+Tomcat 部署网站 用IIS实现反向代理
java·tomcat