Netty五大组件介绍

3 篇文章 0 订阅
1 篇文章 0 订阅

目录

一、netty五大组件

1.1 EventLoop

1.2 Channel && ChannelFuture

1.3 Future && Promise

1.4 Handler & Pipeline

1.5 ByteBuf 

 1.5.1 内容打印工具类

1.5.2 常用创建方式 

1.5.3 ByteBuf组成


一、netty五大组件

1.1 EventLoop

        EventLoop本质上是一个单线程执行器(在内部维护了一个线程和一个Selector),在线程内部的run方法处理Channel上的IO事件。

        它继承了三个父类:

  • 一个是JUC下面的ScheduledExecutorService,因此它用于线程池中所以的方法;
  • 一个是Netty自己提供的OrderedEventExecutor,提供了两个重要方法:boolean inEventLoop(Thread t)用于哦按的一个线程是否属于此EventLoop; EventLoopGroup parent()用于查看自己属于哪个EventLoopGroup。 EventLoopGroup是一组EventLoop,Channel会调用EventLoopGroup的register方法来绑定其中一个EventLoop,后续这个Channel上的所有IO事件都会由此EventLoop来处理(也就是单线程处理,保证IO事件处理时的线程安全问题)
  • 另一个是Netty自己提供的EventExecutorGroup,该接口实现了Iterable迭代器接口,能够遍历EventLoop,同时也提供了next方法能够获取集合中的下一个EventLoop。

代码示例:

@Slf4j
public class EventLoopServer {

    public static void main(String[] args) {
        //默认group,用于执行需要较长时间执行的handler,避免阻塞负责整个channel的EventLoop
        DefaultEventLoopGroup defaultGroup = new DefaultEventLoopGroup(2);
        new ServerBootstrap()
                //细分为boss和Worker,boss只负责ServerSocketChannel的accept事件,worker负责SocketChannel的读写事件
                .group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>(){
                    //连接之后执行
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast("handle1", new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf) msg;
                                log.info(buf.toString(Charset.defaultCharset()));
                                //如果是添加多handle,必须调用上下文的fireChannelxx()方法
                                //该方法会调用invokeChannelRead(findContextInbound(), msg)方法,invokeChannelRead中会首先获取下一个handler的执行线程,
                                //然后判断该线程是否属于当前EventLoop,若属于,则由当前线程直接调用,若不属于,则使用该线程调用
                                ctx.fireChannelRead(msg);
                            }
                        }).addLast(defaultGroup, "handler2 ", new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf) msg;
                                log.info(buf.toString(Charset.defaultCharset()));
                            }
                        });
                    }
                }).bind(8091);
    }
}

ctx.fireChannelRead(msg)逻辑:

 @Override
    public ChannelHandlerContext fireChannelRead(final Object msg) {
        invokeChannelRead(findContextInbound(), msg);
        return this;
    }

    static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelRead(m);
                }
            });
        }
    }

1.2 Channel && ChannelFuture

Channel主要方法:

  • close方法用来关闭Channel;
  • closeFuture,返回一个CloseFuture对象,可以基于sync()方法或者addListener方法实现在Channel的关闭之后执行指定逻辑;
  • pipeline方法用于添加handle处理器;
  • write方法用于将数据写入但不刷出;
  • writeAndFlush用于将数据写入并刷出;

ChannelFuture:继承了JUC下的Future接口,拥有异步返回结果的能力,是异步 Channel IO 操作的结果,Netty 中的所有 IO 操作都是异步的。这意味着任何 IO 调用都会立即返回一个 ChannelFuture 实例,但不能保证请求的 IO 操作在调用结束时已经完成,该实例提供有关 IO 操作的结果或状态的信息。 ChannelFuture 要么未完成,要么已完成。当 IO 操作开始时,会创建一个新的 future 对象。新的 future 最初是未完成的——它既没有成功,也没有失败,也没有取消,因为 IO 操作还没有完成。如果 IO 操作成功、失败或取消完成,则将来会标记为已完成,并提供更具体的信息,例如失败的原因。请注意,即使失败和取消都属于完成状态。

主要方法:

  • addListener(GenericFutureListener) :用于在ChannelFuture上添加一个listener,在 IO 操作完成时收到通知。建议尽可能首选 addListener(GenericFutureListener) 而不是 await(),以便在 IO 操作完成时获得通知并执行任何后续任务。 addListener(GenericFutureListener) 是非阻塞的。它只是简单地将指定的 ChannelFutureListener 添加到 ChannelFuture 中,当与 future 相关的 IO 操作完成时,IO 线程会通知监听器。
  • await():await() 是一个阻塞操作,一旦被调用,调用者线程就会阻塞,直到操作完成。注意不要在 ChannelHandler 内部调用 await() 方法,因为ChannelHandler 中的事件处理方法通常由 IO 线程调用,如果 await() 被 IO 线程调用的事件处理方法调用,它正在等待的 IO 操作可能永远不会完成,因为 await() 会阻塞它正在等待的 IO 操作,这是一个死锁。
  • sync():同步阻塞方法,让调用者线程同步等待,直到NIO线程连接建立后才会执行;

1.3 Future && Promise

        在异步处理时经常会用到这两个接口,netty中的Future继承自JDK中的Future,而Promise又对netty自己的Future做了扩展:

  • JDK中的Future只能同步等待任务结束(成功或失败)后才能得到结果,而netty中的Future既可以同步等待任务结束得到返回结果,也可以通过其它线程异步等待任务结束后返回结果,但都是要等待任务结束;
  • netty中的Promise不仅有Future的功能,还脱离了任务可以独自存在,只作为两个线程间传递结果的容器,可以主动创建,并设置结果和失败异常原因等功能。

        Future在多线程已经是比较常用的了,看下promise的代码示例:

@Slf4j
public class TestNettyPromise {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1. 准备EventLoop对象
        EventLoop eventLoop = new NioEventLoopGroup().next();
        //2. 主动构建一个promise,是一个结果容器;相比Future而言,Future无法主动构建,只能被动获取
        DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
        
        new Thread(() -> {
            //3. 任意一个线程开始执行计算,计算完毕后向promise填充结果
            log.info("开始计算");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                promise.setFailure(e);
            }
            promise.setSuccess(10);
        });
        
        //接收结果
        log.info("等待结果");
        log.info("计算完毕,结果是:{}", promise.get());
    }
}

1.4 Handler & Pipeline

        ChannelHandler是用于处理channel上各种事件的处理器,分为入站(Inbound)、出站(Outbound)两种。所有的ChannelHandler会组成一条长链,也就是pipeline(管道)。

  • 入站handler: 通常是ChannelInboundHandlerAdapter的子类,主要用来读取客户端发送的数据,并写回结果;
  • 出站handler: 通常是ChannelOutboundHandlerAdapter的子类,主要用于对写回结果进行加工

        通俗来讲,一个Channel就像一个产品的加工车间,而Pipeline是车间中的流水线,ChannelHandler是流水线上的各道工序,ByteBuf就是原材料,经过很多工序的加工最终变成产品(注意:通过pipeline在添加handler时一般都是使用的addLast方法,虽然看上去是在管道尾添加,但其实netty自动在管道头和尾添加了handler,所以我们添加的handler都是在中间)。由于handler在开发中顺序经常需要服务端与客户端一起联调,比较麻烦,所以netty提供了一个工具类EmbeddedChannel,能够模仿channel的入站和出站,示例代码如下:

/**
 * channelHandler执行顺序测试工具类
 * @create 2022/7/25 2:19 PM
 */
@Slf4j
public class TestEmbeddedChannel {

    public static void main(String[] args) {
        ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.info("inboundChannel-1");
                super.channelRead(ctx, msg);
            }
        };
        ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.info("inboundChannel-2");
                super.channelRead(ctx, msg);
            }
        };
        ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter() {
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.info("outboundChannel-3");
                super.write(ctx, msg, promise);
            }
        };
        ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter() {
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.info("outboundChannel-4");
                super.write(ctx, msg, promise);
            }
        };
        //构建一个EmbeddedChannel
        EmbeddedChannel embeddedChannel = new EmbeddedChannel(h1, h2, h3, h4);
        //模拟入站顺序
        embeddedChannel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello,inbound".getBytes()));
        //模拟出站顺序
//        embeddedChannel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("world,otbound".getBytes()));
    }
}

1.5 ByteBuf 

        ByteBuf是netty对NIO中的ByteBuffer进行了一个扩展包装,新增了可动态扩容等一系列的优点。

 1.5.1 内容打印工具类

/**
 * ByteBuf调试工具类
 * @create 2022/7/25 2:34 PM
 */
public class ByteBufLogUtil {

    public static void log(ByteBuf buf){
        int length = buf.readableBytes();
        int rows = length / 16 + (length%15 == 0 ? 0:1) +4;
        StringBuilder builder = new StringBuilder(rows * 80 * 2)
                .append("read index:").append(buf.readerIndex())
                .append("  write index:").append(buf.writerIndex())
                .append("  capacity:").append(buf.capacity())
                .append(NEWLINE);
        appendPrettyHexDump(builder, buf);
        System.out.println(builder.toString());
    }
}

1.5.2 常用创建方式 

        通常使用ByteBufAllocator.DEFAULT.buffer()来创建获取,但其实ByteBuf区分为池化、非池化、直接内存与堆内存等多种创建方式。

  •         池化基于直接内存的ByteBuf:
 ByteBuf buf = ByteBufAllocator.DEFAULT.directBuffer();
  •         池化基于堆内存的ByteBuf:         
ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();

        通常直接内存创建和销毁的代价是否昂贵,但由于少一次内存复制,其读写性能也比较高,适合配合池化功能使用;同时直接内存对JVM GC压力小,不受JVM垃圾回收的管理,但是要注意及时主动释放。而堆内存创建和销毁效率高,但其读写性能较差。所以默认情况下,netty是使用的直接内存。

        池化:池化的意义在于能够重用ByteBuf,与直接内存联用,不用每次使用都重新创建和销毁,性能较高;并且池化在分配内存时采用了与Jemalloc类似的内存分配算法来提升分配效率,在面对高并发时,能有效节约内存,减小内存溢出的可能。池化功能的开启通过系统环境变量来设置:

-Dio.netty.allocator.type={unpooled|pooled}

在版本4.1以后,非Android 平台默认启用池化实现,Android平台启用非池化实现;4.1之前默认非池化。 

1.5.3 ByteBuf组成

        ByteBuf由以下部分组成:初始容量、最大容量(默认Integer最大值)、读指针、写指针;

  •         废弃区域:读指针之前的区域,也就是已读的区域,可以通过调用 discardReadBytes()去丢弃这部分;
  •         可读部分:读指针与写指针之间的区域;
  •         可写部分:写指针与初始容量之间的区域;
  •         可扩容部分:初始容量与最大容量之间的区域;

常用方法:        

1)读写:

方法签名含义备注
writeBoolean(boolean value)写入boolean值用一字节0|1表示false|true
writeByte(int value)写入byte值
writeShort(int value)写入short值
writeInt(int value)写入int 值(默认)Big Endian,即0x250,写入后00 00 02 50
writeIntLE(int value)写入int 值Little Endian,即0x250,写入后50 02 00 00
writeBytes(ByteBuf src)写入netty中的ByteBuf
writeByte(byte[] src)写入byte数组
writeBytes(ByteBuffer src)写入nio中的ByteBuffer
writeCharSequence(CharSequence sequence, Charset charset)写入指定字符集的字符串
readByte()读取一个字节
readInt()读取一个int
还有一系列的set/get方法,与ByteBuffer中类似,可以写入值和读取值,但是不会影响readIndex和writeIndex

        在写入数据时若容量不够会自动进行扩容,扩容遵循以下两点规则:

  1. 如果写入后数据大小未超过512,则选择下一个16的整数倍,例如写入后大小为26,则扩容后的capacity为32; 
  2. 如果写入后数据大小超过512,则选择下一个2^n,例如写入后大小为511,则扩容后的capacity为512;
  3. 如果扩容后的capacity超过Integer.MAXVALUE,会报错

2)内存释放(retain & release):

        netty中一般使用的直接内存ByteBuf,直接内存需要手动来释放,而不是等GC垃圾回收。在Netty中采用了引用计数法来控制内存回收,每个ByteBuf都实现了ReferenceCounted接口:

  • 每个ByteBuf对象的初始计数都为1
  • 调用release方法计数减1,当计数减到0时,代表此ByteBuf内存被回收
  • 调用retain方法计数加1,代表此ByteBuf正在使用中,即使其它handler调用了release也不会回收此ByteBuf
  • 当ByteBuf底层内存被回收后,即使ByteBuf对象还在,但该对象的方法已无法正常使用
  • 那个Handler最后使用了ByteBuf,则由那个Handler调用release方法;虽然在netty内部的head handler和tair handler调用了release方法,但若该ByteBuf没有传递过去,也会无法释放,因此还是需要手动调用release。

3)slice:

        零拷贝的体现之一,对原始ByteBuf进行切片成多个ByteBuf,切片后的ByteBuf并没有发生内存复制,还是使用原始ByteBuf的内存,只是切片后的ByteBuf维护了各自独立的readIndex、writeIndex,修改原始ByteBuf同时也会对切分后的ByteBuf造成影响,例如:释放了原始ByteBuf,切分后的ByteBuf也无法使用,但是可以切片后的ByteBuf调用retain方法,消除此影响;并且切片后的容量是有限制的,不可以再进行写入。

  • slice():不传参数默认从readIndex开始切分
  • slice(int index, int length):从index开始,切分length长度

4)duplicate:

        零拷贝的体现之一,截取了原始ByteBuf所有内容,并且没有max capacity的限制,也是原原始ByteBuf使用同一块底层内存,但是读写指针独立。

5)copy:

        与零拷贝相反会将底层内存数据进行深拷贝,因此无论读写都与原始ByteBuf无关。

6)compositeBuffer:

        也是零拷贝的体现之一,创建compositeBuffer生成一个复制buffer,然后调用addComponents方法添加ByteBuf,将添加的ByteBuf组合到新的compositeBuffer中,避免了内存复制,例如:addComponents(true, buf1, buf2),同时也要调用一次retain方法消除同一块内存带来的影响。 

7)Unpooled:

        Unpooled是一个工具类,提供了非池化的ByteBuf创建、组合和复制操作。其中的wrappedBuffer可以用来包装ByteBuf或者普通字节数组,当wrappedBuffer中的参数个数超过一个时,底层会使用compositeByteBuf方法:wrappedBuffer(buf1, buf2)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值