服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 基础入门 > 查看文档

java i/o api之性能分析 (上)

  摘要:

  io api的可伸缩性对web应用有着极其重要的意义。java 1.4版以前的api中,阻塞i/o令许多人失望。从j2se 1.4版本开始,java终于有了可伸缩的i/o api。本文分析并计算了新旧i/o api在可伸缩性方面的差异。

  一、概述

  io api的可伸缩性对web应用有着极其重要的意义。java 1.4版以前的api中,阻塞i/o令许多人失望。从j2se 1.4版本开始,java终于有了可伸缩的i/o api。本文分析并计算了新旧io api在可伸缩性方面的差异。java向socket写入数据时必须调用关联的outputstream的write()方法。只有当所有的数据全部写入时,write()方法调用才会返回。倘若发送缓冲区已满且连接速度很低,这个调用可能需要一段时间才能完成。如果程序只使用单一的线程,其他连接就必须等待,即使那些连接已经做好了调用write()的准备也一样。为了解决这个问题,你必须把每一个socket和一个线程关联起来;采用这种方法之后,当一个线程由于i/o相关的任务被阻塞时,另一个线程仍旧能够运行。

  尽管线程的开销不如进程那么大,但是,考虑到底层的操作平台,线程和进程都属于消耗大量资源的程序结构。每一个线程都要占用一定数量的内存,而且除此之外,多个线程还意味着线程上下文的切换,而这种切换也需要昂贵的资源开销。因此,java需要一个新的api来分离socket与线程之间过于紧密的联系。在新的java i/o api(java.nio.*)中,这个目标终于实现了。

  本文分析和比较了用新、旧两种i/o api编写的简单web服务器。由于作为web协议的http不再象原来那样只用于一些简单的目的,因此这里介绍的例子只包含关键的功能,或者说,它们既不考虑安全因素,也不严格遵从协议规范。

  二、用旧api编写的http服务器

  首先我们来看看用旧式api编写的http服务器。这个实现只使用了一个类。main()方法首先创建了一个绑定到8080端口的serversocket:


  public static void main() throws ioexception {
  serversocket serversocket = new serversocket(8080);
  for (int i=0; i < integer.parseint(args[0]); i++) {
  new httpd(serversocket);
  }
  }

  接下来,main()方法创建了一系列的httpd对象,并用共享的serversocket初始化它们。在httpd的构造函数中,我们保证每一个实例都有一个有意义的名字,设置默认协议,然后通过调用其超类thread的start()方法启动服务器。此举导致对run()方法的一次异步调用,而run()方法包含一个无限循环。

  在run()方法的无限循环中,serversocket的阻塞性accpet()方法被调用。当客户程序连接服务器的8080端口,accept()方法将返回一个socket对象。每一个socket关联着一个inputstream和一个outputstream,两者都要在后继的handlerequest()方法调用中用到。这个方法将读取客户程序的请求,经过检查和处理,然后把合适的应答发送给客户程序。如果客户程序的请求合法,通过sendfile()方法返回客户程序请求的文件;否则,客户程序将收到相应的错误信息(调用senderror())方法。


  while (true) {
  ...
  socket = serversocket.accept();
  ...
  handlerequest();
  ...
  socket.close();
  }

  现在我们来分析一下这个实现。它能够出色地完成任务吗?答案基本上是肯定的。当然,请求分析过程还可以进一步优化,因为在性能方面stringtokenizer的声誉一直不佳。但这个程序至少已经关闭了tcp延迟(对于短暂的连接来说它很不合适),同时为外发的文件设置了缓冲。而且更重要的是,所有的线程操作都相互独立。新的连接请求由哪一个线程处理由本机的(因而也是速度较快的)accept()方法决定。除了serversocket对象之外,各个线程之间不共享可能需要同步的任何其他资源。这个方案速度较快,但令人遗憾的是,它不具有很好的可伸缩性,其原因就在于,很显然地,线程是一种有限的资源。

  三、非阻塞的http服务器

  下面我们来看看另一个使用非阻塞的新i/o api的方案。新的方案要比原来的方案稍微复杂一点,而且它需要各个线程的协作。它包含下面四个类:

  ?niohttpd
  ?acceptor
  ?connection
  ?connectionselector




  niohttpd的主要任务是启动服务器。就象前面的httpd一样,一个服务器socket被绑定到8080端口。两者主要的区别在于,新版本的服务器使用java.nio.channels.serversocketchannel而不是serversocket。在利用bind()方法显式地把socket绑定到端口之前,必须先打开一个管道(channel)。然后,main()方法实例化了一个connectionselector和一个acceptor。这样,每一个connectionselector都可以用一个acceptor注册;另外,实例化acceptor时还提供了serversocketchannel。


  public static void main() throws ioexception {
  serversocketchannel ssc = serversocketchannel.open();
  ssc.socket().bind(new inetsocketaddress(8080));
  connectionselector cs = new connectionselector();
  new acceptor(ssc, cs);
  }

  为了理解这两个线程之间的交互过程,首先我们来仔细地分析一下acceptor。acceptor的主要任务是接受传入的连接请求,并通过connectionselector注册它们。acceptor的构造函数调用了超类的start()方法;run()方法包含了必需的无限循环。在这个循环中,一个阻塞性的accept()方法被调用,它最终将返回一个socket对象――这个过程几乎与httpd的处理过程一样,但这里使用的是serversocketchannel的accept()方法,而不是serversocket的accept()方法。最后,以调用accept()方法获得的socketchannel对象为参数创建一个connection对象,并通过connectionselector的queue()方法注册它。


  while (true) {
  ...
  socketchannel = serversocketchannel.accept();
  connectionselector.queue(new connection(socketchannel));
  ...
  }

  总而言之:acceptor只能在一个无限循环中接受连接请求和通过connectionselector注册连接。与acceptor一样,connectionselector也是一个线程。在构造函数中,它构造了一个队列,并用selector.open()方法打开了一个java.nio.channels.selector。selector是整个服务器中最重要的部分之一,它使得程序能够注册连接,能够获取已经允许读取和写入操作的连接的清单。

  构造函数调用start()方法之后,run()方法里面的无限循环开始执行。在这个循环中,程序调用了selector的select()方法。这个方法一直阻塞,直到已经注册的连接之一做好了i/o操作的准备,或selector的wakeup()方法被调用。


  while (true) {
  ...
  int i = selector.select();
  registerqueuedconnections();
  ...
  // 处理连接...
  }

  当connectionselector线程执行select()时,没有一个acceptor线程能够用该selector注册连接,因为对应的方法是同步方法,理解这一点是很重要的。因此这里使用了队列,必要时acceptor线程向队列加入连接。


  public void queue(connection connection) {
  synchronized (queue) {
  queue.add(connection);
  }
  selector.wakeup();
  }

  紧接着把连接放入队列的操作,acceptor调用selector的wakeup()方法。这个调用导致connectionselector线程继续执行,从正在被阻塞的select()调用返回。由于selector不再被阻塞,connectionselector现在能够从队列注册连接。在registerqueuedconnections()方法中,其实施过程如下:


  if (!queue.isempty()) {
  synchronized (queue) {
  while (!queue.isempty()) {
  connection connection =
  (connection)queue.remove(queue.size()-1);
  connection.register(selector);
  }
  }
  }




扫描关注微信公众号