nio demo

java 里,我们如果想使用 nio 方式处理 io ,一般会这样写:

  1. // 打开服务端 Socket
  2. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  3. // 打开 Selector
  4. Selector selector = Selector.open();
  5. // 服务端 Socket 监听8080端口, 并配置为非阻塞模式
  6. serverSocketChannel.socket().bind(new InetSocketAddress(8080));
  7. serverSocketChannel.configureBlocking(false);
  8. // 将 channel 注册到 selector 中, 注册 OP_ACCEPT 事件
  9. // 然后在 OP_ACCEPT 到来时, 获取 NioSocketChannel,并将 NioSocketChannel
  10. // 注册到某个 Worker NioEventLoop 的 Selector 上
  11. // 注册 OP_READ 事件到 Selector
  12. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
  13. // 不断循环
  14. while (true) {
  15. //迭代selectedkey
  16. }

netty 工作原理

3751588-8220ae4d80809f08.png

netty 服务端启动过程

  1. 首先实例化NioEventLoopGroup,最终会初始化一个NioEventLoop数组,NioEventLoop是一个Executor,每一个NioEventLoop和一个线程联系。
  2. 当调用doBind绑定端口后,会进行一系列的初始化操作,例如初始化pipeline和channel的option,并且会把ServerSocketChannel注册到 NioEventLoop 的 selector 上,并且此时的 instrestOps 是SelectionKey.OP_ACCEPT,此时的ServerSocketChannel是处于监听状态。
  3. 然后会取出一个NioEventLoop,启动NioEventLoop,会在run方法中,在循环中监听事件(Accept,read,write)

accept 过程

image.png

  1. 当客户端进行 connect 后,服务端的 BossGroup 启动的 NioEventLoop 会捕获到 SelectionKey.OP_ACCEPT 事件
  2. 然后会调用 NioMessageUnsafe 的 read 方法来处理连接事件,然后会取出连接的客户端的 NioSocketChannel,然后触发 ChannelActive 事件,该事件也会在 ChannelPipelines 中传递,然后再触发客户端 NioSocketChannel 和服务端 NioServerSocketChannel 的 ChannelRead 操作
  3. 然后服务端的 ServerBootStrap 会为客户端的 NioSocketChannel 设置 childHandler 参数
  4. 紧接着会把取得客户端的 NioSocketChannel 通过 childGroup.register(child) 将 NioSocketChannel 注册到work的NioEventLoop 中,这个过程和 NioServerSocketChannel 注册到 boss 的 NioEventLoop 的过程一样,最终交付给 work 线程对应的 selector 进行 read 事件的监听。

read 操作

image.png

同样是在 NioEventLoop 中进行的,当 work 线程的 selector检测到 OP_READ 事件发生时,触发 ChannelRead 操作,read 操作完成后,该事件会在 Pipeline 中传递下去,给 Pipeline 的 Handler 依次处理

nio 底层实现

java nio 根据操作系统的不同,底层实现不同。在 Linux 系统上,在 kernel 版本大于 2.6 时,已经开始使用 epoll 的实现, EPollSelectorProvider.

netty 提供的 handler

Netty 提供了大量的系 统 ChannelHandler 供用户使用,比较实用的系统 ChannelHandler
总结如下:

  1. 系统编解码框架-ByteToMessageCodec;
  2. 通用基于长度的半包解码器-LengthFieldBasedFrameDecoder;
  3. 码流日志打印Handler-LoggingHandler;
  4. SSL安全认证Handler-SslHandler;
  5. 链路空闲检测Handler-IdleStateHandler;
  6. 流量整形Handler-ChannelTrafficShapingHandler;
  7. Base64编解码-Base64Decoder和Base64Encoder。

netty 线程模型

Netty 用于接收客户端请求的线程池职责如下:

(1)接收客户端 TCP 连接,初始化 Channel 参数;
(2)将链路状态变更事件通知给 ChannelPipeline。

Netty 处理 I/O 操作的 Reactor 线程池职责如下。

(1)异步读取通信对端的数据报,发送读事件到 ChannelPipeline;
(2)异步发送消息到通信对端,调用 ChannelPipeline 的消息发送接口;
(3)执行系统调用 Task;
(4)执行定时任务 Task,例如链路空闲状态监测定时任务。 通过调整线程池的线程个数、是否共享线程池等方式,

Netty 的 Reactor 线程模型可以在单线程、多线程和主从多线程间切换,这种灵活的配置方式可以最
大程度地满足不同用户的个性化定制。

最佳实践

  1. 时间可控的简单业务直接在 I/O 线程上处理
    时间可控的简单业务直接在 I/O 线程上处理,如果业务非常简单,执行时间非常短,不需要访问数据库和磁盘,不需要等待其它资源,则建议直接在业务 ChannelHandler 中执行,不需要再启业务的线程或者线程池。避免线程上下文切换,也不存在线程并发问题。

  2. 复杂和时间不可控业务建议投递到后端业务线程池统一处理
    复杂和时间不可控业务建议投递到后端业务线程池统一处理,对于此类业务,不建议直接在业务 ChannelHandler 中启动线程或者线程池处理,建议将不同的业务统一封装成 Task,统一投递到后端的业务线程池中进行处理。过多的业务 ChannelHandler 会带来开发效率和可维护性问题.

消息入栈出栈

  1. <pre>
  2. * I/O Request
  3. * via {@link Channel} or
  4. * {@link ChannelHandlerContext}
  5. * |
  6. * +---------------------------------------------------+---------------+
  7. * | ChannelPipeline | |
  8. * | \|/ |
  9. * | +---------------------+ +-----------+----------+ |
  10. * | | Inbound Handler N | | Outbound Handler 1 | |
  11. * | +----------+----------+ +-----------+----------+ |
  12. * | /|\ | |
  13. * | | \|/ |
  14. * | +----------+----------+ +-----------+----------+ |
  15. * | | Inbound Handler N-1 | | Outbound Handler 2 | |
  16. * | +----------+----------+ +-----------+----------+ |
  17. * | /|\ . |
  18. * | . . |
  19. * | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
  20. * | [ method call] [method call] |
  21. * | . . |
  22. * | . \|/ |
  23. * | +----------+----------+ +-----------+----------+ |
  24. * | | Inbound Handler 2 | | Outbound Handler M-1 | |
  25. * | +----------+----------+ +-----------+----------+ |
  26. * | /|\ | |
  27. * | | \|/ |
  28. * | +----------+----------+ +-----------+----------+ |
  29. * | | Inbound Handler 1 | | Outbound Handler M | |
  30. * | +----------+----------+ +-----------+----------+ |
  31. * | /|\ | |
  32. * +---------------+-----------------------------------+---------------+
  33. * | \|/
  34. * +---------------+-----------------------------------+---------------+
  35. * | | | |
  36. * | [ Socket.read() ] [ Socket.write() ] |
  37. * | |
  38. * | Netty Internal I/O Threads (Transport Implementation) |
  39. * +-------------------------------------------------------------------+
  40. * </pre>

netty 架构

image.png

第一层:Reactor 通信调度层,它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父 类、ByteBuffer 以及由其衍生出来的各种 Buffer、Unsafe 以及其衍生出的各种内 部类等。该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据 读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事 件、写事件等等,将这些事件触发到 PipeLine 中,由 PipeLine 充当的职责链来 进行后续的处理;

第二层:职责链 PipeLine,它负责事件在职责链中的有序传播,同时负责动态的 编排职责链,职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向 后/向前传播事件,不同的应用的 Handler 节点的功能也不同,通常情况下,往往 会开发编解码 Hanlder 用于消息的编解码,它可以将外部的协议消息转换成内部 的 POJO 对象,这样上层业务侧只需要关心处理业务逻辑即可,不需要感知底层 的协议差异和线程模型差异,实现了架构层面的分层隔离;

第三层:业务逻辑编排层,业务逻辑编排层通常有两类:一类是纯粹的业务逻辑 编排,还有一类是其它的应用层协议插件,用于特性协议相关的会话和链路管理, 例如 CMPP 协议,用于管理和中国移动短信的对接。

架构的不同层次,需要关心和处理的对象都不同,通常情况下,对于业务开发,只需要关心第二和第三层即可,由于应用层协议栈往往是开发一次,到处运 行。这样,实际上对于业务开发和使用者来说,只需要关心第三层的业务逻辑开 发即可。各种应用协议以插件的形式提供,只有协议开发人员关注和管理,其他 业务开发人员不需要关心。这种分层的架构设计理念实现了 NIO 框架各层之间的解耦,非常方便上层业务协议栈的开发和业务的定制。