nio demo
java 里,我们如果想使用 nio 方式处理 io ,一般会这样写:
// 打开服务端 Socket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 打开 Selector
Selector selector = Selector.open();
// 服务端 Socket 监听8080端口, 并配置为非阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
// 将 channel 注册到 selector 中, 注册 OP_ACCEPT 事件
// 然后在 OP_ACCEPT 到来时, 获取 NioSocketChannel,并将 NioSocketChannel
// 注册到某个 Worker NioEventLoop 的 Selector 上
// 注册 OP_READ 事件到 Selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 不断循环
while (true) {
//迭代selectedkey
}
netty 工作原理
netty 服务端启动过程
- 首先实例化NioEventLoopGroup,最终会初始化一个NioEventLoop数组,NioEventLoop是一个Executor,每一个NioEventLoop和一个线程联系。
- 当调用doBind绑定端口后,会进行一系列的初始化操作,例如初始化pipeline和channel的option,并且会把ServerSocketChannel注册到 NioEventLoop 的 selector 上,并且此时的 instrestOps 是SelectionKey.OP_ACCEPT,此时的ServerSocketChannel是处于监听状态。
- 然后会取出一个NioEventLoop,启动NioEventLoop,会在run方法中,在循环中监听事件(Accept,read,write)
accept 过程
- 当客户端进行 connect 后,服务端的 BossGroup 启动的 NioEventLoop 会捕获到 SelectionKey.OP_ACCEPT 事件
- 然后会调用 NioMessageUnsafe 的 read 方法来处理连接事件,然后会取出连接的客户端的 NioSocketChannel,然后触发 ChannelActive 事件,该事件也会在 ChannelPipelines 中传递,然后再触发客户端 NioSocketChannel 和服务端 NioServerSocketChannel 的 ChannelRead 操作
- 然后服务端的 ServerBootStrap 会为客户端的 NioSocketChannel 设置 childHandler 参数
- 紧接着会把取得客户端的 NioSocketChannel 通过 childGroup.register(child) 将 NioSocketChannel 注册到work的NioEventLoop 中,这个过程和 NioServerSocketChannel 注册到 boss 的 NioEventLoop 的过程一样,最终交付给 work 线程对应的 selector 进行 read 事件的监听。
read 操作
同样是在 NioEventLoop 中进行的,当 work 线程的 selector检测到 OP_READ 事件发生时,触发 ChannelRead 操作,read 操作完成后,该事件会在 Pipeline 中传递下去,给 Pipeline 的 Handler 依次处理
nio 底层实现
java nio 根据操作系统的不同,底层实现不同。在 Linux 系统上,在 kernel 版本大于 2.6 时,已经开始使用 epoll 的实现, EPollSelectorProvider.
netty 提供的 handler
Netty 提供了大量的系
统 ChannelHandler 供用户使用,比较实用的系统 ChannelHandler
总结如下:
- 系统编解码框架-ByteToMessageCodec;
- 通用基于长度的半包解码器-LengthFieldBasedFrameDecoder;
- 码流日志打印Handler-LoggingHandler;
- SSL安全认证Handler-SslHandler;
- 链路空闲检测Handler-IdleStateHandler;
- 流量整形Handler-ChannelTrafficShapingHandler;
- Base64编解码-Base64Decoder和Base64Encoder。
netty 线程模型
Netty 用于接收客户端请求的线程池职责如下:
(1)接收客户端 TCP 连接,初始化 Channel 参数;
(2)将链路状态变更事件通知给 ChannelPipeline。
Netty 处理 I/O 操作的 Reactor 线程池职责如下。
(1)异步读取通信对端的数据报,发送读事件到 ChannelPipeline;
(2)异步发送消息到通信对端,调用 ChannelPipeline 的消息发送接口;
(3)执行系统调用 Task;
(4)执行定时任务 Task,例如链路空闲状态监测定时任务。
通过调整线程池的线程个数、是否共享线程池等方式,
Netty 的 Reactor 线程模型可以在单线程、多线程和主从多线程间切换,这种灵活的配置方式可以最
大程度地满足不同用户的个性化定制。
最佳实践
时间可控的简单业务直接在 I/O 线程上处理
时间可控的简单业务直接在 I/O 线程上处理,如果业务非常简单,执行时间非常短,不需要访问数据库和磁盘,不需要等待其它资源,则建议直接在业务 ChannelHandler 中执行,不需要再启业务的线程或者线程池。避免线程上下文切换,也不存在线程并发问题。复杂和时间不可控业务建议投递到后端业务线程池统一处理
复杂和时间不可控业务建议投递到后端业务线程池统一处理,对于此类业务,不建议直接在业务 ChannelHandler 中启动线程或者线程池处理,建议将不同的业务统一封装成 Task,统一投递到后端的业务线程池中进行处理。过多的业务 ChannelHandler 会带来开发效率和可维护性问题.
消息入栈出栈
<pre>
* I/O Request
* via {@link Channel} or
* {@link ChannelHandlerContext}
* |
* +---------------------------------------------------+---------------+
* | ChannelPipeline | |
* | \|/ |
* | +---------------------+ +-----------+----------+ |
* | | Inbound Handler N | | Outbound Handler 1 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* | | \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler N-1 | | Outbound Handler 2 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ . |
* | . . |
* | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
* | [ method call] [method call] |
* | . . |
* | . \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler 2 | | Outbound Handler M-1 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* | | \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler 1 | | Outbound Handler M | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* +---------------+-----------------------------------+---------------+
* | \|/
* +---------------+-----------------------------------+---------------+
* | | | |
* | [ Socket.read() ] [ Socket.write() ] |
* | |
* | Netty Internal I/O Threads (Transport Implementation) |
* +-------------------------------------------------------------------+
* </pre>
netty 架构
第一层:Reactor 通信调度层,它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父 类、ByteBuffer 以及由其衍生出来的各种 Buffer、Unsafe 以及其衍生出的各种内 部类等。该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据 读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事 件、写事件等等,将这些事件触发到 PipeLine 中,由 PipeLine 充当的职责链来 进行后续的处理;
第二层:职责链 PipeLine,它负责事件在职责链中的有序传播,同时负责动态的 编排职责链,职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向 后/向前传播事件,不同的应用的 Handler 节点的功能也不同,通常情况下,往往 会开发编解码 Hanlder 用于消息的编解码,它可以将外部的协议消息转换成内部 的 POJO 对象,这样上层业务侧只需要关心处理业务逻辑即可,不需要感知底层 的协议差异和线程模型差异,实现了架构层面的分层隔离;
第三层:业务逻辑编排层,业务逻辑编排层通常有两类:一类是纯粹的业务逻辑 编排,还有一类是其它的应用层协议插件,用于特性协议相关的会话和链路管理, 例如 CMPP 协议,用于管理和中国移动短信的对接。
架构的不同层次,需要关心和处理的对象都不同,通常情况下,对于业务开发,只需要关心第二和第三层即可,由于应用层协议栈往往是开发一次,到处运
行。这样,实际上对于业务开发和使用者来说,只需要关心第三层的业务逻辑开
发即可。各种应用协议以插件的形式提供,只有协议开发人员关注和管理,其他
业务开发人员不需要关心。这种分层的架构设计理念实现了 NIO 框架各层之间的解耦,非常方便上层业务协议栈的开发和业务的定制。