写点什么

Netty 源码学习 4——服务端是处理新连接的 &netty 的 reactor 模式

  • 2023-11-20
    福建
  • 本文字数:3848 字

    阅读完需:约 13 分钟

零丶引入


在前面的源码学习中,梳理了服务端的启动,以及 NioEventLoop 事件循环的工作流程,并了解了 Netty 处理网络 io 重要的 Channel ,ChannelHandler,ChannelPipeline。


这一篇将学习服务端是如何构建新的连接。


一丶网络包接收流程



当客户端发送的网络数据帧通过网络传输到网卡时,网卡的 DMA 引擎将网卡接收缓冲区中的数据拷贝到 DMA 环形缓冲区,数据拷贝完成后网卡硬件触发硬中断,通知操作系统数据已到达。


随后网卡中断处理程序将 DMA 环形缓冲区的数据拷贝到 sk_buffer,sk_buffer 位于内核中,它提供了一个缓冲区,使得网卡中断程序可以将他接收到的数据暂存起来,避免数据丢失和切换。


随后发起软中断,网络协议栈会处理数据包,对数据包进行解析,路由,分发(根据目的端口号,分发给对应的应用程序,通过网络编程套接字,应用程序可以监听指定端口号,并接受网络协议栈的数据包)


  • 当新的连接建立时,网络协议处理栈会将这个连接的套接字标记为可读,并生成一个 accept 事件,这个事件通知应用程序有新的连接需要处理


  • 当已经建立的连接上有数据到达时,网络协议处理栈会将套接字标记为刻度,并生成一个 read 事件,这个事件通知应用程序有数据可供读取


  • 当应用程序向已经建立的连接写入数据时,如果写缓冲区有足够的空间,写操作会立即完成,不会产生 write 事件。但如果写缓冲区已满,那么写操作将被暂停,当写缓冲区有足够的空间时,write 事件将被触发,通知应用程序可以继续写入数据。


也就是说 netty 服务端程序会监听不同的网络事件,并进行处理,这也是源码学习的切入点!


二丶服务端 NioEventLoop 处理网络 IO 事件



如上是 NioEventLoop 的运行机制,在《Netty源码学习2——NioEventLoop的执行》中我们进行了大致流程的学习,这一篇我么主要关注其 run 中处理网络 IO 事件的部分。



无论是否优化,最终都是拿到就绪的 SelectionKey,循环处理每一个就绪的网络事件,如下便是处理的逻辑:



可以看到无论是 accept 事件还是 read 事件都是调用 AbstractNioChannel 的 Unsafe#read 方法

Unsafe 是对 netty 对底层网络事件处理的封装,下面我们先看下 AbstractNioChannel 的类图,可以看到 NioServerSocketChannel,和 NioSocketChannel 都使用继承了 AbstractNioChannel,只是父类有所不同



那么 NioServerSocketChannel 和 NioSocketChannel 是什么时候 Accept or read 事件感兴趣的昵?


三丶 NioServerSocketChannel 设置对 accept 事件感兴趣


重点在 ServerBootstrap#bind 中,此方法会调用 doBind0



doBind0 会调用 Channel#bind,然后处理 ChannelPipeline#bind 的执行,由于 bind 是出站事件,将从 DefaultChannelPipeline 的 TailContext 开始执行,然后调用到 HeadContext#bind 方法,最终会调用 NioServerSocketChannel 的 unsafe#bind 方法


如下是 NioServerSocketChannel 的 unsafe#bind 的内容:



主要完成两部分操作:


  • 调用 java 原生 ServerSocketChannel#bind 方法,进行端口绑定,这样操作系统网络协议栈在分发网络数据的时候,才直到该分发到这个端口的 ServerSocketChannel


  • 向 EventLoop 中提交一个 pipeline.fireChannelActive()的任务,将在 pipeline 上触发 channelActive 方法,HeadContext#channelActive 将被调用到



  • 这里将调用到 Channel#read 方法,最终会调用到 HeadContext#read



四丶服务端处理 Accept 事件


前面我们说到,NioEventLoop 处理 accept 事件和 read 事件都是调用 unsafe#read 方法,如下是 NioServerSocketChannel#unsafe 的 read 方法


  public void read() {            assert eventLoop().inEventLoop();            final ChannelConfig config = config();            final ChannelPipeline pipeline = pipeline();            final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();            allocHandle.reset(config);
boolean closed = false; Throwable exception = null; try { try { do { //读取数据 int localRead = doReadMessages(readBuf); if (localRead == 0) { break; } if (localRead < 0) { closed = true; break; } // 计数 allocHandle.incMessagesRead(localRead); } while (continueReading(allocHandle)); } catch (Throwable t) { exception = t; } int size = readBuf.size(); for (int i = 0; i < size; i ++) { readPending = false; // 触发channelRead pipeline.fireChannelRead(readBuf.get(i)); } readBuf.clear(); allocHandle.readComplete(); // 触发channelReadComplete pipeline.fireChannelReadComplete();
// 省略 } finally { // 省略 } }
复制代码


这里出现一个 RecvByteBufAllocator.Handle,这里不需要过多关注,在 NioServerSocketChannel 建立连接的过程中,它负责控制是否还需要继续读取数据



ServerSocketChannel 类提供了 accept()方法,用于接受客户端的连接请求,返回一个 SocketChannel 代表了一个底层的 TCP 连接。



如上将 jdk SocketChannel 包装 NioSocketChannel 的时候会设置 SocketChannel 非阻塞并在属性 readInterestOp 记录感兴趣事件为 read


包装生成的 NioSocketChannel 会放到 List 中,后续每一个就绪的连接会一次传播 ChannelRead,并最终传播 ChannelReadComplete



1.channeRead 事件的传播


上面说到 NioEventLoop 读取 NioServerSocketChannel 上的 accept 事件,将每一个新连接封装为 NioServerChannel 后,将依次触发 channelRead。


如下是 ServerBootstrapAcceptor#channelRead 方法,可以看到它会将读取生成的 NioServerChannel 注册到 childGroup,这里的 childGroup 就是 ServerBootstrap 启动时候指定 EventLoopGroup(主从 reactor 模式中的从 reactor)



也就是说主 reactor 负责处理 accept 事件,从 reactor 负责处理 read 事件


2.channelReadComplete 事件传播


大多数人看到 channelReadComplete 都会认为这是 Netty 读取了完整的数据,然而有时却不是这样。channelReadComplete 其实只是表明了本次从 Socket 读了数据,该方法通常可以用来进行一些收尾工作,例如发送响应数据或进行资源的释放等。channelReadComplete 方法在每次读取数据完成后,即使没有更多的数据可读,也会被调用一次。


五丶 netty 对多种 reactor 模式的支持


这里其实可以看出 netty 对多种 reactor 模式(单线程,多线程,主从 reactor)的支持



我们其实可以通过修改 bossGroup,和 workerGroup 使 netty 使用不同的 reactor 模式


六丶将 NioSocketChannel 注册到从 reactor


上面我们说到主 reactor 监听 accept 事件后传播 channelRead 事件,最终由 ServerBootstrapAcceptor 调用 childGroup#register 将包装生成的 NioSocketChannel 注册到从 reactor(也就是 workerGroup——EventLoopGroup)下面我们看看这个注册会发生什么



首先 workerGroup 这个 EventLoopGroup 会调用 next 方法选择出一个 EventLoop 执行 register,然后


  • 将 NioSocketChannel 中的 jdk SockectChannel 注册到 Selector 中,并将 NioSocketChannel 当作附件,这样 selector#select 到事件的时候,可以从附件中拿到网络事件对应的 NioSocketChannel



  • 触发 handlerAdd



  • 这一步触发 ChannelHandler#handlerAdded


  • 最终会调用到 childHandler 中指定的 ChannelInitializer,它会将我们指定的 ServerHandler(这里可以扩展我们的业务处理逻辑)加到 NioSockectChannel 的 pipeline 中



  • 触发 ChannelRegistered


  • 触发 channelActive


  • 由于这是一个新连接,是第一次注册到 EventLoop,因此会触发 channelActive


  • 这将调用到 DefaultChannelPipeline 的 HeadContext#readIfIsAutoRead,最终就和我们第三节的【NioServerSocketChannel 设置对 accept 事件感兴趣】差不多——HeadContext#readIfIsAutoRead 会调用 NioSockectChannel 的 read 方法,最终调用到 NioSockectChannel#unsafe 的 read 方法——将注册对 read 事件感兴趣



七丶再看 Netty 的 Reactor 模式



笔者认为 netty 的 reactor 有以下几个要点


  • ServerBootstrap#bind 方法


  • 不仅仅会绑定端口,还会触发 channelActive 事件,从而使 DefaultChannelPipeline 中的 HeadContext 触发 netty channel unsafe#beginRead,注册 ServerSockectChannel 对 accept 感兴趣


  • NioEventLoop 处理新连接


  • 这一步 Netty 使用 Selector 进行 IO 多路复用,当 accept 事件产生的时候,调用NioServerSocketChannel#unsafe的read方法,这一步会将新连接封装 NioSocketChannel,然后将对应连接的套接字注册到 Selector 上,然后传播 channeRead 事件


  • ServerBootstrapAcceptor 对 channeRead 事件的处理


  • 笔者认为这是 netty reactor 模式的核心,它将 NioSocketChannel 注册到从 reactor 上,让子 reactor 负责处理 NioSocketChannel 上的事件,并最终注册 SocketChannel 对 read 事件感兴趣!


和 tomcat 的 reactor(《Reactor 模式与Tomcat中的Reactor 》)有异曲同工之妙,只是 netty Pipeline 的设计让整个流程更具备扩展性,当然也增加了源码学习的复杂度 doge


文章转载自:Cuzzz

原文链接:https://www.cnblogs.com/cuzzz/p/17842964.html

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Netty源码学习4——服务端是处理新连接的&netty的reactor模式_Netty_快乐非自愿限量之名_InfoQ写作社区