java NIO

Posted _oldzhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java NIO相关的知识,希望对你有一定的参考价值。

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("服务端启动成功!");
		// 轮询访问selector
		while (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();
	}
}

以上是关于java NIO的主要内容,如果未能解决你的问题,请参考以下文章

疯狂创客圈 JAVA死磕系列 总目录

Java NIO之Selector(选择器)

Java NIO之Selector(选择器)

Java NIO之Selector(选择器)

Java NIO 之 Buffer(缓冲区)

Java NIO 之 Buffer(缓冲区)