目录
2.3.6 flip(), rewind() 和 clear()
2.5 Direct Buffer 和 Non-Direct Buffer
学习netty的起因
再对照rocketmq与kafka中发现rocketmq使用了netty的框架(看起来比kakfa的纯写更加高效),后发现根本看不懂其代码,遂与之学习此框架,在官网看其demo后,对demo更加迷惑(完全看不懂在干什么) ,此之前对nio中的架构channal,selector了解一知半解,遂全部重新了解其特性。
其实各个组件只要涉及到socket之间交互,其就万变不离其综,遂总结如下。
该文章中的所有demo均在配套的gitlab中,代码中均有注释和包含一些常用方法。
为什么从NIO->reactor->netty
我认为是一个层层相扣的过程,设计思路层层递进,从而能想明白怎么做,又为何如此做,算是一个完整的学习过程。
一.NIO
Java NIO是Java 1.4引进的异步IO
图一.NIO概念图
NIO有如下三个概念
Channel/Buffer/Selector
这三个概念在讲述下面区别时讲解含义
其与IO区别就在于3点
- 基于Strem(流,IO),基于Buffer(NIO)
- 阻塞与非阻塞
- NIO有Selector(NIO的选择器)
1.1基于 Stream 与基于 Buffer
基于流的IO好理解,比如打开一个文件流(例如FileStream),之后read,就是从前往后的顺序读,也就是说不能随意改变读写位置的指针。
而基于Buffer的不同,基于Buffer的需要打开的是Channel,之后将Channel的数据搬运到Buffer中(读操作,写操作相反),Buffer中就可以随机位置读取,写入。
1.2阻塞和非阻塞
阻塞就是不返回知道能够读到数据,非阻塞就是读不到立即返回失败。
1.3 Selector
也就是选择器,可以简单的认为多个Channel的总控制器,一旦有哪个Channel有消息,送信号量给Selector,Selector选择这个Channel执行相关程序,可以看到多个Channel只需要一个线程去管理,可以很简单的理解为一种多路复用的实现方式。
二.Buffer
需要用NIO Channal时,就必须要使用NIO Buffer,即数据从Buffer读取到channel,或者从channel中写入到buffer中。
Buffer拥有读模式和写模式(需要clear()和flip()方法转换,初创默认为写模式)
Buffer其实就可以看做一块内存区域,我们不过就是在这个内存区域中进行读写罢了,Buffer就是对该内存区域以及一些规定的操作方法的封装。
2.1 Buffer类型
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
从类名中就可以知道各个Buffer的类型。
在Netty 中拥有单独的ByteBuf类型,该类型是直写的内存封装,与上述并不相同(在2.5中会介绍),更重要的是ByteBuf不用读写模式转换,这个会在后续netty中介绍。
2.2 Buffer属性
Buffer 有三个属性:
- capacity
- position
- limit
其中 position 和 limit 的含义与 Buffer 处于读模式或写模式有关(简单来说就是读写指针的位置和剩余量), 而 capacity 的含义与 Buffer 所处的模式无关。
2.2.1 capacity
也就是buffer的容量,初始化时就是其容量。并不是字节数,比如DoubleByte如果初始化为100,则为100个double数据
2.2.2 position
position 表示了读写操作的位置指针.
从Buffer 中写入数据时,是从 position开始写入的,在最初的状态时, position 的值是0。每写入一个数据,position+1。
从 Buffer 中读取数据时,也是从position读取的。 当调用了 filp()方法将 Buffer 从写模式转换到读模式时,position 的值会自动被设置为0,每读出一个数据,position +1。
2.2.3 limit
limit=capacity-position
表示此时还可以写入/读取多少单位的数据。
2.3 Buffer方法
2.3.1 分配内存
分配48大小的buf如下
- ByteBuffer buf = ByteBuffer.allocate(48);
- xxxByte buf=xxxBuffer.allocate(48);
2.3.2 写入数据
- buf.put(127);//手动写入数据
- int bytesRead = inChannel.read(buf); //从channel中读出数据并写入buf中
2.3.3 读取数据
- byte aByte = buf.get(); //读出一个数据 buf的position+1
- int bytesWritten = inChannel.write(buf);//从buf中读出数据并写入channel中
2.3.4 重置position
rewind()方法可以重置 position 的值为0
position(int newPosition)方法可以直接重置位置
2.3.5 mark()和reset()
mark()将当前的 position 的值保存
之后可以通过调用 reset()方法将 position 的值恢复。
2.3.6 flip(), rewind() 和 clear()
- flip()
读写模式转换,limit赋值为position,position赋值为0
源码如下:
- public final Buffer flip() {
- limit = position;
- position = 0;
- mark = -1;
- return this;
- }
- rewind()
倒带,仅仅将position设置为0
源码如下:
- public final Buffer rewind() {
- position = 0;
- mark = -1;
- return this;
- }
- clear()
clear 将 positin 设置为0, 将 limit 设置为 capacity。
clear基本如下使用
1.在一个已经写满数据的 buffer 中, 调用 clear, 可以从头读取 buffer 的数据.(也可以看成写模式到读模式的转换)
2.为了将一个 buffer 填充满数据, 可以调用 clear, 然后一直写入, 直到达到 limit。
源码如下:
- public final Buffer clear() {
- position = 0;
- limit = capacity;
- mark = -1;
- return this;
- }
2.4 Buffer比较
可以通过 equals() 或 compareTo() 方法比较两个 Buffer, 当且仅当如下条件满足时, 两个 Buffer 是相等的:
- 两个 Buffer 是相同类型的
- 两个 Buffer 的剩余的数据个数是相同的
- 两个 Buffer 的剩余的数据都是相同的.
通过上述条件我们可以发现, 比较两个 Buffer 时, 并不是 Buffer 中的每个元素都进行比较, 而是比较 Buffer 中剩余的元素.
2.5 Direct Buffer 和 Non-Direct Buffer
2.5.1 Direct Buffer
- 所分配的内存不在 JVM 堆上, 不受 GC 的管理.(但是 Direct Buffer 的 Java 对象是由 GC 管理的, 因此当发生 GC, 对象被回收时, Direct Buffer 也会被释放)
- 因为 Direct Buffer 不在 JVM 堆上分配, 因此 Direct Buffer 对应用程序的内存占用的影响就不那么明显(实际上还是占用了这么多内存, 但是 JVM 不好统计到非 JVM 管理的内存.)
- 申请和释放 Direct Buffer 的开销比较大. 因此正确的使用 Direct Buffer 的方式是在初始化时申请一个 Buffer, 然后不断复用此 buffer, 在程序结束后才释放此 buffer.
- 使用 Direct Buffer 时, 当进行一些底层的系统 IO 操作时, 效率会比较高, 因为此时 JVM 不需要拷贝 buffer 中的内存到中间临时缓冲区中.
2.5.2 Non-Direct Buffer
- 直接在 JVM 堆上进行内存的分配, 本质上是 byte[] 数组的封装.
- 因为 Non-Direct Buffer 在 JVM 堆中, 因此当进行操作系统底层 IO 操作中时, 会将此 buffer 的内存复制到中间临时缓冲区中. 因此 Non-Direct Buffer 的效率就较低.
2.6 Buffer 使用Demo
在Buffer包下
三.Channel
NIO的所有操作都是从Channel开始的,Channel其实就是对标的就是Stream。但不同还是有不同的,大概有三点
- Channel可写可读,而一个Stream仅支持读或写
- Channel阻塞不阻塞都有可能,Stream阻塞
- Channel用Buffer读取或者写入数据
- Channel支持随机读写,也就是可以设置position的位置
3.1 Channel类型
- FileChannel, 文件操作(该类型使用了内存映射,必为阻塞式)
- DatagramChannel, UDP 操作(有阻塞式和非阻塞式)
- SocketChannel, TCP 操作(有阻塞式和非阻塞式)
- ServerSocketChannel, TCP 操作,使用在服务器端(有阻塞式和非阻塞式)
在这些类型中,kafka的文件读写用了FileChannel
Netty封装了SocketChannel/ServerSocketChannel
Kafka用的处理tcp请求的也是封装了
SocketChannel/ServerSocketChannel
-
- Channel使用demo
均在Channel包下,类名对应其使用,基本方法均在demo中有
四.Selector
Selector就是用一个单一的线程来操作多个Channel的一种实现多路复用的实现策略。但是缺点也很明显,因为多路复用,自然会下降信息传输的效率。
再复习一遍NIO的概念图
图二.NIO概念图
4.1 selector基本方法
4.1.1 创建selector
- Selector selector = Selector.open();
4.1.2 channel 注册到selector中
注意channel必须是非阻塞式,所以channel注册进入selector之前必须做设置,且因为FileChannel必为阻塞式,所以无法使用NIO结构。
- channel.configureBlocking(false);//设置为阻塞式
- SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
注册中第二个参数为注册的事件,如果channel发生了该事件,则通知selector,selector去处理该channel。
注册方法会返回一个SelectionKey对象,4.1.3中会介绍
注册的事件分为
Connect, 即连接事件(TCP 连接)
对应于SelectionKey.OP_CONNECT
Accept, 即确认事件,
对应于SelectionKey.OP_ACCEPT。
Read, 即读事件
对应于SelectionKey.OP_READ, 表示 buffer 可读。
Write, 即写事件
对应于SelectionKey.OP_WRITE, 表示 buffer 可写。
各个事件可以 | 运算符组合
- int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
一个channel在一个selector中仅有一个注册实例,多次注册,仅仅是替换而已。例如:
- channel.register(selector, SelectionKey.OP_READ);
- channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);//仅仅这个生效
4.1.3 SelectionKey
在4.1.2中注册方法会返回SelectionKey
- SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
这个SelectionKey包含有如下属性
interest set 关注的事件集 可按照下述操作获取
- int interestSet = selectionKey.interestOps();
- boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
- boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
- boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
- boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
- // interestOps()这个方法可以动态修改interestSet
- selectionKey.interestOps(OP_READ | SelectionKey.OP_WRITE);;
ready set 代表已经准备好的事件,可按照下述操作获取
- int readySet = selectionKey.readyOps();
- selectionKey.isAcceptable();
- selectionKey.isConnectable();
- selectionKey.isReadable();
- selectionKey.isWritable();
channel 对应的channel
- Channel channel = selectionKey.channel();
selector 对应的selector
- Selector selector = selectionKey.selector();
attached object 可选的附加对象
可以在selectionKey中添加也可以在注册时直接添加
- selectionKey.attach(theObject);//selectoinKey添加
- SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);//注册时添加
- Object attachedObj = selectionKey.attachment();//获取该附加对象
4.1.4 选择channel
使用selector()方法获取准备好的Channel。注意,这里返回的是一个包含selectorKey的Set,因为同时可能会有多个channel准备好。
之后根据其readySet完成相关操作。
代码框架如下:
- Set<SelectionKey> selectedKeys = selector.selectedKeys();
- Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
- while(keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
- if(key.isAcceptable()) {
- // a connection was accepted by a ServerSocketChannel.
- } else if (key.isConnectable()) {
- // a connection was established with a remote server.
- } else if (key.isReadable()) {
- // a channel is ready for reading
- } else if (key.isWritable()) {
- // a channel is ready for writing
- }
- keyIterator.remove();
- }
注意, 在每次迭代时, 我们都调用 "keyIterator.remove()" 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys 集合中, 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys 中。
4.2 selector 基本使用流程
- 通过 Selector.open() 打开一个 Selector.
- 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)
- 进入循环
1.调用 select() 方法
2.调用 selector.selectedKeys() 获取 selected keys
3.迭代每个 selected key:
4.从 selected key 中获取 对应的 Channel 和附加信息(如果有的话)
5.判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用
- SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()
获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中.
6.根据需要更改 selected key 的监听事件.
7.将已经处理过的 key 从 selected keys 集合中删除.
4.3 selector 使用demo
在Selector包中
五.Reactor
在前面的Selector的demo中处理事件很简单,事实上,事件处理有很多复杂的方法,而且高并发情况下,单一线程处理明显也不合适,如使用多线程处理就设计线程分配的问题。
其次ServerSocketChannel与SocketChannel放在同一个Selector中并不是特别合适。
为了解决以上问题,诞生了 Reactor模型。
5.1 Reactor
首先Reactor模式首先是事件驱动的,有一个或者多个并发输入源,有一个Server Handler和多个Request Handlers,这个Service Handler会同步的将输入的请求多路复用的分发给相应的Request Handler。
如下图所示
图三.Reactor
为什么前面讲NIO那么多的channel和selector,其实这里就是一样的。Handler就是对标线程,request对标channel。Handler Service 对标Selector。这里多了个Dispatcher,可以认为就是一个调度工厂,用来缓冲事件,之后分发给handler去处理。
5.2 reactor模型实现方式
5.2.1单线程处理模型
图四.单线程处理模型
跟4.3中的selector的demo一样,accept接收监听,如果是connect,就将channel放入selector(也就是reactor)中,单线程循环处理各个channel事件。
5.2.2 多线程模型
图五.多线程模型
与5.2.1中唯一不同的一点就是对于事件在reactor中缓冲,然后在ThreadPool中捞出一个线程去跑这个事件。
5.2.3 主从模型
最最最重要的模型
图六.主从模型
前面在4.2中为什么要将基本流程就是以为了引出这个模型,首先监听连接的channel和真正与client的channel是不同的,这就可以完成主从selector(Reactor)的分离。
在5.2的基础上 由mainReactor负责接收connect,只要连接完成就交由subReactor,从而保证连接事务与IO事务的分离。
这也是最重要的。
5.3 netty 的模型
具体会在第六节中介绍,这里为了应对5.2中的几个实现方式,简单介绍下。其实就是5.2.3 主从模型的升级版,还解决了一些buffer的关键问题,以及多线程的问题。
六.Netty
为什么前面要大费周章的将NIO的基本使用以及Buffer,channel,selector都讲一遍,因为其实netty其实就是前面的过程的封装,以及部分优化。
NIO模型介绍
NIO分为
- Selector
- EventLoopGroup/EventLoop
- ChannelPipeline
//后续再补了
//demo后续会传到github上