相较于传统的IO基于字节流和字符流的阻塞式操作,NIO则是基于通道(channel)和缓冲区(buffer)的非阻塞式操作。数据总是从通道读取到缓冲区或者从缓冲区写入到通道。NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中(map()方法),这样就可以像访问内存一样来访问文件了,也可以采用“用竹筒多次重复取水”的方式,创建一个固定大小的ByteBuff,每次从Channel中读取、写入的也都是固定大小的数据。


Channel:Channel类似流,程序不能直接访问Channel中的数据,Channel只能与Buffer交互,也就是Channel中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。Java NIO中最重要的Channel的实现:

  • FileChannel从文件中读写数据。
  • DatagramChannel能通过UDP读写网络中的数据。
  • SocketChannel能通过TCP读写网络中的数据。
  • ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样,对每一个新进来的连接都会创建一个SocketChannel。


Buffer:Buffer的三个属性capacity、position、limit;position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。

  • capacity:作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
  • position:当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向后移动到下一个可插入数据的Buffer单元。position最大值可为 capacity – 1. 当读取数据时,也是从某个特定位置读,当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向后移动到下一个可读的位置。
  • limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。方法flip()将Buffer从写模式切换到读模式


Selector:选择器是JavaNIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。所有希望采用非阻塞方式通信的Channel都应该注册到Selector上,通过SelectableChannel.register()方法来实现,register()方法的第二个参数是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣,如果对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来。可以监听四种不同类型的事件:

  1. Read(1)
  2. Write(4)
  3. Connect(8)
  4. Accept(16)


一个Selector实例有三个SelectionKey集合:


  1. 所有的SelectionKey集合:代表了注册在该Selector上的Channel,通过keys()方法返回
  2. 被选择的SelectionKey集合:代表了所有可通过select()方法获取的、需要进行IO处理的Channel,通过selectedKeys()方法返回
  3. 被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKeys会被彻底删除


下面是一个读取文件的简单例子:


public class NIODemo {

	public static void main(String[] args) {
		
		try {
			RandomAccessFile aFile = new RandomAccessFile("D:\\test.txt", "rw");

			FileChannel inChannel = aFile.getChannel();
			//创建一个capacity为48 bytes 的Buffer
			ByteBuffer buf = ByteBuffer.allocate(48);
			
			// 将字节序列从channel写到给定buffer,buffer的大小即每次能写入的最大值,这里则是上面定义的48b。
			while(inChannel.read(buf) != -1) {
				
				buf.flip(); //反转Buffer,接着再从Buffer中读取数据,将Buffer从写模式切换到读模式
				
				Charset charset = Charset.forName("UTF-8");
				CharsetDecoder decoder = charset.newDecoder();
				
				//这里注意:当输入字节序列对于给定 charset 来说是不合法的,或者输入字符序列不是合法的 16 位 Unicode 序列时,
				//这里会抛出java.nio.charset.MalformedInputException
				//例如:当在这块ByteBuffer空间中末尾的字符不符合UTF-8编码。 
				//可以选择初始化一块很大的ByteBuffer空间或者使用map()方法将文件全部映射到内存;或者以指定字符读取文件
				CharBuffer cbuff = decoder.decode(buf);
				System.out.println(cbuff);

				buf.clear(); //clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。
				//Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。
				
			}
			
			aFile.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}



下面模拟一个客户端和服务器通过Socket连接使用NIO实现的过程:

//服务端
public class Server {

	public static void main(String[] args) {
		try {
			Selector selector = Selector.open(); //创建一个selector
			ServerSocketChannel channel = ServerSocketChannel.open();
			channel.bind(new InetSocketAddress("127.0.0.01",30000));
			channel.configureBlocking(false); //与Selector一起使用时,Channel必须处于非阻塞模式下。

			//将ServerSocketChannel注册到selector上,返回一个SelectionKey对象
            //这里ServerSocketChannel只能注册SelectionKey.OP_ACCEPT
			SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);
			
			while (true) {
				
				// select方法会一直阻塞,直到IO事件到达或者设置超时返回;
				//当需要处理IO操作,对应的SelectionKey加入被选择的SelectionKey集合
				int readyChannels = selector.select();//返回的是Channel的个数
				if (readyChannels == 0) {
					continue;
				}
				Set<SelectionKey> selectedKeys = selector.selectedKeys();//返回此Selector的已选择键集。
				System.out.println("被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = "+selectedKeys.size());
				
				Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
				
				while (keyIterator.hasNext()) {
					SelectionKey key = keyIterator.next();
					
					// 检测channel中什么事件或操作已经就绪
					if (key.isAcceptable()) {     //1.对应的Channel包含客户端的连接
						
						ServerSocketChannel sscTemp = (ServerSocketChannel) key.channel();
                       
						//得到一个连接好的SocketChannel
                        SocketChannel socketChannel = sscTemp.accept();
                        socketChannel.configureBlocking(false);
                        
                        //将得到的SocketChannel注册到Selector上,并对该SocketChannel添加兴趣
                        //SocketChannel中可以注册SelectionKey.OP_READ、SelectionKey.OP_WRITE、SelectionKey.OP_CONNECT
                        socketChannel.register(selector, SelectionKey.OP_READ);// ......(1)
                      
                        System.out.println("注册在该Selector上的Channel个数:" + selector.keys().size());
						
					} else if (key.isReadable()) { //2.读取数据,满足Readable条件,则此Channel已准备好进行读取
						//读取通道中的数据
                        SocketChannel readchannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(128);
                        buffer.clear();
						try {
							while (readchannel.read(buffer) > 0) {
								buffer.flip();
								byte[] bytes = new byte[buffer.remaining()];
								buffer.get(bytes);
								System.out.println("server收到的数据 ..."+ new String(bytes));
							}
						} catch (IOException e) {
							e.printStackTrace();
						}		
						
						//......(2)
//						if(readchannel.read(buffer) < 0) {
//							key.cancel();
//						}
						
					} else if (key.isConnectable()) {  //3.连接
						
						System.out.println("connect ...");
			
					} else if (key.isWritable()) {     //4.写

						System.out.println("write .. ");
					
					}
					
					keyIterator.remove();
				}
			}
			
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

}

客户端代码如下:


public class Client extends Thread{
	
	String threadname;
	
	public Client(String threadname){
		this.threadname = threadname;
	}
	
	@Override
	public void run(){

		SocketChannel socketChannel;
		Selector selector;

		try {
			selector = Selector.open();
			InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);
			socketChannel = SocketChannel.open(isa);
			socketChannel.configureBlocking(false);
			
			//将SocketChannel对象注册到指定Selector
			//......(3)
			socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
			
			String data = "这是内容,发给服务端,来自 -- " + threadname;
			ByteBuffer bd = ByteBuffer.allocate(64);
			bd.clear();//开始往Buffer里写数据。
			
			bd.put(data.getBytes());//写入Buffer
			bd.flip();//写模式切换为读模式  
			
			//Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止
			while(bd.hasRemaining()){ //当且仅当此缓冲区中至少还有一个元素时返回 true
				socketChannel.write(bd);
			}	
			
			socketChannel.close();	

		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}

	public static void main(String[] args) {
		ExecutorService es = Executors.newCachedThreadPool();
		es.submit(new Client("线程1"));
		es.submit(new Client("线程2"));
		es.shutdown();
	}
	
}




debug运行客户端和服务端输出结果如下:


被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 1
注册在该Selector上的Channel个数:2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
注册在该Selector上的Channel个数:3
server收到的数据 ...这是内容,发给服务端,来自 -- 线程2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
server收到的数据 ...这是内容,发给服务端,来自 -- 线程1
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
...



结果解析:一开始服务端的Selector注册了一个ServerSocketChannel监听新进来的TCP连接,因此只有一个Channel,当连接进来后,对新进来的连接都会创建一个SocketChannel,并注册新的事件(read事件 (1)处),这时注册在该Selector上的Channel个数就 +1,而任何对 Key所关联的兴趣操作集的改变,都只在下次调用了 select()方法后才会生效,所以read事件在下次select()方法之后进行,而接下来需要进行IO处理的Channel +1。

总共有两个线程创建了两个SocketChannel,加上ServerSocketChannel,因此服务端Selector上共注册了3个Channel。处理完数据之后,发现一直在输出外层循环中的“被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2”,且循环会一直进入key.isReadable(),可是client中的Channel已经关闭了,这是为什么呢?当客户端主动切断连接时,read仍然起作用,也就是说,状态仍然是有东西可读,不过读出来的字节是0,所以需要进一步判断一下读取的字节的数目,把(2)处的注释打开即可删除该Channel的注册关系,当读完Channel中的数据之后,客户端的两个Channel就没有了,就只剩下了ServerSocketChannel这一个Channel了。可以运行新的Client,Server仍可以正常工作。




java jni实例 java nio 例子_数据


还有一个坑!:

  • 在NIO中通过Selector的轮询当前是否有IO事件,根据JDK NIO api描述,Selector的select方法会一直阻塞,直到IO事件达到或超时,但是在Linux平台上这里有时会出现问题,在某些场景下select()方法会直接返回,即使没有超时并且也没有IO事件到达,这就是著名的epoll bug,这是一个比较严重的bug,它会导致线程陷入死循环,会让CPU飙到100%,极大地影响系统的可靠性,到目前为止,JDK都没有完全解决这个问题。


参考文章:

Java NIO 系列教程

基于 NIO 的 TCP 通信

Java NIO浅析

Java NIO编程实例之三Selector