从NIO到Netty的个人见解

 

 

目录

 

学习netty的起因

一.NIO

1.1基于 Stream 与基于 Buffer

1.2阻塞和非阻塞

1.3 Selector

二.Buffer

2.1 Buffer类型

2.2 Buffer属性

2.3 Buffer方法

2.3.1 分配内存

2.3.2 写入数据

2.3.3 读取数据

2.3.4 重置position

2.3.5 mark()和reset()

2.3.6 flip(), rewind() 和 clear()

2.4 Buffer比较

2.5  Direct Buffer 和 Non-Direct Buffer

2.5.1 Direct Buffer

2.5.2 Non-Direct Buffer

2.6 Buffer 使用Demo

三.Channel

3.1 Channel类型

四.Selector

4.1 selector基本方法

4.1.1 创建selector

4.1.2  channel 注册到selector中

4.1.3 SelectionKey

4.1.4 选择channel

4.2 selector 基本使用流程

4.3 selector 使用demo

五.Reactor

5.1 Reactor

首先Reactor模式首先是事件驱动的,有一个或者多个并发输入源,有一个Server Handler和多个Request Handlers,这个Service Handler会同步的将输入的请求多路复用的分发给相应的Request Handler。

5.2 reactor模型实现方式

5.2.1单线程处理模型

5.2.2 多线程模型

5.2.3 主从模型

5.3 netty 的模型

六.Netty


学习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点

  1. 基于Strem(流,IO),基于Buffer(NIO)
  2. 阻塞与非阻塞
  3. NIO有Selector(NIO的选择器)

1.1基于 Stream 与基于 Buffer

基于流的IO好理解,比如打开一个文件流(例如FileStream),之后read,就是从前往后的顺序读,也就是说不能随意改变读写位置的指针。

而基于Buffer的不同,基于Buffer的需要打开的是Channel,之后将Channel的数据搬运到Buffer中(读操作,写操作相反),Buffer中就可以随机位置读取,写入。

1.2阻塞和非阻塞

阻塞就是不返回知道能够读到数据,非阻塞就是读不到立即返回失败。

1.3 Selector

也就是选择器,可以简单的认为多个Channel的总控制器,一旦有哪个Channel有消息,送信号量给SelectorSelector选择这个Channel执行相关程序,可以看到多个Channel只需要一个线程去管理,可以很简单的理解为一种多路复用的实现方式。

二.Buffer

需要用NIO Channal时,就必须要使用NIO Buffer,即数据从Buffer读取到channel,或者从channel中写入到buffer中。

Buffer拥有读模式和写模式(需要clear()flip()方法转换,初创默认为写模式)

Buffer其实就可以看做一块内存区域,我们不过就是在这个内存区域中进行读写罢了,Buffer就是对该内存区域以及一些规定的操作方法的封装。

2.1 Buffer类型

  1. ByteBuffer
  2. CharBuffer
  3. DoubleBuffer
  4. FloatBuffer
  5. IntBuffer
  6. LongBuffer
  7. ShortBuffer

从类名中就可以知道各个Buffer的类型。

在Netty 中拥有单独的ByteBuf类型,该类型是直写的内存封装,与上述并不相同(在2.5中会介绍),更重要的是ByteBuf不用读写模式转换,这个会在后续netty中介绍。

2.2 Buffer属性

Buffer 有三个属性:

  1. capacity
  2. position
  3. 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如下

  1. ByteBuffer buf = ByteBuffer.allocate(48);  
  2. xxxByte buf=xxxBuffer.allocate(48);  

2.3.2 写入数据

  1. buf.put(127);//手动写入数据  
  2. int bytesRead = inChannel.read(buf); //channel中读出数据并写入buf  

2.3.3 读取数据

  1. byte aByte = buf.get(); //读出一个数据 bufposition+1  
  2. 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()

  1. flip()

读写模式转换,limit赋值为position,position赋值为0

源码如下:

  1. public final Buffer flip() {  
  2.     limit = position;  
  3.     position = 0;  
  4.     mark = -1;  
  5.     return this;  
  6. }  
  1. rewind()

倒带,仅仅将position设置为0

源码如下:

  1. public final Buffer rewind() {  
  2.     position = 0;  
  3.     mark = -1;  
  4.     return this;  
  5. }  
  1. clear()

clear positin 设置为0, limit 设置为 capacity

clear基本如下使用

1.在一个已经写满数据的 buffer 中, 调用 clear, 可以从头读取 buffer 的数据.(也可以看成写模式到读模式的转换)

2.为了将一个 buffer 填充满数据, 可以调用 clear, 然后一直写入, 直到达到 limit。

源码如下:

  1. public final Buffer clear() {  
  2.     position = 0;  
  3.     limit = capacity;  
  4.     mark = -1;  
  5.     return this;  
  6. }  

2.4 Buffer比较

可以通过 equals() compareTo() 方法比较两个 Buffer, 当且仅当如下条件满足时, 两个 Buffer 是相等的:

  1. 两个 Buffer 是相同类型的
  2. 两个 Buffer 的剩余的数据个数是相同的
  3. 两个 Buffer 的剩余的数据都是相同的.

通过上述条件我们可以发现, 比较两个 Buffer , 并不是 Buffer 中的每个元素都进行比较, 而是比较 Buffer 中剩余的元素.

2.5  Direct Buffer 和 Non-Direct Buffer

2.5.1 Direct Buffer

  1. 所分配的内存不在 JVM 堆上, 不受 GC 的管理.(但是 Direct Buffer 的 Java 对象是由 GC 管理的, 因此当发生 GC, 对象被回收时, Direct Buffer 也会被释放)
  2. 因为 Direct Buffer 不在 JVM 堆上分配, 因此 Direct Buffer 对应用程序的内存占用的影响就不那么明显(实际上还是占用了这么多内存, 但是 JVM 不好统计到非 JVM 管理的内存.)
  3. 申请和释放 Direct Buffer 的开销比较大. 因此正确的使用 Direct Buffer 的方式是在初始化时申请一个 Buffer, 然后不断复用此 buffer, 在程序结束后才释放此 buffer.
  4. 使用 Direct Buffer 时, 当进行一些底层的系统 IO 操作时, 效率会比较高, 因为此时 JVM 不需要拷贝 buffer 中的内存到中间临时缓冲区中.

 

2.5.2 Non-Direct Buffer

  1. 直接在 JVM 堆上进行内存的分配, 本质上是 byte[] 数组的封装.
  2. 因为 Non-Direct Buffer 在 JVM 堆中, 因此当进行操作系统底层 IO 操作中时, 会将此 buffer 的内存复制到中间临时缓冲区中. 因此 Non-Direct Buffer 的效率就较低.

2.6 Buffer 使用Demo

在Buffer包下

三.Channel

NIO的所有操作都是从Channel开始的,Channel其实就是对标的就是Stream。但不同还是有不同的,大概有三点

  1. Channel可写可读,而一个Stream仅支持读或写
  2. Channel阻塞不阻塞都有可能,Stream阻塞
  3. Channel用Buffer读取或者写入数据
  4. Channel支持随机读写,也就是可以设置position的位置

3.1 Channel类型

  1. FileChannel, 文件操作(该类型使用了内存映射,必为阻塞式)
  2. DatagramChannel, UDP 操作(有阻塞式和非阻塞式)
  3. SocketChannel, TCP 操作(有阻塞式和非阻塞式)
  4. ServerSocketChannel, TCP 操作,使用在服务器端(有阻塞式和非阻塞式)

 

在这些类型中,kafka的文件读写用了FileChannel

Netty封装了SocketChannel/ServerSocketChannel

Kafka用的处理tcp请求的也是封装了

SocketChannel/ServerSocketChannel

    1. Channel使用demo

均在Channel包下,类名对应其使用,基本方法均在demo中有

四.Selector

Selector就是用一个单一的线程来操作多个Channel的一种实现多路复用的实现策略。但是缺点也很明显,因为多路复用,自然会下降信息传输的效率。

再复习一遍NIO的概念图

图二.NIO概念图

4.1 selector基本方法

4.1.1 创建selector

  1. Selector selector = Selector.open();  

4.1.2  channel 注册到selector

注意channel必须是非阻塞式,所以channel注册进入selector之前必须做设置,且因为FileChannel必为阻塞式,所以无法使用NIO结构。

  1. channel.configureBlocking(false);//设置为阻塞式  
  2. 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 可写。

 

各个事件可以 | 运算符组合

  1. int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;  

 

一个channel在一个selector中仅有一个注册实例,多次注册,仅仅是替换而已。例如:

  1. channel.register(selector, SelectionKey.OP_READ);  
  2. channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);//仅仅这个生效  

 

4.1.3 SelectionKey

在4.1.2中注册方法会返回SelectionKey

  1. SelectionKey key = channel.register(selector, SelectionKey.OP_READ);  

这个SelectionKey包含有如下属性

interest set 关注的事件集 可按照下述操作获取

  1. int interestSet = selectionKey.interestOps();  
  2.   
  3. boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;  
  4. boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;  
  5. boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;  
  6. boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;
  7. // interestOps()这个方法可以动态修改interestSet
  8. selectionKey.interestOps(OP_READ | SelectionKey.OP_WRITE);; 

ready set   代表已经准备好的事件,可按照下述操作获取

  1. int readySet = selectionKey.readyOps();  
  2.   
  3. selectionKey.isAcceptable();  
  4. selectionKey.isConnectable();  
  5. selectionKey.isReadable();  
  6. selectionKey.isWritable();  

channel  对应的channel

  1. Channel  channel  = selectionKey.channel(); 

selector  对应的selector

  1. Selector selector = selectionKey.selector(); 

attached object 可选的附加对象

可以在selectionKey中添加也可以在注册时直接添加

  1. selectionKey.attach(theObject);//selectoinKey添加  
  2. SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);//注册时添加  
  3. Object attachedObj = selectionKey.attachment();//获取该附加对象  

4.1.4 选择channel

使用selector()方法获取准备好的Channel。注意,这里返回的是一个包含selectorKey的Set,因为同时可能会有多个channel准备好。

之后根据其readySet完成相关操作。

代码框架如下:

  1. Set<SelectionKey> selectedKeys = selector.selectedKeys();  
  2.   
  3. Iterator<SelectionKey> keyIterator = selectedKeys.iterator();  
  4.   
  5. while(keyIterator.hasNext()) {  
  6.       
  7.     SelectionKey key = keyIterator.next();  
  8.   
  9.     if(key.isAcceptable()) {  
  10.         // a connection was accepted by a ServerSocketChannel.  
  11.     } else if (key.isConnectable()) {  
  12.         // a connection was established with a remote server.  
  13.     } else if (key.isReadable()) {  
  14.         // a channel is ready for reading  
  15.     } else if (key.isWritable()) {  
  16.         // a channel is ready for writing  
  17.     }  
  18.     keyIterator.remove();   
  19. }  

注意, 在每次迭代时, 我们都调用 "keyIterator.remove()" 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys 集合中, 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys 中。

4.2 selector 基本使用流程

  1. 通过 Selector.open() 打开一个 Selector.
  2. 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)
  3. 进入循环

1.调用 select() 方法

2.调用 selector.selectedKeys() 获取 selected keys

3.迭代每个 selected key:

4.从 selected key 中获取 对应的 Channel 和附加信息(如果有的话)

5.判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用

  1. 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

https://img-blog.csdn.net/20161129103112729

为什么前面讲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 主从模型

最最最重要的模型

图六.主从模型

https://img-blog.csdn.net/20161129103725841

前面在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分为

  1. Selector
  2. EventLoopGroup/EventLoop
  3. ChannelPipeline

//后续再补了

//demo后续会传到github上

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值