http://tutorials.jenkov.com/java-nio/overview.html 这人的东西该好好看看。
- Java NIO 指导
NIO 是一个新的可供选择的java io API (自从java1.4开始)。意味着这个新类可以替换标准的JAVA IO 和JAVA NetWorking 的 API。NIO 提供了一个不同的方法进行输入输出,其能力超过标准IO。
java NIO :通道和缓冲区。
在标准IO API,你通过字节流和字符流进行操作。在NIO,你使用通道和缓冲区,数据总是从通道读取到缓冲区,或者从缓冲区写入通道。
(我个人理解呀,通道和IO中的流性质差不多,都是指向数据源和接受的地方,它只起到中介的作用,NIO中多了个缓冲区,要使用的数据都会预先放在缓冲区中。好像多了这个性能还有提升?)
java NIO 非阻塞IO。
java NIO 使你能够实现非阻塞IO,一个线程能请求一个通道去读取数据进入缓冲区,当这个通道正在执行读取数据到缓冲区的操作的时候,线程可以做一些其他事情。当数据读取完毕,该线程可以继续进行数据的相关操作。往外写数据也一样。(好像性能提升就因为这个?不太理解,等找个实例看看。)
java NIO :选择器
java NIO 中有一个概念:选择器,选择器是一个能够监控大量通道的事件(比如说连接打开,数据到达等等)的对象。因此,一个单线程能监控很多通道的数据。
这是怎么运行的在接下来的几篇文章中有提及。
2.java NIO 总览
NIO 由一下三种核心构建组成:通道,选择器,缓冲区。
除此之外 NIO 还有很多类和组件,但是这三种是这个API的核心,在我看来,其他组件,比如说pipe,
FileLock 等等仅仅是一些能被用于连接到这三个组件上的通用类。因此,我会先向三个组件开炮。其他组件也会在它们各自的目录下说明,看看文章顶部的菜单吧(然而我没有。)
通道和缓冲区
典型的,所有NIO中的IO都随着通道一起启动,通道就像流一样(看吧!和我猜的一样)。数据通过通道能够被写入一个缓冲区,数据同样也能从缓冲区写入通道。一下是个图列。
下面介绍NIO主要的通道
- FileChannel 文件通道
- DatagramChannel 数据通道
- SocketChannel 连接到TCP网络套接字的通道(要单独拎出来看看)
- ServerSocketChannel (服务器?)
看吧,这些通道包含了UDP+TCP网络IO,和文件IO。
还有一些iinteresting的接口搭配这个些类,但是我懒得把它们放在这里讲——哪用到在哪讲。
下面介绍一些核心缓冲区:
- ByteBuffer 字节缓冲
- CharBuffer 字符缓冲
- DoubleBuffer 双精度缓冲
- FloatBuffer 浮点缓冲
- IntBuffer 整形缓冲
- LongBuffer 长整形缓冲
- ShortBuffer 短整行缓冲
- (亏的我还记得这些数据类型叫啥)
这些缓冲区包含了一些你能通过IO传输的基本数据类型。
NIO 还有一个 MappedByteBuffer ,用来连接内存映射的文件,我在这也不细说。(说了我特不懂)
选择器
选择器允许一个线程处理多个通道。如果你的应用有很多通道连接是打开的,它也能应对自如。但是它只能处理少量数据传送,比方说聊天服务。
下面是一个例子:一个选择器处理三个通道。
使用选择器,你能注册通道。然后你能调用它的select()方法。这个方法将要阻塞直到通道被注册。(意思是之后注册过的通道才能使用该方法)。一旦这个方法有响应 ,线程就能处理这些通道的事件,比如说获取连接,获取数据等等。
NIO 通道
NIO通道和io流很相似,但是它们有以下不同点:
- NIO能读取或者写入到一个通道,流是个只能单项操作的弱鸡。
- 通道能异步读写。(联想下 javascript的ajax的 asyn(true))。
- 通道总是从缓冲区读出或写入。
通道的实现类
以下是主要的通道实现类:
- FileChannel 文件通道
- DatagramChannel 数据通道 通过UDP读写数据 UDP:用户数据报协议
- SocketChannel 网络通信通道 通过 TCP连接网络
- ServerSocketChannel (???) 允许你监控进入的TCP连接,比如说web服务器,每次连接都有一个SocketChannel被创建。
代码实例:
import java.io.FileNotFoundException;import java.io.IOException;import java.io.RandomAccessFile;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class File { public File() throws FileNotFoundException { } public static void main(String[] args) throws IOException {//先有文件 RandomAccessFile aFile = new RandomAccessFile("nio-data.txt", "rw");//由文件得到文件与缓冲区连接的通道 FileChannel inChannel = aFile.getChannel();//新建一个缓冲区 ByteBuffer buf = ByteBuffer.allocate(48);//写入BUFF 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(); }}
注意 flip()的调用,首先你读取数据到缓冲区,然后你“翻转“”它,然后你能读取它。欲知详情,接着看。
(以后再好好看看RandomAccessFile)
3.java NIO 缓冲区
NIO 缓冲区被用于和通道协同工作,就像你指导的,数据从通道读取到缓冲区,从缓冲区写入通道。
缓冲区本质上来讲是一个块内存区域,在这块区域你能读写数据。这块内存区域被一个NIO 缓冲对象包装起来,这个对象提供一系列方法让它自己能更简单地操作内存块。
缓冲区的基本用法
使用一个缓存对象读写数据需要以下四个步骤。
1,读取数据进入缓冲区。
2,调用 buffer.flip()方法(稍后分析flip 源码)
3,从缓冲区读取数据。
4,调用 buffer.clear() 或者 buffer.compact()。
当你读取数据进入缓冲区时,缓冲区会记录你已经写了多少数据。一旦你需要读取这些数据,你需要通过调用buffer.flip()方法把缓冲区从写入模式转变成读出模式。在读取模式,缓冲区对象让你能读取所有所写入的数据。
一旦你已经获取了所有数据,你需要清空缓冲区,使它能再次被写入。clear()和compact()这两个方法都能让你清空缓冲区数据。clear()清空整个缓冲区,compact()仅仅清空你已经读取过的数据。任何未读取的数据都会移动到缓冲区的开头,新写入的数据将会跟在未读取过的数据的后面。
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");FileChannel inChannel = aFile.getChannel();//create buffer with capacity of 48 bytesByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buf); //read into buffer.while (bytesRead != -1) { buf.flip(); //make buffer ready for read while(buf.hasRemaining()){ System.out.print((char) buf.get()); // read 1 byte at a time } buf.clear(); //make buffer ready for writing bytesRead = inChannel.read(buf);}
(另一博客再附上我自己写的代码吧。)
缓冲区容量,位置和限制。
一个缓冲区对象有三个属性你需要了解。为了了解缓冲区是如何工作的。它们是:
容量。
位置。
限制。
位置和限制的含义取决于缓冲区对象是读取模式还是写入模式。容量的含义总是相同的,无论缓冲区对象的模式是什么。
以下简要说明了它们在不同模式下的含义。
容量
作为一个内存块,缓存对象有一个确定的、固定的大小。你只能写字节,长整型,字符等等到缓冲区对象。一旦这个对象满了。你需要在你写更多数据进去之前清空它。
位置
当你写入数据到缓冲区的时候,你需要在一个确定的内存位置上进行操作。初始化的位置是0,当一个字节,长整型或其他写入缓冲区对象的时候,指向位置改变它的指针到下一个区域。位置指针的最大值是容量-1.
限制(暂时这么翻译)
在写入模式下,缓冲区对象的限制是你最大能写入多少数据,即缓冲区的容量。
当翻转缓冲区为读取模式的时候,限制的含义是你最多能从缓冲区对象读取多少数据。因此,当翻转一个缓冲区对象到读取模式,在写入模式下。限制是一系列写入位置。换句话说,你写了多少就能读取多少。(限制被设置为写入模式被标记的位置)(很简单的含义绕那么一大圈,以后用代码说明吧)
缓冲区类型
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些缓冲区类型代表了不同的数据类型。换句话说,它们让你以前用字节流进行操作的方式用字符,整形等等代替。
MappedByteBuffer
有点特殊,它会被它自己的内容覆盖。???
分配一个缓冲区
你能通过以下两种方法往缓冲区写入数据。
1.从通道往缓冲区写数据。
2.自己通过put()方法往缓冲区写入数据。
下面是一个例子说明从通道往缓冲区写数据:
int bytesRead = inChannel.read(buf); //read into buffer.
通过put()方法写入数据。
buf.put(127);
还有很多其他对put()方法的重写,它允许你用不同的往缓冲区写数据。比如 写到一个特别的位置,或者写一串数组到缓冲区。自己去看API描述吧。
flip()
这个方法能转变缓冲区的读写模式,调用这个方法设置“操作位置”到0.设置“限制多小”到“操作位置”所在地方。
换句话说,“Position”标记了读取位置,“limit”标记了有多少字节,字符等数据能被写入缓冲区或从缓冲区读取。
从缓冲区读取数据
有以下两种方式你能从缓冲区读取数据。
1.从缓冲区读数据到通道。
2.使用get()方法从缓冲区中获取数据到程序中。
一下为它们两个方法的例子。
//read from buffer into channel.int bytesWritten = inChannel.write(buf);
byte aByte = buf.get();
同样。get()方法也有很多种形式的重载,允许已多种形式你读取数据到缓冲区。比如,从缓冲区读取一个特定的位置或读取一列数组。具体看API。
rewind() Buffer.rewind()这个方法设置“POSITION”到零,所以你能重新读取Buffer里面的所有数据。“limit”没有改变,因此还需要标记有多少元素(字节,字符。。)能被读取到缓冲区。
clear() and compact()
一旦你已经从缓冲区里面读取了数据,你应该让缓冲区能再次被写入数据,因此你能调用以上那两个方法。
如果你已经调用clear()让position设置为0同时limit设置为容量最大值。换句话说,缓冲区已经被清空了,在缓冲区的数据还没有清空,只有标记告诉你你能在哪写入数据。
如果在缓存区保存还没有被读取过的数据,当你调用clear()时,这些数据将要被擦除。意味着你不能再标记你已经读取或还有哪些数据没有读取过。
如果在缓冲区还有没读取过的数据,同时你想稍后再读取它,但是你需要先写入一些东西,你可以调用compact()代替clear()。
compact()复制所有没有读取过的数据到缓冲区的开头,然后设置position到没读取过的数据的右边,limit 还是保持设置为capacity最大值,就像clear()做的一样。现在缓冲区已经准备写入数据,同时它不会覆盖你没读取过的数据。
mark() and reset()
你能通过buffer.mark()在缓冲区标记一个位置.你能在之后重置Position到标记的位置通过调用buffer.reset()方法。下面有一个例子。
buffer.mark();//call buffer.get() a couple of times, e.g. during parsing.buffer.reset(); //set position back to mark.
equals() and compareTo()
使用以上两个方法是可行的。
equals()
如果满足一下条件。则两个buffer是相等的。
1.它们是同样的类型(byte,char。。)
2.它们有相同数量的数据储存在buffer中。
3.所有剩余数据都是相同的。
如同你看到的,equals 仅仅比较buffer的一部分而不是每一个元素,它仅仅比较剩余的元素。
compareTo()
这个方法比较两个buffer剩余的元素,比如一个buffer被认为和另一个相似以为
1.第一个元素与与之做对比的另一个元素相同。
2.所有元素相同,但是第一个buffer在第二个buffer之前用光了所有元素(它有更少的元素)
(以上不理解,以后找个代码看看。)
Java NIO Scatter / Gather
NIO伴随着内建的聚集和分散支持,分散、聚集这个概念用于读取和写入通道。
一个从通道读取的scattering是一个读取操作~~~读取数据到多个buffer(这就是scatter的含义)。因此,channel从多个缓冲区聚集数据到一个channel。
一个gathering 是写入到一个通道的操作。多个Buffer的数据写入到一个channel。(都是相对的。)因此,channel聚集数据从多个buffer到一个channel。
Scatter / gather 在你需要多种传输数据分开使用的时候有用,比如,如果一条信息由头部和身体组成,你可能需要保持头部和身体在不同的buffer,这么做可能让你更容易的分开使用它们。
Scattering Reads
分散读取:读取数据从一个通道到多个buffer:
以下是代码:
ByteBuffer header = ByteBuffer.allocate(128);ByteBuffer body = ByteBuffer.allocate(1024);ByteBuffer[] bufferArray = { header, body };channel.read(bufferArray);
注意 缓冲区们怎么第一次被插入一个数组,然后数组通过channel.read()读取参数,read()方法然后顺序的从channel写到不同的buffer.一旦buffer满了,channel写到接下来另一个buffer。
事实上scattering 在移动到下一个buffer之前读取 填充满一个buffer。意思是它对应动态大小的信息是不合适的。换句话说,如果你已经有了header和body,header的大小是固定的(比如说128字节),那么scattering能正常工作。
Gathering Writes
代码如下:
ByteBuffer header = ByteBuffer.allocate(128);ByteBuffer body = ByteBuffer.allocate(1024);//write data into buffersByteBuffer[] bufferArray = { header, body };channel.write(bufferArray);
那些buffers 通过write()方法——顺序的写下buffers里面的内容。只有数组在“position”和“limit”之间的数据能被写入。因此,如果一个buffer有一个128字节的容量,但是只有58字节已经被写入从buffer到channel。因此,一个gathering 写操作能在动态大小的消息部分运行下去。对比scattering。
Java NIO Channel to Channel Transfers
在NIO中,你能直接从一个通道向另一个通道转移数据,如果其中一个通道是FileChannel 。那么FileChannel 类中有一个犯法是transferTo()和transferFrom方法。
transferFrom()
这个方法能让你从一个源通道把数据转移到FileChannel。
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);
参数说明,要写入的目标文件的position。要传输的最大字节数count。如果源通道要传输的自己输少于count,那么就少传点。
除此之外,一些SocketChannel的实现传输的数据可能只是SocketChannel当前已经存在于它的内部的buffer。因此,它可能不会从SocketChannel传输所有需求的数据到FileChannel。
transferTo()
从FileChannel传输数据到其他Channel。
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);
SocketChannel存在的问题在这个方法中也有。
SocketChannel 可能仅仅从FileChannel传输字节指导发送buffer满了,然后它停止。
Java NIO Selector
NIO组件中的选择器能检查一个或多个NIO通道,然后决定哪个通道已经准备好,比如说读或者写。通过这种方法,一条线程能管理多个通道,因此多个网络服务能连接。
Why Use a Selector?
使用一条线程处理多个通道的好处是你需要更少的线程处理这些通道,事实上。你能使用一条线程处理所有通道,在线程之间切换对于操作系统来世代价是很高的,每一条线程也占用一些系统资源,因此,所用线程越少越好。
记住:现在的操作系统和CPU在处理多任务操作时越来越好。所以处理多线程的开销越来越小。事实上如果一个CPU有多个核心,你可能要浪费CPU性能由于没有使用多任务同时处理。不管怎样,这些东西和本文毛关系都没有。总而言之,你能处理多线程通道通过一个线程。
Java NIO: A Thread uses a Selector to handle 3 Channel's |
Creating a Selector
Selector.open()方法能创建一个选择器。估计是个static方法。
Selector selector = Selector.open();
通过选择器创建通道
为了通过selector使用Channel,你必须注册通过选择器注册通道。如下所示。
channel.configureBlocking(false);SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
通道在和选择器一起使用的时候必须处于一个非阻塞的模式。这意味着你不能使用FileChannel和Selector仪器工作。因为FileChannel不能被转变成非阻塞模式,然而Socket能。
注意:第二个参数的register(),这是一个有趣的设定。意味着通过选择器,你可以选择你感兴趣的通道监听。以下你有四种时间你能监听。
1.连接
2.接受
3.读
4.写
A channel that "fires an event" is also said to be "ready" for that event. So, a channel that has connected successfully to another server is "connect ready". A server socket channel which accepts an incoming connection is "accept" ready. A channel that has data ready to be read is "read" ready. A channel that is ready for you to write data to it, is "write" ready.
一个通道为一个事件准备好。一个已经连接成功的通道对于另一个服务是“准备连接”。一个服务器嵌套通道
(看不懂。。。)
These four events are represented by the four SelectionKey
constants:
以下有四个事件表示SelectionKey常量。
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果你对多个事件感兴趣,使用OR。
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey's
就像之前章节说的,当你通过Selector通过register()方法注册Channel时候返回一个SelectionKey对象。
这个对象包含一些有趣的属性。
- The interest set
- The ready set
- The Channel
- The Selector
- An attached object (optional)
以下会有说明。
Interest Set
The interest set is the set of events you are interested in "selecting", as described in the section "Registering Channels with the Selector". You can read and write that interest set via the SelectionKey
like this:
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;
As you can see, you can AND the interest set with the given SelectionKey
constant to find out if a certain event is in the interest set.
Ready Set
The ready set is the set of operations the channel is ready for. You will primarily be accessing the ready set after a selection. Selection is explained in a later section. You access the ready set like this:
int readySet = selectionKey.readyOps();
You can test in the same way as with the interest set, what events / operations the channel is ready for. But, you can also use these four methods instead, which all reaturn a boolean:
selectionKey.isAcceptable();selectionKey.isConnectable();selectionKey.isReadable();selectionKey.isWritable();
Channel + Selector
Accessing the channel + selector from the SelectionKey
is trivial. Here is how it's done:
Channel channel = selectionKey.channel();Selector selector = selectionKey.selector();
Attaching Objects
You can attach an object to a SelectionKey
this is a handy way of recognizing a given channel, or attaching further information to the channel. For instance, you may attach the Buffer
you are using with the channel, or an object containing more aggregate data. Here is how you attach objects:
selectionKey.attach(theObject);Object attachedObj = selectionKey.attachment();
You can also attach an object already while registering the Channel
with the Selector
, in the register()
method. Here is how that looks:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
Selecting Channels via a Selector
Once you have register one or more channels with a Selector
you can call one of the select()
methods. These methods return the channels that are "ready" for the events you are interested in (connect, accept, read or write). In other words, if you are interested in channels that are ready for reading, you will receive the channels that are ready for reading from the select()
methods.
Here are the select()
methods:
- int select()
- int select(long timeout)
- int selectNow()
select()
blocks until at least one channel is ready for the events you registered for.
select(long timeout)
does the same as select()
except it blocks for a maximum of timeout
milliseconds (the parameter).
selectNow()
doesn't block at all. It returns immediately with whatever channels are ready.
The int
returned by the select()
methods tells how many channels are ready. That is, how many channels that became ready since last time you called select()
. If you call select()
and it returns 1 because one channel has become ready, and you call select()
one more time, and one more channel has become ready, it will return 1 again. If you have done nothing with the first channel that was ready, you now have 2 ready channels, but only one channel had become ready between each select()
call.
selectedKeys()
Once you have called one of the select()
methods and its return value has indicated that one or more channels are ready, you can access the ready channels via the "selected key set", by calling the selectors selectedKeys()
method. Here is how that looks:
SetselectedKeys = selector.selectedKeys();
When you register a channel with a Selector
the Channel.register()
method returns a SelectionKey
object. This key represents that channels registration with that selector. It is these keys you can access via the selectedKeySet()
method. From the SelectionKey
.
You can iterate this selected key set to access the ready channels. Here is how that looks:
SetselectedKeys = selector.selectedKeys();Iterator 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();}
This loop iterates the keys in the selected key set. For each key it tests the key to determine what the channel referenced by the key is ready for.
Notice the keyIterator.remove()
call at the end of each iteration. The Selector
does not remove the SelectionKey
instances from the selected key set itself. You have to do this, when you are done processing the channel. The next time the channel becomes "ready" the Selector
will add it to the selected key set again.
The channel returned by the SelectionKey.channel()
method should be cast to the channel you need to work with, e.g a ServerSocketChannel or SocketChannel etc.
wakeUp()
A thread that has called the select()
method which is blocked, can be made to leave the select()
method, even if no channels are yet ready. This is done by having a different thread call the Selector.wakeup()
method on the Selector
which the first thread has called select()
on. The thread waiting inside select()
will then return immediately.
If a different thread calls wakeup()
and no thread is currently blocked inside select()
, the next thread that calls select()
will "wake up" immediately.
close()
When you are finished with the Selector
you call its close()
method. This closes the Selector
and invalidates all SelectionKey
instances registered with this Selector
. The channels themselves are not closed.
Full Selector Example
Here is a full example which opens a Selector
, registers a channel with it (the channel instantiation is left out), and keeps monitoring the Selector
for "readiness" of the four events (accept, connect, read, write).
Selector selector = Selector.open();channel.configureBlocking(false);SelectionKey key = channel.register(selector, SelectionKey.OP_READ);while(true) { int readyChannels = selector.select(); if(readyChannels == 0) continue; SetselectedKeys = selector.selectedKeys(); Iterator 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(); }}