NIO

转载自:http://blog.csdn.net/ns_code/article/details/15460405
              http://blog.csdn.net/ns_code/article/details/15378417
              http://blog.csdn.net/ns_code/article/details/15545057

      Java NIO (New IO) 是一个可供选择的 Java API (从Java 1.4引入),它可以替代标准的java IO API。它提供了一种与标准IO不同的工作方式。
      在标准的IO中,是通过字符流和字节流进行操作,而在NIO中,则通过Channels and Buffers(通道和缓冲区)工作。数据总是从通道读入缓冲区,或者从缓冲区写入通道。
      Java NIO实现了Asynchronous IO(异步IO)操作。比如:当线程从通道读取数据到缓冲区时,它也可以做其他事情,一旦数据被写入缓冲区后,线程可以继续处理它。从缓冲区向通道写数据也类似。
      Java NIO中包含Selectors(选择器)的概念,一个选择器可以监听多个通道上发生的事件(比如:连接建立,数据到达等),这样单个的线程可以监听多个数据通道,这也是非阻塞IO的核心。而在标准IO的Socket编程中,单个线程则只能在一个端口监听。

      NIO包(java.nio.*)引入了四个关键的抽象数据类型:
      1、 Buffer:它是包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
      2、 Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作。
      3、 Channels:包含socket,file和pipe三种管道,它实际上是双向交流的通道。
      4、 Selector:它将多元异步I/O操作集中到一个或多个线程中。

      标准的IO是基于字节流和字符流进行操作的,它不能前后移动流中的数据,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作的,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中,需要时可以在缓冲区中前后移动所保存的数据。
      在标准IO的Socket编程中,套接字的某些操作可能会造成阻塞:accept()方法的调用可能会因为等待一个客户端连接而阻塞,read()方法也可能会因为没有数据可读而阻塞,write()方法在数据没有完全写入时也可能会发生阻塞,阻塞发生时,该线程被挂起,什么也干不了。NIO则具有非阻塞的特性,可以通过对channel的阻塞行为的配置,实现非阻塞式的信道。在非阻塞情况下,线程在等待连接,写数据等(标准IO中的阻塞操作)的同时,也可以做其他事情,这便实现了线程的异步操作。

     非阻塞式网络IO的特点:
         1)把整个过程切换成小的任务,通过任务间协作完成。
         2)由一个专门的线程来处理所有的 IO 事件,并负责分发。
         3)事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
         4)线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的进程切换。

      NIO主要原理及使用
      NIO采取通道(Channel)和缓冲区(Buffer)来传输和保存数据,它是非阻塞式的I/O,即在等待连接、读写数据(这些都是在一线程以客户端的程序中会阻塞线程的操作)的时候,程序也可以做其他事情,以实现线程的异步操作。
     考虑一个即时消息服务器,可能有上千个客户端同时连接到服务器,但是在任何时刻只有非常少量的消息需要读取和分发(如果采用线程池或者一线程一客户端方式,则会非常浪费资源),这就需要一种方法能阻塞等待,直到有一个信道可以进行I/O操作。NIO的Selector选择器就实现了这样的功能,一个 Selector实例可以同时检查一组信道的I/O状态,它就类似一个观察者,只要把需要探知的SocketChannel告诉Selector,接着做别的事情,当有事件(比如,连接打开、数据到达等)发生时,它会通知我们,传回一组SelectionKey,读取这些Key,就会获得刚刚注册过的SocketChannel,然后从这个 Channel中读取数据,接着可以处理这些数据。
      Selector内部原理实际是在做一个对所注册的Channel的轮询访问,不断的轮询,一旦轮询到一个Channel有所注册的事情发生,比如数据来了,它就会读取Channel中的数据,并对其进行处理。
要使用选择器,需要创建一个Selector实例,并将其注册到想要监控的信道上(通过Channel的方法实现)。最后调用选择器的select()方法,该方法会阻塞等待,直到有一个或多个信道准备好了I/O操作或等待超时,或另一个线程调用了该选择器的wakeup()方法。现在,在一个单独的线程中,通过调用select()方法,就能检查多个信道是否准备好进行I/O操作,由于非阻塞I/O的异步特性,在检查的同时,也可以执行其他任务。

     基于NIO的TCP连接的建立步骤
     服务端:
        1、传建一个Selector实例;
        2、将其注册到各种信道,并指定每个信道上感兴趣的I/O操作;
        3、重复执行:
             1)调用一种select()方法;
             2)获取选取的键列表;
             3)对于已选键集中的每个键:
                 a、获取信道,并从键中获取附件(如果为信道及其相关的key添加了附件的话);
                 b、确定准备就绪的操纵并执行,如果是accept操作,将接收的信道设置为非阻塞模式,并注册到选择器;
                 c、如果需要,修改键的兴趣操作集;
                 d、从已选键集中移除键

    客户端:
    与基于多线程的TCP客户端大致相同,只是这里是通过信道建立的连接,但在等待连接建立及读写时,可以异步地执行其他任务。

基于NIO的TCP通信Demo
下面给出一个基于NIO的TCP通信的Demo,客户端发送一串字符串到服务端,服务端将该字符串原原本本地反馈给客户端。
客户端代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.sql.Time;
import java.util.concurrent.TimeUnit;

public class NIOTCPClient {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		try {
			new NIOTCPClient().work("127.0.0.1", 9898, "你好");
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void work(String ip, int port, String message) throws Exception {
		// 创建一个信道,并设为非阻塞模式
		SocketChannel socketChannel = SocketChannel.open();
		socketChannel.configureBlocking(false);
		// 向服务端发起连接
		if (!socketChannel.connect(new InetSocketAddress(ip, port))) {
			// 不断地轮询连接状态,直到完成连接
			while (!socketChannel.finishConnect()) {
				// 在等待连接的时间里,可以执行其他任务,以充分发挥非阻塞IO的异步特性
				// 这里为了演示该方法的使用,只是一直打印"----"
				TimeUnit.MILLISECONDS.sleep(500);
				System.out.println("。。。。。。");
			}
		}
		// 为了与后面打印的"."区别开来,这里输出换行符
		System.out.print("\n");
		// 分别实例化用来读写的缓冲区
		byte[] msgBytes = message.getBytes();
		ByteBuffer writeBuf = ByteBuffer.wrap(msgBytes);
		ByteBuffer readBuf = ByteBuffer.allocate(msgBytes.length);

		// 接收到的总的字节数
		int totalBytesRcvd = 0;
		// 每一次调用read()方法接收到的字节数
		int bytesRcvd;

		// 循环执行,直到接收到的字节数与发送的字符串的字节数相等
		while (totalBytesRcvd < msgBytes.length) {
			// 如果用来向通道中写数据的缓冲区中还有剩余的字节,则继续将数据写入信道
			if (writeBuf.hasRemaining()) {
				socketChannel.write(writeBuf);
			}
			// 如果read()接收到-1,表明服务端关闭,抛出异常
			if ((bytesRcvd = socketChannel.read(readBuf)) == -1) {
				throw new SocketException("Connection closed prematurely");
			}
			// 计算接收到的总字节数
			totalBytesRcvd += bytesRcvd;
			// 在等待通信完成的过程中,程序可以执行其他任务,以体现非阻塞IO的异步特性
			// 这里为了演示该方法的使用,同样只是一直打印"."
			System.out.print(".");
		}
		// 打印出接收到的数据
		System.out.println("Received: "
				+ new String(readBuf.array(), 0, totalBytesRcvd));
		// 关闭信道
		socketChannel.close();
	}
}
服务端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOTCPServer {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		try {
			new NIOTCPServer().work(9898);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void work(int... ports) throws Exception {
		// 缓冲区的长度
		final int BUFSIZE = 256;
		// select方法等待信道准备好的最长时间
		final int TIMEOUT = 3000;
		// 创建一个选择器
		Selector selector = Selector.open();
		for (int port : ports) {
			// 实例化一个信道
			ServerSocketChannel listenSocketChannel = ServerSocketChannel.open();
			// 将该信道绑定到指定端口
			listenSocketChannel.socket().bind(new InetSocketAddress(port));
			// 配置信道为非阻塞模式
			listenSocketChannel.configureBlocking(false);
			// 将选择器注册到各个信道
			listenSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
		}
		// 创建一个实现了协议接口的对象
		EchoSelectorProtocol protocol = new EchoSelectorProtocol(BUFSIZE);
		// 不断轮询select方法,获取准备好的信道所关联的Key集
		while (true) {
			// 一直等待,直至有信道准备好了I/O操作
			if (selector.select(TIMEOUT) == 0) {
				// 在等待信道准备的同时,也可以异步地执行其他任务,
				// 这里只是简单地打印"."
				System.out.println("*****等待*******");
				continue;
			}
			System.out.println("--------------");
			// 获取准备好的信道所关联的Key集合的iterator实例
			Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
			// 循环取得集合中的每个键值
			while (keyIter.hasNext()) {
				SelectionKey key = keyIter.next();
				// 如果服务端信道感兴趣的I/O操作为accept
				if (key.isAcceptable()) {
					System.out.println("key.isAcceptable()");
					protocol.handleAccept(key);
				}
				// 如果客户端信道感兴趣的I/O操作为read
				if (key.isReadable()) {
					System.out.println("key.isReadable()");
					protocol.handleRead(key);
				}
				// 如果该键值有效,并且其对应的客户端信道感兴趣的I/O操作为write
				if (key.isValid() && key.isWritable()) {
					System.out.println("key.isValid() && key.isWritable()");
					protocol.handleWrite(key);
				}
				// 这里需要手动从键集中移除当前的key
				keyIter.remove();
			}
		}
	}

}

class EchoSelectorProtocol {
	private int bufSize; // 缓冲区的长度

	public EchoSelectorProtocol(int bufSize) {
		this.bufSize = bufSize;
	}

	// 服务端信道已经准备好了接收新的客户端连接
	public void handleAccept(SelectionKey key) throws IOException {
		SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept();
		socketChannel.configureBlocking(false);
		// 将选择器注册到连接到的客户端信道,并指定该信道key值的属性为OP_READ,同时为该信道指定关联的附件
		socketChannel.register(key.selector(), SelectionKey.OP_READ,
				ByteBuffer.allocate(bufSize));
		System.out.println("set  SelectionKey.OP_READ");
	}

	// 客户端信道已经准备好了从信道中读取数据到缓冲区
	public void handleRead(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();
		// 获取该信道所关联的附件,这里为缓冲区
		ByteBuffer buf = (ByteBuffer) key.attachment();
		long bytesRead = socketChannel.read(buf);
		// 如果read()方法返回-1,说明客户端关闭了连接,那么客户端已经接收到了与自己发送字节数相等的数据,可以安全地关闭
		if (bytesRead == -1) {
			socketChannel.close();
		} else if (bytesRead > 0) {
			// 如果缓冲区总读入了数据,则将该信道感兴趣的操作设置为为可读可写
			key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
			System.out.println("set  SelectionKey.OP_READ | SelectionKey.OP_WRITE");
		}
		
	}

	// 客户端信道已经准备好了将数据从缓冲区写入信道
	public void handleWrite(SelectionKey key) throws IOException {
		// 获取与该信道关联的缓冲区,里面有之前读取到的数据
		ByteBuffer buf = (ByteBuffer) key.attachment();
		// 重置缓冲区,准备将数据写入信道
		buf.flip();
		SocketChannel socketChannel = (SocketChannel) key.channel();
		// 将数据写入到信道中
		socketChannel.write(buf);
		if (!buf.hasRemaining()) {
			// 如果缓冲区中的数据已经全部写入了信道,则将该信道感兴趣的操作设置为可读
			key.interestOps(SelectionKey.OP_READ);
			System.out.println("set  SelectionKey.OP_READ");
		}
		// 为读入更多的数据腾出空间
		buf.compact();
	}

}
运行结果:
*****等待*******
--------------
key.isAcceptable()
set  SelectionKey.OP_READ
--------------
key.isReadable()
set  SelectionKey.OP_READ | SelectionKey.OP_WRITE
--------------
key.isReadable()
key.isValid() && key.isWritable()
set  SelectionKey.OP_READ
--------------
key.isReadable()
set  SelectionKey.OP_READ | SelectionKey.OP_WRITE
--------------
key.isValid() && key.isWritable()
set  SelectionKey.OP_READ
--------------
key.isReadable()
*****等待*******
*****等待*******
*****等待*******
.....................Received: 你好
      说明:以上的服务端程序,select()方法第一次能选择出来的准备好的信道都是服务端信道,其关联键值的属性都为OP_ACCEPT,其有效操作都为 accept,在执行handleAccept方法时,为取得连接的客户端信道也进行了注册,属性为OP_READ,这样下次轮询调用select()方法时,便会检查到对read操作感兴趣的客户端信道(当然也有可能有关联accept操作兴趣集的信道),从而调用handleRead方法,在该方法中又注册了OP_WRITE属性,那么第三次调用select()方法时,便会检测到对write操作感兴趣的客户端信道(当然也有可能有关联read操作兴趣集的信道),从而调用handleWrite方法。
      结果:从结果中很明显地可以看出,服务器端在等待信道准备好的时候,线程没有阻塞,而是可以执行其他任务,客户端在等待连接和等待数据读写完成的时候,线程没有阻塞,也可以执行其他任务。

     几个需要注意的地方
      1、对于非阻塞 SocketChannel来说,一旦已经调用connect()方法发起连接,底层套接字可能既不是已经连接,也不是没有连接,而是正在连接。由于底层协议的工作机制,套接字可能会在这个状态一直保持下去,这时候就需要循环地调用finishConnect()方法来检查是否完成连接,在等待连接的同时,线程也可以做其他事情,这便实现了线程的异步操作。
      2、write()方法的非阻塞调用只会写出其能够发送的数据,而不会阻塞等待所有数据,而后一起发送,因此在调用write()方法将数据写入信道时,一般要用到while循环,如:
       while(buf.hasRemaining())
            channel.write(buf);
    3、任何对key(信道)所关联的兴趣操作集的改变,都只在下次调用了select()方法后才会生效。
    4、selectedKeys()方法返回的键集是可修改的,实际上在两次调用select()方法之间,都必须手动将其清空,否则,它就会在下次调用select()方法时仍然保留在集合中,而且可能会有无用的操作来调用它,换句话说,select()方法只会在已有的所选键集上添加键,它们不会创建新的建集。
    5、对于ServerSocketChannel来说,accept是唯一的有效操作,而对于SocketChannel来说,有效操作包括读、写和连接,另外,对于DatagramChannle,只有读写操作是有效的。


NIO

   1、NIO主要包括两个部分:java.nio.channles包介绍Selector和Channel抽象,java.nio包介绍Buffer抽象。Selector和Channel抽象的关键点是:一次轮询一组客户端,查找哪个客户端需要服务;Buffer则提供了比Stream抽象更高效和可预测的I/O。Channel使用的不是流,正是Buffer缓冲区来发送或读写数据。
   2、Buffer抽象代表了一个有限容量的数据容器,其本质是一个数组,由指针指示了在哪存放数据和从哪读取数据。使用Buffer有两个主要的好处:第一,与读写缓冲区数据相关联的系统开销暴露给了程序员,可以由程序员直接控制操作;第二,一些对Java对象的特殊Buffer映射操作能够直接操作底层平台的资源。这些操作节省了在不同地址空间中复制数据的开销——这在现代计算机体系结构中是开销很大的操作。
   3、NIO的强大功能部分来自于channel的非阻塞特性。NIO的Channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道。在非阻塞式信道上调用一个方法总是会返回。例如,在一个非阻塞式ServerSocketChannel上调用accept()方法,如果有连接请求在等待,则返回客户端SocketChannel,否则,返回null;read()方法在没有数据可读时,不会阻塞等待,而是返回0。 在等待连接、读取数据等的时候,线程也可以做其他事情,这便实现了线程的异步操作
   4、Selector类的select()方法会阻塞等待,直到有信道准备好了IO操作,或等待超时,或另一个线程唤醒了它(调用了该选择器的wakeup()方法)。select()方法返回的是自上次调用它之后,有多少通道变为就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。
   5、在用Iterator迭代SelectionKey集合时,每次迭代末尾注意调用remove()方法。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除,以备下次该通道变成就绪时,Selector可以再次将其放入已选择键集中。如果不移除每个处理过的键,它就会在下次调用select()方法时仍然保留在集合中,而且可能会有无用的操作来调用它。Selector选择器实现了在单个线程中监听多个信道的功能。
   6、缓冲区是定长的,不可以扩展容量,ByteBuffer是最常用的缓冲区。缓冲区中各索引值的大小关系:0=<mark=<position=<limit=<capacity。
   7、allocateDirect()方法尝试分配直接缓存区,使用直接缓冲区,Java将从平台能够直接进行I/O操作的存储空间中为缓冲区分配后援存储空间,但不保证一定能成功,因此在尝试分配直接缓冲区后必须调用isDirect()方法进行检查,分配和销毁直接缓冲区通常要比分配和销毁非直接缓冲区消耗更多的系统资源。
   8、Buffer的clear()方法并不改变缓冲区中的数据,它只是将position设置为0,并将limit设置为等于capacity,从而使缓冲区准备好从缓冲区的put操作或信道的读操作接收新的数据。flip()方法用来将缓冲区准备为数据传出状态,这通过将limit设置为position的当前值,再将position的值设为0来实现。Rewind()方法将position设置为0,并使mark值无效,limit值不变,这样便可以重复传送缓冲区中的数据。compact()方法将position与limit之间的元素复制到缓冲区的开始位置,从而为后续的read()/put()操作让出空间,但数据复制是一个非常耗费系统资源的操作,因此要保守地使用compact()方法。如果调用slice()方法创建了一个共享了原始缓冲区子序列的新缓冲区,则在先缓冲区上调用array()方法还是返回整个缓冲数组。
   9、对于非阻塞SocketChannel来说,一旦已经调用connect()方法发起连接,底层套接字可能既不是已经连接,也不是没有连接,而是正在连接。由于底层协议的工作机制,套接字可能会在这个状态一直保持下去,这时候就需要循环地调用finishConnect()方法来检查是否完成连接,在等待连接的同时,线程也可以做其他事情,这便实现了线程的异步操作。
   10、每个选择器都有一组与之关联的信道,一个信道也可以注册多个Selector实例,因此可以有多个关联的SelectionKey实例。任何对key所关联的兴趣操作集的改变,都只在下次调用select()方法后才会生效。对于serverSocketChannel来说,accept是唯一的有效操作,而对于socketChannel来说,有效操作包括读、写和连接,对于DatagramChannle,只有读写操作是有效的。一个信道可能只与一个选择器注册一次,因此后续对register()方法的调用只是简单地更新该key所关联的兴趣操作集。
   11、 selectedKeys()方法返回的键集是可修改的,实际上在两次调用select()方法之间,都必须手动将其清空,换句话说,select()方法只会在已有的所选键集上添加键,它们不会创建新的建集。


你可能感兴趣的:(NIO)