http://www.cnblogs.com/xdp-gacl/p/3634409.html

NIO 提供了一个全新的底层IO 模型,与最初Java.io包中面向流的概念不同,NIO采用了面向块的概念,在尽可能的情况下,将以大的数据块为单位进行操作,而不是每次一个字节或者一个字符。

NIO 提供了与平台无关的非阻塞IO,与面向线程,阻塞式的IO 方式相比,多道通信,非阻塞IO可以使程序更有效的处理大量连接的情况。

IO 与NIO

IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

常见I/O模型对比

所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。

需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。

下图是几种常见I/O模型的对比:

img

以socket.read()为例子:

传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。

对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。

最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。

换句话说,BIO里用户最关心“我要读”,NIO里用户最关心”我可以读了”,在AIO模型里用户更需要关注的是“读完了”。

NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。

##缓冲区与Buffer

基本IO 操作中所有操作都是直接以流的形式完成的,而在NIO中,所有操作都要使用到缓冲区处理,且所有的读写操作都是通过缓冲区完成的,缓冲区是一个线性的,有序的数据集,只能容纳某种特定的数据类型。可以把换冲区想象成一个水桶,总是水满了再进行和通道,内存交互,这样就减少了IO.详见http://www.cnblogs.com/xdp-gacl/p/3634409.html。

NIO中针对每一种基本数据类型都有一种对应的缓冲区操作类.

buffer的基本操作

Java.io.Buffer本身是一个抽象类

buffer 类常用方法

buffer

新IO 针对每一种数据类型都有一种对应的缓冲区操作类,Java.nio.ByteBuffer java.nio.CharBuffer java.nio.ShortBuffer java.nio.IntBuffer java.nio.LongBuffer java.nio.FloatBuffer java.nio.DoubleBuffer

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.nio.IntBuffer;
public class BufferDemo()
{
public static void main(String[] args)
{
IntBuffer buf = InBuffer.allocate(10);//开辟10个大小缓冲区
System.out.println("写入数据前的position limit capacity") System.out.println(buf.position()+buf.limit()+buf.capacity())
int temp[]={5,7,9};
buf.put(3);// 向缓冲区写入数据
buf.put(temp);//向缓冲区写入一组数据
System.out.println("写入数据后的position limit capacity"); System.out.println(buf.position()+buf.limit()+buf.capacity();)
buf.flip();// 重设缓冲区 执行后,limit设为position,position设为0
System.out.println("准备输出数据时的position limit capacity"); System.out.println(buf.position()+buf.limit()+buf.capacity();)
System.out.println("缓冲区的内容");
while(buf.hasReamining())
{
int x = buf.get();
System.out.println(x);
}
}
}

在每次写入之后position会有变化。而当调用flip()方法时,position 和limit发生变化。

深入缓冲区操作

position 表示下一个缓冲区读取或者写入的操作指针,写入数据时,此指针就会改变。 limit 表示还有多少数据需要存储或者读取。limit指的是缓冲区中第一个不能读写的元素的数组下标索引,也可以认为是缓冲区中实际元素的数量(也就是上界)。position<=limit。 capacity 表示缓冲区的最大容量。在分配缓冲区时设置,一般不会改变。

创建子缓冲区

可以使用各个缓冲区类的slice() 方法从一个缓冲区创建一个新的子缓冲区,子缓冲区与原缓冲区中部分数据可以共享。

创建只读缓冲区
1
2
IntBuffer buf =IntBuffer.allocate(10);
read =buf.asReadOnlyBuffer();

通道(NIO把它支持的I/O对象抽象为Channel)

(通道和流的区别之处在于通道是双向的,流是单向的.)

通道可以用来读取和写入数据,通道类似于之前的输入输出流,程序不会直接操作通道,所有的内容都是先读到或者写入到缓冲区中,再通过缓冲区中取得或者写入的通道本身是双向操作的,既可以完成输入也可以输出。

Channel 本身是一个接口,

12

FileChannel

FileChannel 是Channel的子类,可以进行文件的读写操作,如果要使用FileChannel,则要依靠FileInputStream 或者FileOutputStream 类中的getChannel() 方法取得输入或者输出的通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo()
{
public static void main(String[] args) throws Exception
{
String info[] = {"c","python","javascript","markdown"};//待输出的数据
File f =new File("/home/brett/Desktop/1.txt");
FileOutputStream output = new FileOutputStream(File);//实例化输出流
FileChannel fout =null;//声明输出的通道对象
fout = output.getChannel();//得到输出的文件通道
ByteBuffer buf = ByteBuffer.allocate(1024);//开辟缓存。
for(int i=0;i<info.length;i++)
{
buf.put(info[i].getBytes());//向缓存写入数据
}
buf.flip(); //重设缓冲区,准备输出
fout.write(buf);//输出
fout.close(); //关闭输出通道
output.close() //关闭输出流
}
}//以上程序使用输出通道将内容全部放到缓冲中,一次性写入到文件中的。

Selector

原来使用Io和socket构造网络服务时,所有的网络服务将使用阻塞的方式进行客户端的连接,而如果使用了新IO则可以构造一个非阻塞的网络服务。

要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。


NIO中的处理流程一般为

由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。

一般情况下,使用NIO,主要包括了下面三种要用到的线程。

  1. 事件分发器,单线程选择就绪的事件。
  2. I/O处理器,包括connect、read、write等,这种纯CPU操作,一般开启CPU核心个线程就可以。
  3. 业务线程,在处理完I/O后,业务一般还会有自己的业务逻辑,有的还会有其他的阻塞I/O,如DB操作,RPC等。只要有阻塞,就需要单独的线程。

其中,事件分发器的作用,即 将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧!开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。


Java NIO (Channels and Buffers)

标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和 缓冲区(Buffer) 进行操作,数据总是从通道读到缓冲区,或者从缓冲区读写到通道中。NIO 还可以让你使用非阻塞的IO。NIO还引入了选择器的概念,选择器用于监听多个通道的事件。NIO的核心API就是Channel Buffer Selector

Channel 和 Buffer

基本上,所有的NIO都是从一个Channel开始,Channel 有点像流,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel 中,

Selector

selector 允许单线程处理多个Channel,如果一个应用打开了多个Channel,但是每个Channel的流量都很低。使用Selector会很方便。下图是一个单线程中使用一个Selector处理三个Channel的图示

12

Channel(流要写具体的流,才有getChannel方法,Channel 也是)

通道类似于流,但又有些不同。既可以从通道中读取数据,又可以写数据到通道,而流的读写通常是单向的。通道可以异步的读写,通道中的数据总是要先读到一个Buffer,或者从一个Buffer中写入。具体的实现类有 FileChannel,DatagramChannel通过UDP读写网络中的数据, SocketChannel 能通过TCP 读写网络中的数据,ServerSocketChannel 以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception
{
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
}

Buffer

NIO 中的Buffer用于和NIO通道进行交互,缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装为NIO Buffer 对象,并提供一组方法,进行方便的访问。

使用Buffer 读写数据一般需要遵循以下四个步骤:

1
2
3
4
1. 写入数据到Buffer 
2. 调用flip() 方法,
3. 从buffer中读取数据,
4 .调用clear() 方法或者compact()方法。

当向Buffer写入数据时,buffer会记录写入了多少数据,一旦需要读取数据,就需要通过flip()方法将Buffer从写模式切换为读模式,在读模式下,就可以读取之前写入到Buffer的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方法可以清空缓冲区,调用clear() 或者compact() 方法,clear() 方法会清空整个缓冲区,compact()方法只会清除已经读过的数据。任何未读的数据都会被移到缓冲区的起始处,新写入的数据会放到缓冲区未读数据的后面。

Buffer的三个工作属性 capacity position limit

Position 和limit的含义取决于Buffer在读模式还是写模式,

position(指向下一个可以读或者可以写的位置)

当你写数据到Buffer中时,position表示当前的位置。初始的position值为0写数据后,position会移动到下一个可以写的位置。当切换到读模式的时候,position会被重置为0.当从Buffer的position出读取数据时,position向前移动到下一个可读的位置。

limit(读模式下表示最多能读的数据,写模式下最多能写的数据)

写模式下,Buffer的limit表示最多可以往里面写多少数据,所以写模式下,limit等于capacity。当切换到读模式时,limit表示你最多能读多少数据,因此,当切换到读模式时,limit设置成写模式的position。换句话说,你可以读到之前写入的所有数据。

Buffer的分配

要想获得一个Buffer首先要分配,每一个Buffer都有一个allocate方法。

1
ByteBuffer buf = ByteBuffer.allocate(48);//分配一个48字节的Buffer。
向Buffer中写数据

两种方式,从Channel写到Buffer 、通过Buffer的put()方法写到Buffer里。

1
2
int bytesRead = inChannel.read(buf);//从Channel写待Buffer
buf.put(127);
flip() 方法

flip将写模式转换为读模式,调用flip()方法将会将position设回0,limit设置成原来的position。

从Buffer中读取数据

两种方式,1,从Buffer读数据到Channel 2. 使用get() 从Buffer读取数据。

1
2
int byteWritten =inChannel.write(buf);
byte abyte =buf.get();
rewind()

Buffer.rewind()将position设回0,所以可以重读Buffer中的所有数据limit保持不变,仍然表示能从Buffer中读取多少元素。

clear() 和compact()

Scatter/Gather

Java NIO开始支持scatter/gather.

Scatter/gather用于描述从Channel中读取或者写入到channel的操作

分散scatter 从Channel中读取是指在读操作时将读取的数据写入到多个Buffer中。

聚集 gather 写入Channel指在写操作时将多个Buffer的数据写到同一个Channel,因此,Channel将多个Buffer中的数据聚集后发送到Channel。

scatter/gather 经常用于需要将传输的数据分开处理的场合,例如传输一个消息头消息体组成的消息,这样将消息体消息头分散到不同的Buffer中。

1
2
3
4
5
//scatter read
ByteBuffer header = ByteBuffer.allocate(128);
Byteuffer body =ByteBuffer.allocate(256);
ByteBuffer[] bytes ={header,body};
channel.read(bytes)
1
2
3
4
5
//gather write
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);

通道之间的数据传输

在NIO中,如果两个通道中一个是FileChannel,那你可以直接将数据从一个Channel传输到另外一个Channel中。

FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中。

transferTo()方法将数据从FileChannel传输到其它Channel中。

1
2
3
4
5
6
7
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel,position,count);
1
2
3
4
5
6
7
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(,position, count,toChannel);

Selector

https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html

Selector是NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程就可以管理多个Channel,从而管理多个网络连接。

Selector提供选择已经就绪的任务的能力

一个Selector实例可以同时检查一组信道的I/O状态。用专业术语来说,选择器就是一个多路开关选择器,因为一个选择器能够管理多个信道上的I/O操作。然而如果用传统的方式来处理这么多客户端,使用的方法是循环地一个一个地去检查所有的客户端是否有I/O操作,如果当前客户端有I/O操作,则可能把当前客户端扔给一个线程池去处理,如果没有I/O操作则进行下一个轮询,当所有的客户端都轮询过了又接着从头开始轮询;这种方法是非常笨而且也非常浪费资源,因为大部分客户端是没有I/O操作,我们也要去检查;而Selector就不一样了,它在内部可以同时管理多个I/O,当一个信道有I/O操作的时候,他会通知Selector,Selector就是记住这个信道有I/O操作,并且知道是何种I/O操作,是读呢?是写呢?还是接受新的连接;所以如果使用Selector,它返回的结果只有两种结果,一种是0,即在你调用的时刻没有任何客户端需要I/O操作,另一种结果是一组需要I/O操作的客户端,这是你就根本不需要再检查了,因为它返回给你的肯定是你想要的。这样一种通知的方式比那种主动轮询的方式要高效得多!

简单的说,selector就是多路复用器.Selector 会不断的轮询注册在其上的Channel,如果某个channel上面发生读或者写事件,这个Channel就会处于就绪状态,会被selector轮询出来,然后通过selectionKey可以获取到就绪Channel的集合,进行后续的IO操作.

一个多路复用器selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它没有最大连接句柄1024/2048的限制,这意味着只需要一个线程负责selector的轮询,就可以接入成千上万客户端.

使用Selector的原因

用单个线程来处理多个channel的好处是 只需要更少的线程来处理通道。所以使用selector 能够处理多个通道。

一个线程轮询selector ,当有IO时,从线程池中取出线程进行处理.

并且不必为每个连接都创建一个线程,不用去维护多个线程,

用法

我们需要做的第一件事就是创建一个 Selector

1
Selector selector = Selector.open();

然后,我们将对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector

为了接收连接,我们需要一个 ServerSocketChannel。事实上,我们要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口,我们打开一个 ServerSocketChannel,如下所示:

1
2
3
4
5
6
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking( false );

ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress( ports[i] );
ss.bind( address );

第一行创建一个新的 ServerSocketChannel ,最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。

下一步是将新打开的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法,如下所示:

1
SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );

register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件类型。

请注意对 register() 的调用的返回值。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。

现在已经注册了我们对一些 I/O 事件的兴趣,下面将进入主循环。使用 Selectors 的几乎每个程序都像下面这样使用内部循环:

1
2
3
4
5
6
7
int num = selector.select();
Set selectedKeys = selector.selectedKeys(); //!!!!关键在这里,直接返回的都是有IO请求的,所以就不用像IO一样一个一个轮询,只用轮询有请求的通道.
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey)it.next();
// ... deal with I/O event ...
}

首先,我们调用 Selectorselect() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。

接下来,我们调用 SelectorselectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合

我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。

FileChannel

Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。所以无法使用selector

打开FileChannel

使用FileChannel前,必须先打开它,但是无法直接打开,必须通过使用一个InputStream OutputStream RandomAccessFile来获取一个FileChannel实例。下面是通过RandomAccessFile打开FileChannel的示例。

1
2
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
从FileChannel读取数据

调用多个read()方法从FileChannel读取数据

1
2
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesread =inChannel.read(buf);

首先,分配一个Buffer。从FileChannel中读取的数据将被读到Buffer中。然后,调用FileChannel.read()方法。该方法将数据从FileChannel读取到Buffer中。read()方法返回的int值表示了有多少字节被读到了Buffer中。如果返回-1,表示到了文件末尾。

从FileChannel写数据
1
2
3
4
5
6
7
8
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}

注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。

关闭FileChannel
1
channel.close();
fileChannel的position 方法

特定位置进行读写,也可以调用position(Long pos)方法 设置FileChannel当前位置

1
2
long pos = channel.position();
channel.position(pos +123);
size()

FileChannel实例的size()方法将返回该实例所关联文件的大小。如:

1
long filesize = channel.size();
force()

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。