Netty设计模式与源码分析(三)

accept事件

NioEventLoop.run方法,最终执行processSelectedKeys方法,因为此时我们有客户端连接事件,我们有accept事件后就会有SelectedKeys,我们此时就会操作SelectedKeys。

 private void processSelectedKeys() {
    
    
     if (selectedKeys != null) {
    
    
         //执行这里
         processSelectedKeysOptimized();
     } else {
    
    
         processSelectedKeysPlain(selector.selectedKeys());
     }
 }
private void processSelectedKeysOptimized() {
    
    
    for (int i = 0; i < selectedKeys.size; ++i) {
    
    
        final SelectionKey k = selectedKeys.keys[i];
        // null out entry in the array to allow to have it GC'ed once the Channel close
        // See https://github.com/netty/netty/issues/2363
        selectedKeys.keys[i] = null;

        final Object a = k.attachment();

        if (a instanceof AbstractNioChannel) {
    
    
            processSelectedKey(k, (AbstractNioChannel) a);
        } else {
    
    
           ****************************
        }
        *************************************
    }
}

当有客户端accept事件时会走这段逻辑
在这里插入图片描述
NioServerSocketChannel会调用NioMessageUnsafe.read方法。
在这里插入图片描述
NioMessageUnsafe.read方法,我们主要关注doReadMessages与pipeline.fireChannelRead(readBuf.get(i));方法,如图所示
在这里插入图片描述

NioServerSocketChannel.doReadMessages

因为此时我们处理的时accept事件,服务端这里接收到的是客户端的SocketChannel,并把SocketChannel封装成netty中的NioSocketChannel。

在这里插入图片描述

public NioSocketChannel(Channel parent, SocketChannel socket) {
    
    
    super(parent, socket);
    config = new NioSocketChannelConfig(this, socket.socket());
}

继续调用父类

此时监听的是read事件,是不是跟我们的Nio一致了。继续调用父类,把read事件注册到SocketChannel上,并发SocketChannel与netty的NioSocketChannel关联上。并调用父类初始化pipeline与unsafe。

protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
    
    
     super(parent, ch, SelectionKey.OP_READ);
 }

将刚刚封装的NioSocketChannel存入到readBuf集合中。
在这里插入图片描述

pipeline.fireChannelRead(readBuf.get(i))

执行pipeline的ChannelRead方法,注意这个pipeline是NioServerSocketChannel的pipeline,我们前面说过了,此时的pipeline大约是这个样子的。这个参数是我们在集合取的数据就是NioSocketChannel,前面已经讲过,此时会执行pipeline中ChannelRead方法,因为head的ChannelRead还是调用父类的方法,所以默认继续调用fireChannelRead(就是下一个inbound handler的ChannelRead方法),所以我们主要关注ServerBootstrapAcceptor的ChannelRead方法。

head --> ServerBootstrapAcceptor --> tail
在这里插入图片描述
回顾下代码
前面将服务端注册时执行的这段逻辑,把ServerBootstrapAcceptor加入到了pipeline中,注意,currentChildHandler参数就是我们自己定义的ChannelInitializer对象。我们现在还没有将ChannelInitializer里的handler加入到handler中。
在这里插入图片描述

在这里插入图片描述

ServerBootstrapAcceptor.ChannelRead

在这里插入图片描述
通过前面的代码我们知道childHandler就是我们创建的ChannelInitializer对象,就是这个,
在这里插入图片描述
此时客户端的NioSocketChannel的pipeline是怎么样子的呢。

head -> ChannelInitializer(这个是我们自己定义的ChannelInitializer对象) - > tail

注意,此时ChannelInitializer的initChannel还没有被调用,所以我们自定义的handler还没有加入到pipeline中,我们继续看代码。

childGroup.register

刚刚说完pipeline,我们继续看代码,childGroup是什么,就是work线程组。
在这里插入图片描述
从work线程组取一个NioEventLoop执行register方法。

@Override
 public ChannelFuture register(Channel channel) {
    
    
     return next().register(channel);
 }

同NioServerSockerChannel

@Override
 public ChannelFuture register(Channel channel) {
    
    
     return register(new DefaultChannelPromise(channel, this));
 }

最终调用到这里,把NioEventLoop与NioSocketChannel关联起来,再执行注册事件,此时注册的是read事件。
在这里插入图片描述
NioSocketChannel注册事件

这几步骤代码再回顾一下
在这里插入图片描述
doRegister();
NioSocketChannel获取jdk底层的SocketChannel注册read事件到selector上,注意,这个selector是NioEventLoop关联的selector。

pipeline.invokeHandlerAddedIfNeeded();
通过前面的分析,我们知道现在NioSocketChannel的pipeline是这个样子的

head -> ChannelInitializer - > tail

此步就会调用ChannelInitializer的initChannel方法,然后会把ChannelInitializer删除掉。此时的NioSocketChannel pipeline是这个样子的

head - > serverHandler - > tail
在这里插入图片描述
pipeline.fireChannelRegistered()

客户端注册成功方法

从head开始,执行下一个inbound handler的ChannelRegistered方法,如果我们的handler重写了ChannelRegistered方法此时会执行。细节debug代码。

pipeline.fireChannelActive();

客户端连接活跃执行的方法

从head开始,执行下一个inbound handler的ChannelActive方法,如果我们的handler重写了ChannelActive方法此时会执行。细节debug代码。

到这里accept事件就注册完成了

read事件

当客户端与服务端建立链接后,也就是accept后,此时需要客户端发起数据请求,现在执行的是read事件,注意,此时执行read事件的线程是work线程,只有work线程才会处理读写事件,boss只负责accept事件并将事件传递个work并注册读事件。work线程数是cpu核心数的2倍。因为accept事件非常快,一个线程就可以处理成千上万个请求,而work线程则是根据next方法一个一个取值。一个一个执行。
在这里插入图片描述
read事件跟前面一样了,先在直接内存中读取数据,直接内存好处,节省从内核空间与用户空间的复制事件。

这里的pipeline是NioSocketChannel的,也就是这个样子的

head - > serverHandler - > tail

byteBuf = allocHandle.allocate(allocator):获取直接内存
allocHandle.lastBytesRead(doReadBytes(byteBuf));:将channel数据写入到直接内存的buffer中

pipeline.fireChannelRead(byteBuf)
执行开始读数据方法,从head开始执行下一个handler,并把buf当作参数传欸

pipeline.fireChannelReadComplete();
读取数据结束的方法,从head开始执行下一个hander

注意:如果handler想要向下传递,需要执行pipeline.fireXXXXX()方法

在这里插入图片描述

至此,现在netty连接事件已经完成。

总结

从netty学习到了什么

  • 主从Reactor线程模型
  • NIO多路复用非阻塞
  • 无锁串行化设计思想
  • 支持高性能序列化协议
  • 零拷贝(直接内存的使用)
  • ByteBuf内存池设计
  • 灵活的TCP参数配置能力
  • 并发优化

支持高性能序列化协议

支持想java对象序列化,可以直接传java 对象,netty会自动进行序列化与
反序列化

无锁串行化设计思想

      在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于
共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。
为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理
尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和
同步锁。
       为了尽可能提升性能,Netty采用了串行无锁化设计,在IO线程内部进行
串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU
利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启
动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多
个工作线程模型性能更优。
       Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的
fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop
调用到用户的Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操
作导致的锁的竞争,从性能角度看是最优的。

直接内存的使用

优点

  • 不占用堆内存空间,减少了发生GC的可能j
  • ava虚拟机实现上,本地IO会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)

缺点

  • 初始分配较慢
  • 没有JVM直接帮助管理内存,容易发生内存溢出。为了避免一直没有FULL GC,最终导致直接内存把物理内存被耗完。我们可以指定直接内存的最大值,通过-XX:MaxDirectMemorySize来指定,当达到阈值的时候,调用system.gc来进行一次FULL GC,间接把那些没有被使用的直接内存回收掉。

ByteBuf内存池设计

    随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级
    的工作。但是对于缓冲区Buffer(相当于一个内存块),情况却稍有不同,特别是
    对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,
    Netty提供了基于ByteBuf内存池的缓冲区重用机制。需要的时候直接从池子里
    获取ByteBuf使用即可,使用完毕之后就重新放回到池子里去。

示例:我们读取从channel读取数据到buf中时就是用的内存池+直接内存形式的。

并发优化

  • volatile的大量、正确使用;
  • CAS和原子类的广泛使用;
  • 线程安全容器的使用;
  • 通过读写锁提升并发性能。

流程图

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_37904966/article/details/111304910
今日推荐