今天,聊一聊Netty源码。Netty在我们Java圈中挺出名的,因为其高性能,号称支持百万的客户端连接,因此,很多框架通讯都是采用了Netty,如Dubbo、Zookeeper、Spring WebFlux等等。简单来说,Netty实际上是对Java原生NIO的封装,因为Java提供的原生NIO用起来确实不太方便,因此Netty做了封装,使得我们在使用Netty的时候,写一些模板性的代码即可,并且只需要添加一个或多个我们自己写好的ChannelHandler类,在对应的方法中,写好业务逻辑即可,而不必关注NIO使用上的一些细节。先看看我写的,以Netty服务端为例,如下图所示:
这是一个最简单的Netty Server,并且只有NettyServerHandler是我写的,它继承自SimpleChannelInboundHandler抽象类,如下图所示:
回到NettyServer类中,先看NioEventLoopGroup,这是Netty提供的类,可以把它理解为是线程池,当然和我们常用的线程池还是有很大的不同的,创建NioEventLoopGroup,该类提供了有参构造和无参构造,如果是有参的话,传入一个具体的数值,就是指明创建多少个NioEventLoop,如果是无参构造的话,默认是创建CPU核心数×2,这块将在代码中体现,如下图所示:
以上就是NioEventLoopGroup构造方法的调用链路,其中,在它的父类MultithreadEventLoopGroup中,就会判断传入的nThreads是否为空,为空则是CPU核心数×2,不为空则以传入的nThreads为准,如下图所示:
在该构造方法中,先是判断传入的executor是否为空,为空则初始化一个ThreadPerTaskExecutor对象并赋值给executor,看看ThreadPerTaskExecutor,如下图所示:
由此可以看到,调用ThreadPerTaskExecutor#execute()方法,传入一个Runnable对象,它就会把我们传入的Runnable对象做一下包装(实际上就是创建一个Thread对象,把Runnable对象作为入参传入),并调用start方法,启动线程。回到MultithreadEventExecutorGroup的有参构造中,继续往下看,然后就是初始化他的成员属性children,它是一个EventExecutor类型的数组,接着就是根据我们传入的线程数做循环,在循环中调用MultithreadEventExecutorGroup#newChild()方法,给数组中的每一个元素赋值,看看该方法,如下图所示:
创建的是NioEventLoop,调用的是有参构造,传入了NioEventLoopGroup对象、ThreadPerTaskExecutor对象、SelectorProvider对象、DefaultSelectStrategyFactory对象、RejectedExecutionHandler对象、EventLoopTaskQueueFactory对象、EventLoopTaskQueueFactory对象。进到构造方法中看看,如下图所示:
构造中调用了父类的构造,最好也点进去看看,可能会有一些初始化的逻辑,如下图所示:
上图中的父类构造中,初始化了一个taskQueue成员属性,这是一个阻塞队列,通常用来做异步操作的,挺重要的,先记下来,后续很可能会用到,继续看父类的构造,如下图所示:
到上图就没什么可看的了,就是把NioEventLoopGroup做了一下赋值而已。到这里,NioEventLoopGroup的构造方法,及其父类的构造方法全部看完了,回到NettyServer类中:如下图所示:
接着就是创建ServerBootstrap对象,并且通过链式调用,给ServerBootstrap属性赋值,包括调用ServerBootstrap#group()方法,将上面创建的两个NioEventLoopGroup对象传入,可以点进去看看,如下图所示:
再看看父类的group()方法:
这里就可以记录一下:我们创建了两个NioEventLoopGroup对象,分别是 bossGroup 和 workerGroup,分别给ServerBootstrap的group属性和childGroup属性进行了赋值。再看看ServerBootstrap#channel()方法,传入的是NioServerSocketChannel.class,如下图所示:
再看看,最终就是给ServerBootstrap的channelFactory属性赋值,赋值的对象是ReflectiveChannelFactory,创建该对象的时候调用的是有参构造,传入的就是NioServerSocketChannel.class,这个有参构造就不看了,后续还会使用到ReflectiveChannelFactory。再看看ServerBootstrap#childHandler()方法,传入ChannelInitializer的实现类,如下图所示:
就是把传入的ChannelInitializer的实现类赋值给childHandler属性。以上就是ServerBootstrap类创建后的赋值,再来看ServerBootstrap#bind()方法,这个方法很重要,涉及到了Netty怎么启动注册并且绑定端口,点进去看看,如下图所示:
最终调用到了ServerBootstrap#doBind()方法,在该方法中发有一个ServerBootstrap#initAndRegister()方法,一般方法中带有"init"、"start"等单词的需要重点关注下,点到该方法中看看它干了些啥,如下图所示:
发现用到了ServerBootstrap的channelFactory属性,这个属性不就是前面我说的ReflectiveChannelFactory类的对象吗,那就看看ReflectiveChannelFactory#newChannel()方法,如下图所示:
会发现就干了一件事,通过反射创建了某个对象,而这个constructor其实就是之前传入的NioServerSocketChannel.class的默认构造,因此创建的也是NioServerSocketChannel对象,可以看看NioServerSocketChannel类的构造方法,看看构造方法中有没有做一些初始化,或者赋值之类的,如下图所示:
在上图中,比较核心的就是NioServerSocketChannel#newChannel()方法,通过这个方法可以得到一个ServerSocketChannel对象,而这个对象实际上就是JDK提供的类。如果我们不用Netty,直接用JDK提供的原生类开发NIO,就必然会用到这个类,所以说,Netty本质上就是用JDK原生的NIO做了封装,使之用起来更简单方便。在最后的有参构造中就接收了ServerSocketChannel对象,并且构造方法中调用了父类的构造,传入了该对象和SelectionKey.OP_ACCEPT,第二个参数,看过JDK原生代码的应该知道,这表示,服务端接收的是客户端的连接请求,除了SelectionKey.OP_ACCEPT之外,还有下图中的几个:
分别表示不同的ServerSocketChanne/SocketChannel上发生的不同事件类型。另外一个就是创建的是NioServerSocketChannelConfig对象,并给属性config赋值,调用的是它的有参构造,传了两个参数:一个是NioServerSocketChannel它自己,另一个是ServerSocketChannel对象,就是之前通过NioServerSocketChannel#newChannel()方法所得到的,如下图所示:
再看看NioServerSocketChannel父类的javaChannel()方法,即AbstractNioChannel#javaChannel()方法,如下图所示:
这个ch属性,实际上就是在之前调用NioServerSocketChannel父类,在父类中赋值的,具体如下图所示:
这个ch实际就是ServerSocketChannel对象,所以AbstractNioChannel#javaChannel()方法得到的也就是ServerSocketChannel对象,通过ServerSocketChannel#socket()方法,得到的就是ServerSocket对象。ServerSocket对象就是真正跟客户端建立连接,并生成服务端的Socket对象,这样就可以跟客户端通信了。AbstractNioMessageChannel的构造,又调用了父类的构造,继续往上看,如下图所示:
在这里就可以看到,ch就是在这里赋值的,并且调用ch的方法,即SelectableChannel#configureBlocking()方法,传入false,表示以非阻塞的形式运行,不这样设置会报错,并把SelectionKey.OP_ACCEPT赋值给了readInterestOp属性。再看看父类,继续往上看,如下图所示:
看上面的构造,可知传入的parent属性是null,并且初始化了两个非常重要的属性,即:unsafe和pipeline,看看这两个属性,具体赋值的哪两个对象。先看unsafe,现在NioServerSocketChannel找一下newUnsafe()方法,发现没有,在它的父类AbstractNioMessageChannel找一下,刚好就找到了,如下图所示:
因此,unsafe属性实际上赋值的是NioMessageUnsafe对象;再看看pipeline属性,同样的找法,发现newChannelPipeline()方法,就在本类中,即AbstractChannel类中,如下图所示:
因此,pipeline属性是DefaultChannelPipeline对象,并传入了this,即NioServerSocketChannel对象。又看了一下NioMessageUnsafe和DefaultChannelPipeline,发现NioMessageUnsafe的构造中没什么逻辑,但是DefaultChannelPipeline是有以下逻辑,需要点进DefaultChannelPipeline的构造中看看,如下图所示:
初始化了两个成员属性:head和tail,head是HeadContext对象,tail是TailContext对象,都是调用的有参构造,传入的是this,即DefaultChannelPipeline。然后通过next属性和prev属性,把head和tail关联了起来,说白了就是构成了head和tail的双向链表。其中HeadContext和TailContext都是继承自AbstractChannelHandlerContext,如下图所示:
进入AbstractChannelHandlerContext的构造中看看,如下图所示:
也没做什么,就只是做了下简单的赋值,如给pipeline属性赋值,还有executor,不过executor传入的为null,等等。到现在为止,NioServerSocketChannel的初始化就讲完了,回到ServerBootstrap#initAndRegister()方法中,看到了ServerBootstrap#init()方法,如下图所示:
看看ServerBootstrap#init()方法,如下图所示:
里面主要是拿到ServerBootstrap的两个属性childGroup和childHandler,然后调用pipeline的addLast()方法,传入一个ChannelInitializer对象,在ChannelInitializer#initChannel()方法中,最核心的也是调用了调用pipeline的addLast()方法,放入了一个ServerBootstrapAcceptor对象。继续往下看,然后对于catch中的内容属于分支逻辑,暂时不用看,看看ServerBootstrap#config()方法,返回的是什么对象,如下图所示:
看看config属性是在哪里赋值的,如下图所示:
可以知道,直接就创建了ServerBootstrapConfig对象,调用有参构造传入了this,即ServerBootstrap对象。最终,调用ServerBootstrapConfig#group()方法,实际上就是调用的是ServerBootstrapConfig中的bootstrap属性的group()方法,而bootstrap就是刚刚我们传入的ServerBootstrap,即调用ServerBootstrap#group()方法,得到的就是之前我们创建的NioEventLoopGroup bossGroup属性,如下图所示:
最终调用的就是EventLoopGroup#register()方法,传入的是NioServerSocketChannel对象。看看EventLoopGroup#register()方法是什么逻辑,如下图所示:
NioEventLoopGroup的父类是MultithreadEventLoopGroup,因此看MultithreadEventLoopGroup#register()方法,如下图所示:
看看MultithreadEventLoopGroup#next()方法,如下图所示:
这个chooser,实际上就是之前在MultithreadEventExecutorGroup的有参构造中初始化的,并且把初始化的children属性传进去了,这个属性是EventExecutor数组,如下图所示:
因此这个选择器就会从传入的EventExecutor数组中按照一定的规则,选出一个EventExecutor来,也就是NioEventLoop对象,回到register()方法中,如下图所示:
SingleThreadEventLoop就是NioEventLoop的父类,如下图所示:
看看AbstractChannel#register()方法,传入了NioEventLoop对象和DefaultChannelPromise对象,如下图所示:
看这个方法,里面有两个if判断逻辑,进入之后都是直接返回了,这种的话,其实没必要看了,一般不会进去,不然这个register()方法就相当于啥事没干了。然后就是把传入的NioEventLoop对象赋值给eventLoop属性。下面又是一个判断if/else的判断,这个需要看一下,其实不管是if还是else,最终都会调用 AbstractChannel#register0()方法,想抓主线逻辑的话可以直接看这个方法。不过还是看看if中的条件吧,如下图所示:
由于thread此时还是为空的,当然就不等于”当前线程”了,所以if中的逻辑为false,进入else逻辑,继续往下看,就调用到了的NioEventLoop#execute()方法,传入一段λ表达式,实际上就是Runnable对象,Runnable#run()方法就只是调用了AbstractChannel#register0()方法,传入DefaultChannelPromise对象。点到该方法中看看,如下图所示:
可知最终进入到了execute()方法的两个的入参的重载方法,里面又调用了两个方法,分别是:SingleThreadEventExecutor#addTask()方法 和 SingleThreadEventExecutor#startThread()方法,当然,SingleThreadEventExecutor是NioEventLoop的父类。前一个方法把Runable作为入参传进去了,如下图所示:
最终是把Runnable对象放入了taskQueue属性中,它是阻塞队列,之前在创建NioEventLoop对象的时候在它的构造方法中初始化的。再看后一个方法,如下图所示:
这里面中只有一个if逻辑,直接看里面的逻辑即可,里面有一个SingleThreadEventExecutor#doStartThread()方法,这个方法看起来就像是真正有注册的业务逻辑,继续进去看看,如下图所示:
方法很长,没截取完,核心就是通过SingleThreadEventExecutor的executor属性调用executor#execute()方法,传入Runnable对象,启动一个线程,在Runnable对象的run()方法中,又是一大段的try/catch逻辑,catch里面是分支逻辑,暂时不用看,只然后就是把当前线程赋值给了SingleThreadEventExecutor的thead属性,这样的话,后续SingleThreadEventExecutor#inEventLoop()方法,就返回true了。再看SingleThreadEventExecutor#inEventLoop()方法,就返回true了。SingleThreadEventExecutor.this#run()方法,如下图所示:
该方法,又是一大段逻辑,其中一段是switch来处理的,如果有小伙伴接触或Java的NIO,会有这么个方法:selector.select(),实际上就是调用操作系统的epoll相关的系统函数,意思是获取那些有读/写事件发生的Socket,进而进行处理。因此这里可以大胆的猜一下,switch满足的是SelectStrategy.SELECT条件,里面有一个重要的逻辑,是判断taskQueue队列是否为空,前面不是已经往里面放了一个Runnable对象吗,因此不满足if的条件。继续往下,又是一段if/else的逻辑,判断ioRatio和strategy的值,这种其实就可以打断点看看,静态看源码就不适用了,这里就是进入到下面的方法,如下图所示:
进入到NioEventLoop#processSelectedKeys()方法中看看,如下图所示:
这里判断selectedKeys是否为空,这个之前在创建NioEventLoop对象的时候,构造中赋值过了,所以不为空,所以进入到if逻辑中,继续往下看,如下图所示:
由于此时还没有客户端连接过来,所以selectedKeys集合为空,跳出for循环。回到NioEventLoop#run()方法,如下图所示:
由于是finally,所以NioEventLoop#runAllTasks()方法一定会执行,进入该方法看看,如下图所示:
由上图可知最终就是从taskQueue队列中拿到之前我们传入的Runnable对象,并调用Runnable#run()方法,所以最后还是要回到AbstractChannel#register()方法,如下图所示:
在该方法中,AbstractChannel#doRegister()方法,点进去看看,如下图所示:
在该方法中,核心的只有一行:就是调用AbstractNioChannel#javaChannel()方法,拿到ServerSocketChannel对象,再调用AbstractNioChannel#eventLoop()#unwrappedSelector()方法,拿到一个Selector对象,并将ServerSocketChannel对象注册到Selector对象上,传入0,表示ServerSocketChannel对象对连接事件”感兴趣”。再回到AbstractChannel#register0()方法中,如下图所示:
先看看DefaultChannelPipeline#invokeHandlerAddedIfNeeded()方法,如下图所示:
再看看DefaultChannelPipeline# callHandlerAddedForAllHandlers()方法,如下图所示:
先看看DefaultChannelPipeline的pendingHandlerCallbackHead属性是怎么赋值的:
发现是调用了DefaultChannelPipeline#callHandlerCallbackLater()方法,再看看这个方法是在哪里被调用的:
调用的地方很多,先随便点进去看看,以第一个为例:
原来是调用了DefaultChannelPipeline#addFirst()方法,但是这个方法我们没有用到,我们用到的是DefaultChannelPipeline#addLast()方法,可以找一下这个之前调用这个方法的地方,往下找,如下图所示:
最终也会看到DefaultChannelPipeline#callHandlerCallbackLater()方法,传入的是DefaultChannelHandlerContext和true,点进方法中看看,如下图所示:
由于传入的最终就是added为true,则创建的是PendingHandlerAddedTask对象,调用它的有参构造,传入DefaultChannelHandlerContext对象。如果多次调用DefaultChannelPipeline#addLast()方法,有代码可知,会构建一个单向链表。回到DefaultChannelPipeline#callHandlerAddedForAllHandlers()方法中,如下图所示:
就是调用PendingHandlerAddedTask#exeute()方法,并且通过next属性找到下一个PendingHandlerAddedTask对象,继续调用PendingHandlerAddedTask#exeute()方法,直到链表的尾节点为止。看看PendingHandlerAddedTask#exeute()方法,如下图所示:
再看DefaultChannelPipeline#callHandlerAdded0()方法,如下图所示:
看看AbstractChannelHandlerContext#handler()方法返回的是什么对象:实际上也跟之前调用DefaultChannelPipeline#addLast()方法有关,具体调用链路如下:
由此可以看出,调用的是ChannelInitializer#handlerAdded()方法,如下图所示:
ChannelInitializer#initChannel()方法实际上是一个抽象方法,供子类实现的,也就是我们创建ChannelInitializer的实现类的时候,实现的initChannel()方法,在该方法的finally中,调用了pipeline.remove(this),移除了ChannelInitializer对象,说白了ChannelInitializer类的作用就只是往pipeline中添加ChannelHandler,如下图所示:
也就是在这里面,通过传入的SocketChannel,拿到pipeline,进而把我们写的ChannelHandler加入到pipeline的双向链表中。可以是看看怎么添加的,如下图所示:
可以看出,创建的DefaultChannelHandlerContext对象,调用其有参构造,传入ChannelHandler对象和DefaultChannelPipeline对象,并且DefaultChannelHandlerContext对象会放在tail结点之前,并通过next和prev进行关联。再回到AbstractChannel#register0()方法,如下图所示:
再看看pipeline#fireChannelRegistered()方法中,调用的是AbstractChannelHandlerContext.invokeChannelRegistered()方法,传入的是head,即HeadContext对象,记住,下面会用到。如下图所示:
AbstractChannelHandlerContext#invokeChannelRegistered()方法中,有if/else的逻辑,看看应该走哪个逻辑,看看AbstractChannelHandlerContext#invokeHandler()方法,如下图所示:
看看handlerState属性的值,实际上在前面已经赋值了,如下图所示:
由上图可知,是通过CAS赋值的。回到AbstractChannelHandlerContext#invokeChannelRegistered()方法中,走的是if逻辑中的代码,如下图所示:
上面传入的是head,因此应该进入HeadContext#channelRegistered()方法,如下图所示:
DefaultChannelPipeline#invokeHandlerAddedIfNeeded()方法不会被调用,看看该方法为什么不会被调用,如下图所示:
firstRegistration为false,因为在AbstractChannel#register0()方法中调用过了,并且将firstRegistration设置成了false。再看第二个方法,即HeadContext#fireChannelRegistered()方法,如下图所示:
看看AbstractChannelHandlerContext#findContextInbound()方法,如下图所示:
也就是拿到HeadContext的下一个节点,也就是我们添加的自定义的ChannelHandler所在的DefaultChannelHandlerContext,如果没有添加,得到的下一个节点就是TailContext,并返回。再调用AbstractChannelHandlerContext#invokeChannelRegistered()方法,传入的则是HeadContext的下一个节点,如下图所示:
简单来说:就是从head节点开始,一个个往后调用,直到调用的tail为止,同时会调用到我们自己写的ChannelHandler#channelRegistered()方法,如果有实现的话,如下图所示:
就可以执行我们的业务逻辑了。总结一下:如果我们编写的ChannelHandler实现了ChannelInboundHandler,则调用顺序是从head–>…–>tail;如果实现的是ChannelOutboundHandler,则调用顺序是从tail–>…–>head,并且都会经过我们自己写的ChannelHandler。当然,这仅对于方法名是"fireXxxx"类似这种,如下图所示:
再回到AbstractChannel#register0()方法中,如下图所示:
pipeline.fireChannelActive()方法的调用逻辑亦是如此,只不过pipeline.fireChannelActive()方法和pipeline.fireChannelRegistered()方法一样,只会在客户端连接到服务端的时候,调用一次。其实这个时候,还没有真正的将我们自己编写的ChannelHandlerf放入pipeline中,因为此时还没有客户端连接,也就不会调用到ServerBootstrapAcceptor#channelRead()方法。
这时,也只是在ServerBootstrap#init()方法中,将ServerBootstrapAcceptor对象所在的DefaultChannelHandlerContext对象加入到pipeline,准确地讲,是NioServerSocketChannel的pipeline,如下图所示:
那在什么时候,才会真正的触发ServerBootstrapAcceptor#channelRead()方法呢?这还是要回到NioEventLoop#processSelectedKeysOptimized()方法中,如下图所示:
看看NioEventLoop#processSelectedKey()方法中,如下图所示:
假设此刻有一个客户端发起了连接,这时候selector#select()方法,就会感知到一个SelectionKey.OP_ACCEPT的事件,服务端就会得到一个与这个客户端进行通讯的NioSocketChannel,会进到图中框起来的if逻辑中(SelectionKey.OP_READ或者SelectionKey.OP_ACCEPT都会进入到if中),进而调用unsafe.read()方法,如下图所示:
前面讲过,这个NioServerSocketChannel的unsafe属性实际上就是AbstractNioMessageChannel的子类,继续看AbstractNioMessageChannel#read()方法,如下图所示:
进入AbstractNioMessageChannel#doReadMessages()方法中,传入一个List readBuf对象,如下图所示:
再看看SocketUtils#accept()方法,如下图所示:
这不就是Java NIO的原生代码吗?得到的是SocketChannel对象,不就是前面我说的“服务端就与客户端进行通讯的NioSocketChannel”吗。然后就会将SocketChannel对象放入readBuf中,回到 AbstractNioMessageChannel#read()方法中,如下图所示:
就会到用到pipeline.fireChannelRead()方法,而入参则是SocketChannel对象,也包括后面的 pipeline.fireChannelReadComplete()方法,如果发生了异常,则调用pipeline.fireExceptionCaught()方法,而调用pipeline.fireChannelReadComplete()方法,最终就会调用到ServerBootstrapAcceptor#channelRead()方法,进而将我们自己编写的ChannelHandler放入NioSocketChannel中的pipeline属性中,如下图所示:
在ServerBootstrapAcceptor#channelRead()方法还有一个核心的方法,即:childGroup.register()方法,入参则是SocketChannel对象,而这个childGroup实际上就是之前我们创建的EventLoopGroup workerGroup,如下图所示:
由于也是EventLoopGroup,因此注册逻辑跟上文中注册NioServerSocketChannel类似,这里就不多讲了。不同的是NioSocketChannel继承自AbstractNioByteChannel,而NioServerSocketChannel继承自AbstractNioMessageChannel,因此,AbstractNioChannel#read()方法的实现是不同的,如下图所示:
AbstractNioMessageChannel#read()前文已讲过,这里不再赘述,关键是看看AbstractNioByteChannel#read()方法,也就是客户端的发送数据到服务端的时候,会调用该方法,如下图所示:
从上图中可以看出,客户端发送的数据,会被写入到ByteBuf对象中,最后再调用pipeline。fireChannelRead()方法,传入ByteBuf对象,然后我们自己编写的ChannelHandler实现了channelRead()方法,就可以拿到客户端发送过来的数据了,进而处理我们自己的业务逻辑,如下图所示:
再回到AbstractNioByteChannel#read()方法中,再看看allocHandle.allocate()方法,如下图所示:
在AbstractByteBufAllocator#ioBuffer()方法中,实际上进的是if逻辑,最终得到是的directBuffer,而不是heapBuffer。当然,也可以配置Netty参数 ,让它不进if逻辑,一般不这样做,因为directBuffer使用的是零拷贝技术,也就是说,客户端发送的数据,到了网卡,然后操作系统将网卡的数据,读到内存中,而此时,在堆中,比如directBuffer对象其实只是有一个类似于“address”的属性,它存储的是一个引用地址,指向了客户端发送的数据所存储的内存地址,相比于heapBuffer,它其实内部会有一个byte数组属性,用于存储客户端发送的数据,这种的话,就需要把客户端发送的数据所存储的内存中再次拷贝到堆内存中,并储存在byte数组中。这样的话,相比于directBuffer,heapBuffer就多了一次数据从内存–>堆的拷贝,同时服务端向客户端发送数据,也是多了一次从堆内存–>内存的拷贝,降低了速度。并且,不管是堆内存–>内存的拷贝,还是内存–>堆的拷贝,Java还会调用Linux的系统函数处理数据,这还涉及到了用户态到内核态之间的来回切换,同样也降低了速度。因此,Netty使用零拷贝技术,也是Netty号称可以支撑百万连接的原因之一。
最后做一个小的总结,下图是我在网上找的,充分说明了Netty框架的运行模式,它是基于Reactor模型的主从架构:需要创建两个线程组EventLoopGroup,即bossGroup和workerGroup,一般bossGroup设置的线程组中线程的数量为1(也可以设置>1,如果这样设置的话,就需要绑定其他的端口,设置几个,绑定几个端口),workerGroup设置的线程组是多个,不设置默认创建的线程数量为 CPU核心数×2。并且,bossGroup中的线程会处理NioServerSocketChannel,NioServerSocketChannel只处理连接事件,当有客户端连接过来,则会产生一个对应的NioSocketChannel,并从workerGroup中按照一定的算法(默认是轮询),从中选择一个线程,处理NioSocketChannel,而NioSocketChannel主要是处理读写事件,用户发过来数据,接收数据,并且通过我们自己编写的ChannerHandler去处理,并将处理的结果,再通过NioSocketChannel返回给客户端。至于是怎么感知到读写事件,这就依赖于Linux的epoll系统函数,这块以后有机会再聊聊。以上,就是我对Netty框架的一些拙见,如有错误,欢迎各位大佬批评指正,我会及时修正,在此感激不尽!