netty学习(二)Netty线程模型与核心组件
1.I/O线程模型
1.1 传统阻塞IO模型
传统阻塞IO模型就是每一个请求都需要一个线程来进行处理,这个处理包括两方面:
- 连接的处理
- 业务的处理
有如下问题:
- 当请求过多时,创建的线程也会过多,那么线程的切换也会很频繁,上下文处理造成资源浪费
- 操作系统能处理的线程是有限制的,当线程达到限制后,新的请求就会被阻塞
- 单个线程大部分时间都是阻塞的,因为此请求并没有数据可以读写,造成了资源浪费
1.2 Reactor模型
针对传统IO模型的问题:reactor模型有如下解决方案:
- 使用IO复用模型,使得一个线程可以处理多个请求
- 基于线程池来复用线程资源,减少线程的创建造成的资源浪费,同时可以达到负载均衡的效果
如图,reactor模式将请求的连接处理和业务处理分开,使得只有一个ServiceHandler对象进行阻塞,当有线程的业务需要处理时,才从线程池里取线程进行业务处理,这样就大大减少了线程的阻塞。
Reactor中包含两大核心组件:
- reactor:即上图中的ServiceHandler,负责监听和分发事件(阻塞)
- Handler:即上图中的EventHandler,负责具体的业务处理(非阻塞)
1.2.1 单Reactor单线程
单 reactor单线程即将所有的处理交给一个线程,通过多路复用处理所有的IO操作(就是之前的NIO),优点是编码比较简单(只有一个线程),缺点是单线程无法处理高并发(在handler处理一个连接的业务时无法处理其他事件),且当线程以外终止会导致整个线程不可用,可靠性不高。
1.2.2 单Reactor多线程
单reactor多线程即每一个handler只负责事件的读取和返回,而具体的业务处理则交给woker线程处理,优点时充分利用了多核处理器的性能(采用多线程),缺点是虽然并发量比单线程高很多,但是reactor仍存在性能瓶颈(reactor仍在一个线程中,handler的数量很多)
1.2.3 主从Reactor多线程
所以就再分咯,建立一个reactor主线程和多个reactor子线程,主线程只负责连接的建立和handler的分发,将handler分配给子线程进行处理,这样就不会存在单线程下的性能瓶颈。
1.3 Netty线程模型
netty线程模式是基于主从reactor多线程模型的,我们直接来捋一下流程
- netty中抽象出了两组线程池,BossGroup专门负责客户端的连接,WokerGroup专门负责读写
- BossGroup中的每一个线程中都包含了一个selector选择器和一个任务队列,然后进入一个accept事件循环中
- accept事件循环包含三个步骤,a. 调用select得到所有待处理的accept请求 b. 将这些待处理的accept请求注册到WorkerGroup中的线程的selector中 c. 处理任务队列的任务
- WorkerGroup中的线程与BossGroup中线程的流程一致,但监听的是read/write事件,且step 2会将读写任务交予对应的Channel处理器处理
- 每一个WokerGroup中的线程处理业务时会使用pipeline,其中包含了多个channel处理器
2. Netty核心组件
2.1 事件的接收
2.1.1 channel接口
channe时javaNIO中的组件(前面已经讲过了),它代表了一个连接事件的载体(一个文件FileChannel、一个套接字SocketChannel等)
2.1.2 EventLoop接口
EventLoopGroup中包含了多个EventLoop,以达到多线程处理的效果。
EventLoop处理了多个连接生命周期中所发生的事件,而且每一个EventLoop只能绑定一个线程。
这就好比与我们学习NIO中时的主线程,其中也包含了两个组件:selector和TaskQueue。selector即之前学习的选择器,TaskQueue里可以存放我们需要异步处理的任务,后面会详细讲解。
2.1.3 ChannelFuture接口
在Netty中,所有的IO操作都是异步的,Netty提供了ChannelFuture接口让我们很方便的执行异步操作,该接口有addListener方法和removeListener方法用于添加和删除监听者,当操作完成时(不论成功还是失败)都会给监听者发送通知。
2.2 事件的处理
2.2.1 ChannelHandler接口
ChannelHandler是一个由事件触发的处理应用程序逻辑的容器,我们在编写代码时大部分的业务逻辑都在这里完成,最为经常使用的是ChannelInboundHandler子接口,该接口接收和处理入站事件和数据,也可以冲刷数据发送给客户端,这点在后面的实战中会大量使用。
2.2.2 ChannelPipeline接口
pipeline管道即一个操作的流程,所以在ChannelPipeline中就包含了多个ChannelHandler事件流,他们的执行顺序由添加顺序所决定。
2.3 数据容器ByteBuf
还记得我们使用NIO时使用的数据容器ByteBuffer吗,该容器是一个双向的读写容器,可以与channel进行数据交互。
而在netty中,实现了一个ByteBuf类来代替ByteBuffer,ByteBuf中提供了大量的API供我们使用,且数据结构的不同也使得效率较高。
2.3.1 数据结构的区别
在ByteBuffer中,实现双向读写的方式是flip方法,每一次切换时,都要调用一次flip方法来将指针进行调整。
而在ByteBuf中的数据结构是这样的:
*<pre>
* +-------------------+------------------+------------------+
* | discardable bytes | readable bytes | writable bytes |
* | | (CONTENT) | |
* +-------------------+------------------+------------------+
* | | | |
* 0 <= readerIndex <= writerIndex <= capacity
* </pre>
ByteBuf分别为读和写都设置了一定的区域,这样就避免了切换。
ByteBuf允许我们像使用字节数组一样来对其进行使用。
2.4 编解码之Protobuf
在netty中自带了很多的编解码器用于在网络中进行传输,比如常用的StringEncode/StringDecode(对字符串进行编解码)和ObjectEncode/ObjectDecode(对java对象进行编解码)。但是Object编解码器存在如下问题:1.无法跨语言,只能是java对象 2.使用的是java的序列化技术,效率不高
Protobuf是Google发布的开源项目,是一种轻便级高效的结构化数据存储格式,可以用于结构化数据序列化。很适合做数据存储或者RPC的数据交换格式。
ProtoBuf支持跨平台跨语言(支持c++、java、python、go等)