转载

Netty 新手入门 ( 一 ) Netty 与 NIO

最近因为工作需要使用到Netty方面的技术点,而以前也对这方面没有太多的关注过,所以边学习边总结,也给自己留个资料,以便以后查看起来方便。

Java NIO

再说Netty 之前先简单了解下Java 的NIO,因为Netty也是为了简化我们的编码对Java NIO进行了一系列的封装。

Java NIO 从java领域讲就是 NEW I/O ,另一种就是Non-blocking I/O。它是一种同步非阻塞的I/O模型,也是I/O多路复用的基础。

在NIO中有三个非常重要的概念,缓冲区(Buffer)、通道(Channel)、选择器(Selector)

缓冲区(Buffer)

1、基础概念

缓冲区本质上来说就是一个数组,在NIO中,所有的操作都是面向缓冲区的。读取数据时,是从缓冲区读取,写入数据时也是写入到缓冲区。所有NIO的数据操作都是操作的缓冲区,而面向流I/O系统中,数据是直接写入到Stream对象中的。

在NIO API中所有的缓冲区类型都继承Buffer,我们经常用到的就是ByteBuffer,NIO这边对Java的基本类型都有实现。

2、基本原理

上面提到过缓冲区其实就是一个数组,其实是一个特殊的数组,它里面内置了一些机制来帮助我们进行数据的读写,能够跟踪和记录缓冲区的变化方便我们使用。

在缓冲区中有三个重要的属性进行合作完成了缓存区内部状态变化的跟踪:

  • position: 指定下一个将要被写入或者读取的元素索引,它的值由get()/put()方法自动更新,在新创建一个Buffer对象时,position被初始化为0。
  • limit: 指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)
  • capacity:指定了可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小,或者至少是指定了准许我们使用的底层数组的容量.

以上三个属性的之间的关系 :0<=position<=limit<=capacity。也就是说当我们创建一个容量为10的ByteBuffer对象时,初始化的时候,position设置为0,limit和capacity设置为10,在以后使用ByteBuffer对象过程中,capacity的值不会再发生变化,而其他两个将会随着使用而变化。

简单的写个伪代码:

//1、分配一个容量为10的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);

//2、然后我们可以执行操作将数据读取到buffer
channel.read(buffer);
//3、读取到buffer后我们需要对buffer 进行翻转操作(重点)
buffer.flip();

//4、翻转后,就可以从buffer 里面进行数据读取了
buffer.get();

//5、重置buffer
buffer.clear();
复制代码

上面大体上分为了五部分,下面简单的了解下到底发生了什么? 1、初始化buffer,分配空间,可以直观的看到buffer三个参数的位置

Netty 新手入门 ( 一 ) Netty 与 NIO

2、从通道中读取一些数据到缓冲区,这里我们读取了4个数据,可以看到position此时为4,而limit为10

Netty 新手入门 ( 一 ) Netty 与 NIO

3、当我们需要把数据从缓冲出输出时,必须先调用flip()方法。此方法完成了两件事:一是把limit值设置为当前position的值。二是设置position值为0。其实就是为了告诉我们当前数据的头和尾是什么。

Netty 新手入门 ( 一 ) Netty 与 NIO

4、通过get()丛缓冲区读取数据,这时position值会随着数据读取增加,而limit保持不变,但position不会超过limit的值。

Netty 新手入门 ( 一 ) Netty 与 NIO

5、在数据读取完成后,我们可以使用clear()方法将buffer状态进行初始化

Netty 新手入门 ( 一 ) Netty 与 NIO

3、缓冲区分配

在上面例子中我们可以看到分配一个缓冲区对象时,会调用方法allocate(10),相当于创建了一个指定大小的数组,并包装成缓冲区对象。 分配缓冲区一般有两种方式:

//1、分配指定大小缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
//2、包装现有数组
byte[] arr = new byte[10];
ByteBuffer buffer2 = ByteBuffer.warp(arr);
复制代码

4、子缓冲区

子缓冲区,就是在我们刚才ByteBuffer.allocate(10)分配的空间内在划出一块区域独立操作,调用slice() 可创建一个子缓冲区。子缓冲区和当前缓冲区是数据共享的,其实只是给其实一块区域提供了一个视图窗口。

5、只读缓冲区

看名字就知道,这buffer只能读取数据。我们可以调用asReadOnlyBuffer()方法,将我们的ByteBuffer 转换为只读缓冲区,注意,这个方法不是真的将我们当前缓冲区转换为只读了,而是返回来一个和当前缓冲区参数一样的缓冲区,并和原缓冲区数据共享,只不过这个是只读的。如果原缓冲区内容发送改变,只读缓冲区内容也会发生变化(简单理解下就是增加了一个只读引用) 伪代码

ByteBuffer buffer = ByteBuffer.allocate(10);

//创建只读缓冲区
ByteBuffer readonly = buffer.asReadOnlyBuffer();

复制代码

6、直接缓冲区

都知道我们不管怎么创建buffer其实都在JVM内部,在进行数据传输的时候都避免不了,从虚拟机内存拷贝到内核空间的复制操作。而直接缓冲区,就是省略了从用户空间到内核空间拷贝的过程,使用了一个物理内存映射文件来直接对数据进行操作。在代码中使用是很简单的,需调用allocateDirect()方法,而不是allocate()方法。

Netty 新手入门 ( 一 ) Netty 与 NIO

7、内存映射(mmap)

内存映射是一种读和写文件数据的方法,比常规基于流或通道的I/O快的多。简单点说就是将文件的磁盘扇区映射到进程的虚拟内存空间的过程。

选择器

与传统Client/Server模式不同,NIO中非阻塞I/O采用了基于Reactor模式的工作方式,I/O调用不会被阻塞,而是注册感兴趣的特定I/O事件,如可读数据到达、新的套接字连接等,在发生特定事件时,系统再通知我们。NIO中实现非阻塞I/O的核心对象是Selector,Selector是注册各种I/O事件的地方,而且当那些事件发生时,就是Seleetor告诉我们所发生的事件

Netty 新手入门 ( 一 ) Netty 与 NIO

从图中可以看出,当有读或写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,同时从SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据

使用NIO中非阻塞I/O编写服务器处理程序,大体上可以分为下面三个步骤。

  • (1)向Selector对象注册事件。
  • (2)从Selector中获取事件。
  • (3)根据不同的事件进行相应的处理。

通道

通道就是一个对象,它可以帮我们进行数据读取和写入,当然所有数据都是通过操作Buffer对象来处理的。

1、NIO读取写入数据

读取数据分为三步:

  • (1)从FileInputStream获取Channel。
  • (2)创建Buffer。
  • (3)将数据从Channel读取到Buffer中。

来看下伪代码

//获取文件Channel
FileChannel fc = out.getChannel();
//分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffe.put(1);
buffe.put(2);

buffer.flip();

//将数据写入文件
fc.write(buffer);
out.close();
复制代码

2、多路复用I/O

什么是多路复用器? 1、解决IO 状态的问题 不解决你的IO 读写数据的问题。 2、解决用更少的系统调用,一下询问所有的IO状态,不是一次次的询问IO 的状态(与内核询问),减少了用户态和内核态切换的过程。

目前流行的多路复用I/O的实现主要包括四种:select、poll、epoll、kqueue。如下表所示是它们的一些重要特性的比较。

多路复用器 Select poll 是阻塞的,其实就是把文件描述符放在jvm 的内存中开辟了一个数组,如果是epoll 就是调用了 epoll_create 放到了内核空间。

Netty 新手入门 ( 一 ) Netty 与 NIO
原文  https://juejin.im/post/5ef1c958f265da02f239a0b9
正文到此结束
Loading...