java NIO

来源:互联网 发布:让座 知乎 编辑:程序博客网 时间:2024/05/27 00:28

javaNIO是非阻塞的IO。可以用于替代IO操作,但用于对文件的操作时它并不能设置为非阻塞,它的优势体现在网络通信上。从上一篇文章java网络-Socket来看,即使使用多线程来处理Socket,但一个线程只能处理一个客户端的请求,单个线程在read的时候还是会阻塞,开销还是很大。如果使用NIO来处理,当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Channels、Buffers、Selectors是NIO的核心组成部分。

Channel:

Channel类似于IO中的流。主要实现有:FileChannel(用于处理文件)DatagramChannel(处理UDP)SocketChannel(处理TCP连接)、ServerSocketChannel(可以监听TCP连接,和ServerSocket能创建一样它可以创建SocketChannel),但流是单向读写的,要实现对流的读写需要分别使用input/ouput流,Channel是双向的。而且通道可以异步读写。数据在通道中需要先写入到Buffer中,也只能从Buffer中读取。

Selector
Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。


File读取文件的一个例子

public class FileChannelTest {    public static void main(String args[]) throws IOException {        RandomAccessFile aFile = new RandomAccessFile("nio-data.txt", "rw");        FileChannel inChannel = aFile.getChannel();        //定义一个固定大小的buffer        ByteBuffer buf = ByteBuffer.allocate(48);        //将数据从channel写入buffer 返回channel字节数        int bytesRead = inChannel.read(buf);        while (bytesRead != -1) {            System.out.println("Read " + bytesRead);            buf.flip();            while(buf.hasRemaining()){                System.out.print((char) buf.get());            }            //将buffer清空 并将空的channel写入buf 控制循环结束            buf.clear();            bytesRead = inChannel.read(buf);        }        aFile.close();    }}
Buffer:

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存Buffer读写数据一般经过下面四个步骤:
1,写入数据到Buffer
2,调用flip()方法:将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。
3,从Buffer中读取数据
4,调用clear()方法或者compact()方法:

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。

clear()方法会清空整个缓冲区。

compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
Buffer有三个重要的属性:capacity、position、limit

capacity指Buffer创建时的容量大小,position和limit取决于当前是读还是写模式。


在写模式下:

position初始为0,当写入一个数据到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。

limit表示可写的最大位置,和capacity相同。

在读模式下:

position会在切换到读模式时重置为0,表示可从最开始位置读取,当读取完一个buffer单元后会后移一个单元到下一次读取的位置。

limit为所有数据占据的最大buffer单元处,也就是在切换前写模式下的position位置。

向Buffer中写数据:

1,从Channel写到Buffer:inChannel.read(buf);

2,使用Buffer的put()方法:buf.put(123);

从Buffer中读取数据:

1,从Buffer读取数据到Channel:inChannel.write(buf);

2,使用get()方法从Buffer中读取数据:byte aByte = buf.get();

Buffer常用方法:

1,flip()方法:将Buffer从写模式切换到读模式,将position设置为0,写模式下的position设置为limit

2,rewind()方法:将position设回为0,所以可以重读Buffer中的所有数据

3,clear()、compact()方法:上面已介绍,实际上clear也不会真正清除数据,只是将position设置为0,可以从0开始写了。如果存在未读数据使用compact()可将未读数据置前。

4,mark()、reset()方法:可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。

通道之间的数据传输:

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

FileChannel的transferFrom()方法可以将数据从源通道传输到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();toChannel.transferFrom(position, count, fromChannel);//fromChannel.transferTo(position, count, toChannel);
方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。

Selector:
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。单个线程处理多个Channels的好处是可以减少多线程切换的开销。
Selector的创建
Selector selector = Selector.open();
向Selector注册通道
SelectableChannel.register()方法来实现,如下:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。
Selectionkey.OP_READ是一个interest集合,selector监听的事件类型,有Connect/Accept/Read/Write等类型。可以同时监听多个。返回的类型也是SelectionKey类型,包含了interest集合、ready集合、Channel、Selector

interest集合:
注册时监听的事件类型
ready集合:
ready 集合是通道已经准备就绪的操作的集合,可以通过下面四个方法来检测目前就绪的事件是什么事件,从而进行相应的处理
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
获取Channel和Selector:
Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();
SelectionKey还可以携带附加的对象。

//获取selector对象Selector selector = Selector.open();//设置Channel为非阻塞channel.configureBlocking(false);//向channel注册选择器 非监听read事件的就绪状态SelectionKey key = channel.register(selector, SelectionKey.OP_READ);while(true) {  //阻塞方法  有read事件就绪时会返回就绪channel的个数  int readyChannels = selector.select();  if(readyChannels == 0) continue;  //访问“已选择键集”中的就绪通道SelectionKey对象   Set selectedKeys = 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    }    //Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。    keyIterator.remove();  }}

下面是一个完整的例子,Selector监听Accept事件,监听到新进来的连接创建SocketChannel后又让原来的Selector监听这个通道上的read事件,当read就绪后打印客户端传输过来的数据。

public class NIOServer {// 通道管理器private Selector selector;/** * 获得一个ServerSocket通道,并对该通道做一些初始化的工作 * @param port 绑定的端口号 * @throws IOException */public void initServer(int port) throws IOException {// 获得一个ServerSocket通道ServerSocketChannel serverChannel = ServerSocketChannel.open();// 设置通道为非阻塞serverChannel.configureBlocking(false);// 将该通道对应的ServerSocket绑定到port端口serverChannel.socket().bind(new InetSocketAddress(port));// 获得一个通道管理器this.selector = Selector.open();// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,// 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。serverChannel.register(selector, SelectionKey.OP_ACCEPT);}/** * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 * @throws IOException */public void listen() throws IOException {System.out.println("服务端启动成功!");// 轮询访问selectorwhile (true) {// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞selector.select();// 获得selector中选中的项的迭代器,选中的项为注册的事件Iterator<?> ite = this.selector.selectedKeys().iterator();while (ite.hasNext()) {SelectionKey key = (SelectionKey) ite.next();// 删除已选的key,以防重复处理ite.remove();handler(key);}}}/** * 处理请求 * @param key * @throws IOException */public void handler(SelectionKey key) throws IOException {// 客户端请求连接事件if (key.isAcceptable()) {handlerAccept(key);// 获得了可读的事件} else if (key.isReadable()) {handelerRead(key);}}/** * 处理连接请求 * @param key * @throws IOException */public void handlerAccept(SelectionKey key) throws IOException {ServerSocketChannel server = (ServerSocketChannel) key.channel();// 获得和客户端连接的通道SocketChannel channel = server.accept();// 设置成非阻塞channel.configureBlocking(false);// 在这里可以给客户端发送信息哦System.out.println("新的客户端连接");// 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。channel.register(this.selector, SelectionKey.OP_READ);}/** * 处理读的事件 * @param key * @throws IOException */public void handelerRead(SelectionKey key) throws IOException {// 服务器可读取消息:得到事件发生的Socket通道SocketChannel channel = (SocketChannel) key.channel();// 创建读取的缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);int read = channel.read(buffer);if(read > 0){byte[] data = buffer.array();String msg = new String(data).trim();System.out.println("服务端收到信息:" + msg);//回写数据ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());channel.write(outBuffer);// 将消息回送给客户端}else{System.out.println("客户端关闭");key.cancel();}}/** * 启动服务端测试 * @throws IOException */public static void main(String[] args) throws IOException {NIOServer server = new NIOServer();server.initServer(8000);server.listen();}}

1 1
原创粉丝点击