NIO

来源:互联网 发布:php 大数据搜索引擎 编辑:程序博客网 时间:2024/05/08 15:52

 

通常的,对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞非阻塞。所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止。而对于非阻塞状态, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待

在前面的章节中提到的Tcp通信的例子中,就是采用的阻塞式的工作方式:当接收tcp数据时,如果远端没有数据可以读,则会一直阻塞到读到需要的数据为止。这种方式的传输和传统的被动方法的调用类似,非常直观,并且简单有效,但是同样也存在一个效率问题,如果你是开发一个面对着数千个连接的服务器程序,对每一个客户端都采用阻塞的方式通信,如果存在某个非常耗时的读写操作时,其它的客户端通信将无法响应,效率非常低下。


自由地使用线程是处理阻塞问题最典型的办法。一种常用做法是:每建立一个Socket连接时,同时创建一个新线程对该Socket进行单独通信(采用阻塞的方式通信)。这种方式具有很高的响应速度,并且控制起来也很简单,在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程的无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况。

 

 

example:
server code:

public class MultiUserServer extends Thread {
 private Socket client;
 
 public MultiUserServer(Socket c) {
  this.client = c;
 }

 public void run() {
  try {
   BufferedReader in = new BufferedReader(new InputStreamReader(client
     .getInputStream()));
   PrintWriter out = new PrintWriter(client.getOutputStream());
   // Mutil User but can't parallel
   while (true) {
    String str = in.readLine();
    System.out.println(str);
    SocketLog.debug("receive message: " + str);
    out.println("has receive....");
    out.flush();
    if (str.equals("end"))
     break;
   }
   client.close();
  } catch (IOException ex) {
  }
 }

 public static void main(String[] args) throws IOException {
  int port = 5678;
  if (args.length > 0)
   port = Integer.parseInt(args[0]);
  ServerSocket server = new ServerSocket(port);
  SocketLog.debug("the server socket application is created!");
  while (true) {
   // transfer location change Single User or Multi User
   MultiUserServer mu = new MultiUserServer(server.accept());
   mu.start();
  }
 }
}

client code:

public class Client {

 static Socket server;

 public static void main(String[] args) throws Exception {
  
  //set socket proxy.
  String proxyHost = "192.161.88.22";
  String proxyPort = "2080";
  System.getProperties().put("socksProxySet","true");
  System.getProperties().put("socksProxyHost",proxyHost);
  System.getProperties().put("socksProxyPort",proxyPort);
  
  String host = "192.20.9.18";
  int port = 1086;
  if (args.length > 1)
  {
   host = args[0];
   port = Integer.parseInt(args[1]);
  }
  System.out.println("connetioning:" + host + ":" + port);
  server = new Socket(host, port);
  BufferedReader in = new BufferedReader(new InputStreamReader(server
    .getInputStream()));
  PrintWriter out = new PrintWriter(server.getOutputStream());
  BufferedReader wt = new BufferedReader(new InputStreamReader(System.in));
  while (true) {
   String str = wt.readLine();
   out.println(str);
   out.flush();
   if (str.equals("end")) {
    break;
   }
   System.out.println(in.readLine());
  }
  server.close();
 }
}

 

 

另一种较高效的做法是:nio:非阻塞通讯模式.

 

服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时(读就绪),则调用该socket连接的相应读操作;如果发现某个Socket端口上有数据可写时(写就绪),则调用该socket连接的相应写操作;如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高。

 

在Socket编程中就可以通过select等相关API实现这一方式。


NIO 设计背后的基石:反应器模式,用于事件多路分离和分派的体系结构模式。 分布式系统中的服务器应用程序必须处理多个向它们发送服务请求的客户机。然而,在调用特定的服务之前,服务器应用程序必须将每个传入请求多路分用并分派到各自相应的服务提供者。反应器模式正好适用于这一功能。它允许事件驱动应用程序将服务请求多路分用并进行分派,然后,这些服务请求被并发地从一个或多个客户机传送到应用程序。
反应器模式的核心功能如下:
将事件多路分用
将事件分派到各自相应的事件处理程序

 

反应器本质上提供一组更高级的编程抽象,简化了事件驱动的分布式应用的设计和实现其基本概念是反应器框架检测事件的发生(通过在OS事件多路分离接口上进行侦听),并发出对预登记事件处理器(eventhandler)对象中的方法的"回调"(callback)。该方法由应用开发者实现,其中含有应用处理此事件的特定代码。 

 

 

反应器模式与观察者模式(Observer pattern)在这个方面极为相似:当一个主体发生改变时,所有依属体都得到通知。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联。

 

 

NIO 的非阻塞 I/O 机制是围绕 选择器和 通道构建的。 
通道(Channel 类):表示服务器和客户机之间的一种通信机制。
选择器(Selector类):是 Channel 的多路复用器。Selector 类将传入的客户机请求多路分用并将它们
分派到各自的请求处理程序。

 

 

通道表示连到一个实体(例如:硬件设备、文件、网络套接字或者能执行一个或多个不同 I/O操作(例如:读或写)的程序组件)的开放连接。可以异步地关闭和中断 NIO 通道。所以,如果一个线程在某条通道的 I/O操作上阻塞时,那么另一个线程可以将这条通道关闭。类似地,如果一个线程在某条通道的 I/O 操作上阻塞时,那么另一个线程可以中断这个阻塞线程。

 


如图 2 所示,在 java.nio.channels 包中有不少通道接口。我们主要关心 java.nio.channels.SocketChannel 接口和 java.nio.channels.ServerSocketChannel 接口。 这两个接口可用来分别代替 java.net.Socketjava.net.ServerSocket 。尽管我们当然将把注意力放在以非阻塞方式使用通道上,但通道可以以阻塞方式或非阻塞方式使用。

 

在反应器模式情形中, Selector 类充当 Reactor 角色。 Selector 对多个 SelectableChannels 的事件进行多路复用。每个 ChannelSelector 注册事件。当事件从客户机处到来时, Selector 将它们多路分用并将这些事件分派到相应的 Channel

 

 

 

简单的来说:

NIO是一个基于事件的IO架构,最基本的思想就是:有事件我通知你,你再去做你的事情.
而且NIO的主线程只有一个,不像传统的模型,需要多个线程以应对客户端请求,也减轻
了JVM的工作量。
当Channel注册至Selector以后,经典的调用方法如下:

        while (somecondition) {
            int n = selector.select(TIMEOUT);
            if (n == 0)
                continue;
            for (Iterator iter = selector.selectedKeys().iterator(); iter
                    .hasNext();) {
                if (key.isAcceptable())
                    doAcceptable(key);
                if (key.isConnectable())
                    doConnectable(key);
                if (key.isValid() && key.isReadable())
                    doReadable(key);
                if (key.isValid() && key.isWritable())
                    doWritable(key);
                iter.remove();
            }
        }
nio中取得事件通知,就是在selector的select事件中完成的。在selector事件时有一个线程
向操作系统询问,selector中注册的Channel&&SelectionKey的键值对的各种事件是否有发生,
如果有则添加到selector的selectedKeys属性Set中去,并返回本次有多少个感兴趣的事情发生。
如果发现这个值>0,表示有事件发生,马上迭代selectedKeys中的SelectionKey,
根据Key中的表示的事件,来做相应的处理。
实际上,这段说明表明了异步socket的核心,即异步socket不过是将多个socket的调度(或者还有他们的线程调度)
全部交给操作系统自己去完成,异步的核心Selector,不过是将这些调度收集、分发而已。