nio之所以为为新,在于它并没在原来I/O的基础上进行开发,而是提供了全新的类和接口,除了原来的基本功能之外,它还提供了以下新的特征:
► 多路选择的非封锁式I/O设施
►支持文件锁和内存映射
►支持基于Perl风格正则表达式的模式匹配设施
►字符集编码器和译码器
为了支持这些新的功能,nio使用了两个新的概念:
1. 信道(channel)
信道是一个连接,可用于接收或发送数据,如文件和套接字。因为信道连接的是底层的物理设备,他可以直接支持设备的读/写,或提供文件锁。对于文件、管道、套接字都存在相应的信道类。可以把信道看成是数据流的替代品。信道没有包装类,提高了性能。 所有的信道类都位于java.nio.channels包中。
2. 缓冲区(buffer)
缓冲区是一个数据容器。可以把它看做内存中的一个大的数组,用来存储来自信道的同一类型的所有数据,因此,程序员可以使用字节、字符、整数等缓冲区。字节缓冲区提供必要的方法,可以提取或存入所有基本类型(boolean型除外)的数据。
buffer类的核心是一块内存区,便于核心代码和java代码同时访问,核心代码可以直接访问它,java代码可以通过API访问它。
缓冲区基本上是一块内存区域,因而可以执行一些与内存有关的操作,如清除其中的内容,支持读写或只读操作等。所有的buffer类都位于java.nio包中。
下面看如何使用它们:
1. 使用信道
在信道的使用中,文件的信道是最具有代表性的,API也是最多的,下面我们以文件信道为例介绍它。
● 获取文件信道
文件的信道的类为FileChannel,遗憾的是他并没有向我们提供打开文件的方法,我们可以通过调用FileInputStream、FileOutputStream和RandomAccessFile类实例的getChannel()方法来获取其实例。例如:
RandomAccessFile raf = new RandomAccessFile(“data.txt”, “rw”);
FileChannel fc = raf.getChannel();
● 从信道读取数据
读取的数据会默认放到字节缓冲区中。
FileChannel提供了四个API读取数据:
a. read(ByteBuffer dst) 将字节序列从此通道读入给定的缓冲区
b. read(ByteBuffer[] dsts) 将字节序列从此通道读入给定的缓冲区
c. read(ByteBuffer[] dsts, int offset, int length)
将字节序列从此通道读入给定缓冲区的子序列中
d. read(ByteBuffer dst, long position)
从给定的文件位置开始,从此通道读取字节序列,并写入给定的缓冲区
● 向信道写入数据
数据来源默认是字节缓冲区。
FileChannel提供了四个API写入数据:
a. write(ByteBuffer src)
将字节序列从给定的缓冲区写入此通道
b. write(ByteBuffer[] srcs)
将字节序列从给定的缓冲区写入此通道
c. write(ByteBuffer[] srcs, int offset, int length)
将字节序列从给定缓冲区的子序列写入此通道
d. write(ByteBuffer src, long position)
从给定的文件位置开始,将字节序列从给定缓冲区写入此通道
● 使用文件锁
文件锁机制主要是在多线程同时读写某个文件资源时使用。
FileChannel提供了两种加锁机制,lock和tryLock,两者的区别在于,lock是同步的,
直至成功才返回,tryLock是异步的,无论成不成功都会立即返回。
● 使用内存映射
FileChannel提供的的API为:
MappedByteBuffer map(FileChannel.MapMode mode, long position, long size);
映射模式一个有三种:
a.只读: 试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException.(MapMode.READ_ONLY)
b.读/写: 对得到的缓冲区的更改最终将传播到文件;该更改对映射到同一文件的其他程序不一定是可见的。 (MapMode.READ_WRITE)
c.专用: 对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其他程序也不是可见的;相反,会创建缓冲区已修改部分的专用副本。 (MapMode.PRIVATE)
2. 使用缓冲区● 层次结构所有缓冲区的基类都是Buffer,除Boolean类型外,其它数据类型都有对应的缓冲区类,另有一个ByteOrder类,用来设置缓冲区的大小端顺序,即BigEndian或者是LittleEndian,默认情况下是BigEndian。其层次结构图如下:
● 获取缓冲区对象一共有两种类型的缓冲区,直接缓冲区和非直接缓冲区,两者区别在于直接缓冲区上的数据操作,虚拟机将尽量使用本机I/O,并尽量避免使用中间缓冲区。判断一个缓冲区是否是直接缓冲区,可以调用isDirect()方法。有三种方式来获取一个缓冲区的对象:
a. 调用allocate()或者allocateDirect()方法直接分配,其中allocateDirect()返回的是直接缓冲区。
b. 包装一个数组,如:byte[] b = new byte[1024];ByteBuffer bb = ByteBuffer.wrap(b);
c. 内存映射,即调用FileChannel的map()方法。
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
ByteBuffer buffer2 = ByteBuffer.allocateDirect(1024);
ByteBuffer buffer3 = ByteBuffer.wrap(new String("hello").getBytes());
● 缓冲区基本属性这几个属性是每个缓冲区都有的并且是常用的操作。
a. 容量(capacity),缓冲区大小
b. 限制(limit),第一个不应被读取或写入的字节的索引,总是小于容量。
c. 位置(position),下一个被读取或写入的字节的索引,总是小于限制。
d. clear()方法:设置limit为capacity,position为0。
e. filp()方法:设置limit为当前position,然后设置position为0。
f. rewind()方法:保持limit不变,设置position为0。
● 缓冲区数据操作操作包括了读取和写入数据两种。读取数据使用get()及其系列方法,除boolean外,每一种类型包括了对应的get()方法,如getInt(),getChar()等,get()方法用来读取字节,支持相对和绝对索引两种方式。写入数据使用put()及其系列方法,和get()方法是对应的。 下面这个例子演示了如何使用缓冲区和信道:
package nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class BufferDemo {
public static void main(String[] args) throws Exception{
//分配一个非直接缓冲区
ByteBuffer bb = ByteBuffer.allocate(100);
//向缓冲区写入0到100的字节制
for(int i = 0; i <100; i++){
byte b = (byte) (Math.random() * 100);
bb.put(b); }
System.out.println("写入文件前的缓冲区数据");
bb.flip();
while(bb.hasRemaining())
System.out.print(bb.get() + " ");
System.out.println();
//获取一个关联到文件buffer.txt的信道
FileChannel fc = new FileOutputStream("buffer.txt").getChannel();
//将缓冲区数据写到文件中
bb.flip();
fc.write(bb);
//防止缓存
fc.force(true);
//关闭信道
fc.close();
bb = null;
fc = null;
//下面从文件中读取数据
fc = new FileInputStream("buffer.txt").getChannel();
ByteBuffer bb2 = ByteBuffer.allocate((int) fc.size());
fc.read(bb2);
System.out.println("从文件读取的缓冲区数据");
bb2.flip();
while(bb2.hasRemaining())
System.out.print(bb2.get() + " ");
System.out.println();
fc.close();
bb2 = null;
fc = null; }}
3.视图缓冲区
上面我们的缓冲区都是基于字节的,像IntBuffer、LongBuffer等这些都可以调用ByteBuffer的 as***Buffer(***表示某个数据类型)得到,所以这种类型的缓冲区又被称为视图缓冲区(View Buffer), 视图缓冲区有以下特点:
a. 视图缓冲区有自己独立的position和limit,但它不是一个新的创建,只是原来字节缓冲区的一个逻辑缓冲区,字节缓冲区的任何修改都会影响视图缓冲区,反之亦然。
b. 视图缓冲区按照数据类型的大小进行索引,而不是字节顺序。
c. 也提供了put()和get()及其系列方法,用于数据的整块传输。
下面这个例子演示了视图缓冲区:
package nio;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;
public class ViewBufferDemo {
public static void main(String[] args) throws Exception{
//将文件内容读到缓冲区中
FileChannel fc = new FileInputStream("buffer.txt").getChannel();
ByteBuffer bb = ByteBuffer.allocate((int) fc.size());
fc.read(bb);
fc.close();
fc = null;
System.out.println("从文件读取的字节缓冲区数据");
bb.flip();
while(bb.hasRemaining())
System.out.print(bb.get() + " ");
System.out.println();
//获取视图缓冲区
bb.flip();
IntBuffer ib = bb.asIntBuffer();
System.out.println("将字节缓冲区作为整形缓冲区的数据");
while(ib.hasRemaining())
System.out.print(ib.get() + " ");
System.out.println();
bb = null;
ib = null;
}}
4.映射内存缓冲区
调用信道的map()方法后,即可将文件的某一部分或全部映射到内存中,映射内存缓冲区是一 个直接缓冲区,继承自ByteBuffer,但相对于ByteBuffer,它有更多的优点:
a. 内存映射I/O是对信道/缓冲区技术的改进。 当传输大量的数据时,内存映射I/O速度相对较快,这是因为它使用虚拟内存把文件传输到进程的地址空间中。
b. 映射内存也成为共享内存,因此可以用于相关进程(均映射同一文件)之间的整块数据传输,这些进程甚至可以不必位于同一系统上,只要每个都可以访问同一文件即可。
c. 当对FileChannel执行映射操作,把文件映射到内存中时,得到的是一个连接到文件的映射的字节缓冲区,这种映射的结果是,当输出缓冲区的内容时,数据将出现在文件中,当读入缓冲区时,相当于得到文件中的数据。 下面这个例子演示了映射内存:
package nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class CopyFile {
public static void main(String[] args) throws Exception {
FileChannel fIChan, fOChan;
MappedByteBuffer mBuf;
fIChan = new FileInputStream("buffer.txt").getChannel();
fOChan = new FileOutputStream("bufferTemp.txt").getChannel();
mBuf = fIChan.map(FileChannel.MapMode.READ_ONLY, 0, fIChan.size()); fOChan.write(mBuf);
fIChan.close();
fOChan.close();
fIChan = null;
fOChan = null;
mBuf = null;
}}
NIO网络支持
●服务器端:接收请求的应用程序。
●客户端:向服务器端发出请求的应用程序。
●套接字通道:客户端与服务器端之间的通信通道。它能识别服务器端的IP地址和端口号。数据以Buffer中元素的形式通过套接字通道传送。
●选择器:所有非阻塞技术的主要对象。它监视着已注册的套接字通道,并序列化服务器需要应答的请求。
●关键字:选择器用来对对象的请求进行排序。每个关键字代表一个单独的客户端子请求并包含识别客户端和请求类型的信息。
图一:使用非阻塞套接字体系的结构图。
你可能注意到,客户端应用程序同时执行对服务器端的请求,接着选择器将其集中起来,创建关键字,然后将其发送至服务器端。这看起来像是阻塞(Blocking)体系,因为在一定时间内只处理一个请求,但事实并非如此。实际上,每个关键字不代表从客户端发至服务器端的整个信息流,仅仅只是一部分。我们不要忘了选择器能分割那些被关键字标识的子请求里的数据。因此,如果有更多连续地数据发送至服务器端,那么选择器就会创建更多的根据时间共享策略(Time-sharing policy)来进行处理的关键字。强调一下,在图一中关键字的颜色与客户端的颜色相对应。
服务器端非阻塞(Server Nonblocking)
我以前的部分介绍过的实体都有与其相当的Java实体。客户端和服务器端是两个Java应用程序。套接字通道是SocketChannel类的实例,这个类允许通过网络传送数据。它们能被Java程序员看作是一个新的套接字。SocketChannel类被定义在java.nio.channel包中。
选择器是一个Selector类的对象。该类的每个实例均能监视更多的套接字通道,进而建立更多的连接。当一些有意义的事发生在通道上(如客户端试图连接服务器端或进行读/写操作),选择器便会通知应用程序处理请求。选择器会创建一个关键字,这个关键字是SelectionKey类的一个实例。每个关键字都保存着应用程序的标识及请求的类型。其中,请求的类型可以是如下之一:
●尝试连接(客户端)
●尝试连接(服务器端)
●读取操作
●写入操作
一个通用的实现非阻塞服务器的算法如下:
基本上,服务器端的实现是由选择器等待事件和创建关键字的无限循环组成的。根据关键字的类型,及时的执行操作。关键字存在以下4种可能的类型。
Acceptable: 相应的客户端要求连接。
Connectable:服务器端接受连接。
Readable:服务器端可读。
Writeable:服务器端可写。
通常一个表示接受的关键字创建在服务器端。事实上,这种关键字仅仅通知一下服务器端客户端请求连接。在这种环境下,正如你通过算法得到的结论一样,服务器端个性化套接字通道和连接这个通道到选择器以便进行读/写操作。从这一刻起,当接受客户端读或写操作时,选择器将为客户端创建Readable或Writeable关键字。从而,服务器端将截取这些关键字并执行正确的动作。
现在,你可以用下面这个推荐算法和Java语言写服务器端了。用这种方法能成功的创建套接字通道,选择器,和套接字-选择器注册(socket-selector registration)。
// Create the server socket channel
ServerSocketChannel server = ServerSocketChannel.open();
// nonblocking I/O
server.configureBlocking(false);
// host-port 8000
server.socket().bind(new java.net.InetSocketAddress(host,8000));
System.out.println("Server attivo porta 8000");
// Create the selector
Selector selector = Selector.open();
// Recording server to selector (type OP_ACCEPT)
server.register(selector,SelectionKey.OP_ACCEPT);
我们使用OP_ACCEPT,意思是选择器仅能报告客户端尝试连接服务器端。其他可能的选项是:OP_CONNECT,在客户端下使用;OP_READ; 和OP_WRITE。
// Infinite server loop
for(;;) {
// Waiting for events
selector.select();
// Get keys
Set keys = selector.selectedKeys();
Iterator i = keys.iterator();
// For each keys
while(i.hasNext()) {
SelectionKey key = (SelectionKey) i.next();
// Remove the current key
i.remove();
// if isAccetable = true// then a client required a connection
if (key.isAcceptable()) {
// get client socket channel
SocketChannel client = server.accept();
// Non Blocking I/O
client.configureBlocking(false);
// recording to the selector (reading)
client.register(selector, SelectionKey.OP_READ);
continue; }
// if isReadable = true // then the server is ready to read
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
// Read byte coming from the client
int BUFFER_SIZE = 32;
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
try {
client.read(buffer);
} catch (Exception e) {
// client is no longer active
e.printStackTrace();
continue;
} // Show bytes on the console
buffer.flip();
Charset charset=Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);
System.out.print(charBuffer.toString());
continue;
} }}
循环的第一行是调用select方法,它会阻塞进程执行并等待选择器上记录的事件。在这段代码中,套接字通道由服务器变量指代。实际上,服务器端不是一个SocketChannel对象,而是一个ServerSocketChannel对象。它象SocketChannel一样是SelectableChannel类的一般化,通常用于服务器端的应用程序。
选择器等待的事件是客户端尝试连接。当这样的操作出现时,服务器端的应用程序便获得一个由选择器创建的关键字和检查每个关键字的类型。你也许注意到,当一个关键字被处理时,它不得不调用remove方法从这组关键字中被移出。如果这个关键字的类型是可接受的(isAcceptable()=true),那么服务器端便通过调用accept方法来查找客户端套接字通道,设置它为非阻塞,并将OP_READ选项把它登记进选择器中。我们也可以使用OP_WRITE 或者是OP_READ|OP_WRITE选项,但为了简单,我实现的服务器端仅仅能从通道中读取,不能进行写入操作。
客户端套接字通道现在已经登记入选择器并可进行读取操作。从而,当客户端在套接字通道上写数据时,选择器将通知服务器端应用程序这里有一些数据读。随着可读关键字的创建,从而isReadable()=true。在这个例子中,应用程序从套接字通道上读取数据使用的是32个字节的ByteBuffer,字节译码使用的是ISO-8859-1编码规则,同时读取的数据也会显示在服务器端的控制台上。
客户端非阻塞(Client Nonblocking)
为了检验编制的服务器端能否以非阻塞的方法工作正常,我将实现一个客户端以期在套接字通道上连续地写字符串“Client XXX”,这里的“XXX”是命令行所传递的参数。例如,当客户端运行的命令行的参数是89时,服务器端的控制台上就会显示“Client 89 Client 89 Client 89 Client 89 ...”。如果其它的客户端开始的参数是92时会发生些什么呢?如果服务器端已阻塞,任何事情都不会发生,服务器端还是显示连续的字符串“Client 89”。自从我们的服务器使用了非阻塞套接字,那么控制台就会显示下面这样的字符串:"Client 89 Client 89 Client 92 Client 89 Client 92 Client 92 Client 89 Client 89 ...",这意味着在套接字通道上的读/写操作并不阻塞服务器应用程序的执行。
这里有一段客户端应用程序的代码:
// Create client SocketChannel
SocketChannel client = SocketChannel.open();
// nonblocking I/O
client.configureBlocking(false);
// Connection to host port 8000
client.connect(new java.net.InetSocketAddress(host,8000));
// Create selector
Selector selector = Selector.open();
// Record to selector (OP_CONNECT type)
SelectionKey clientKey = client.register(selector, SelectionKey.OP_CONNECT);
// Waiting for the connection
while (selector.select(500)> 0) {
// Get keys
Set keys = selector.selectedKeys();
Iterator i = keys.iterator();
// For each key
while (i.hasNext()) {
SelectionKey key = (SelectionKey)i.next();
// Remove the current key
i.remove();
// Get the socket channel held by the key
SocketChannel channel = (SocketChannel)key.channel();
// Attempt a connection
if (key.isConnectable()) {
// Connection OK
System.out.println("Server Found");
// Close pendent connections
if (channel.isConnectionPending())
channel.finishConnect();
// Write continuously on the buffer
ByteBuffer buffer = null;
for (;;) {
buffer = ByteBuffer.wrap( new String(" Client " + id + " ").getBytes()); channel.write(buffer);
buffer.clear();
}
}
}}
也许,客户端应用程序的结构让你回忆起服务器端应用程序的结构。然而,这里也有许多不同的地方。套接字通道使用OP_CONNECT选项连接到选择器上,意思是当服务器接受连接时选择器将不得不通知客户端,这个循环不是无穷的。While循环的条件是:
while (selector.select(500)> 0)
意思是客户端尝试连接,最大时长是500毫秒;如果服务器端没有应答,selete方法将返回0,因为在通道上的服务器没有激活。在循环里,服务器端检测关键字是否可连接。在这个例子中,如果有一些不确定的连接,客户端就关闭那些不确定的连接,然后写入字符串“Client”后面接着从命令行参数中带来的变量ID。