Java NIO 异步网络构建高性能服务器

Java NIO 异步网络构建高性能服务器
2010年12月15日
  转自:http://jimmee.javaeye.com/blog/673212
  又看到关于nio的文章,转了转了。
  1.问题
  构建高性能的服务器时,肯定要求性能越高越好,这是不言自明的道理.那么一般服务器处理客户端请求,都有哪些方式呢?
  (1) 最初级的处理方式:用一个ServerSocket进行无限循环来监听,客户端来一个连接就处理一个,后面来的连接则只能等待前面的处理好了才能进行处理,可想而知,如果每个连接处理的处理时间很长,这样的处理在一个客户要求快的响应的情况下会多么糟糕.大家都经常上网,你愿意多等那么10几秒么? 这种情况的代码形式如下:
  Java代码
  
  
  ServerSockt serverSocket = .... [b]while[/b] ([b]true[/b]) { Socket socket = serverSocket.accept(); 处理socket... } ServerSockt serverSocket = .... while (true) { Socket socket = serverSocket.accept(); // 处理socket... }
  所以说,很多讲网络编程的书,动不动举的例子就是上面,我真的怀疑这些人做过实际开发没有.在实际应用过程中,很少会使用这种编程开发方式的.
  (2) 稍微进步一些的:一客户端一个线程.这中处理的方式是,当来一个客户端时,就新创建一个线程进行处理.代码的方式如下:
  Java代码
  
  
  ServerSockt serverSocket = .... [b]while[/b] ([b]true[/b]) { Socket socket = serverSocket.accept(); ([b]new[/b] Thead() { [b]public[/b] [b]void[/b] run() { // 处理socket } }) .start(); } ServerSockt serverSocket = .... while (true) { Socket socket = serverSocket.accept(); (new Thead() { public void run() { // 处理socket } }) .start(); }
  // 注意; 我上面敲的代码仅仅是表达意思,如何编写正确的方式,我想就不用多说了把.
  可能刚接触这种处理方式的人会觉得,这样真好阿!每个客户端连接都用一个线程来处理,应该性能很高,响应性很好吧.如果连接数非常少的话,上面这种处理方法的确没多大问题.但是,但凡学过操作系统的人都知道,现代操作系统进程是资源分配的基本单位,而进程是调度的基本单位.要知道,调度是需要花费CPU时间的,如果线程数过多,CPU的时间都花费在调度上了,真正用于处理连接的时间则很少,当然,这仅仅是一个方面,线程的创建的会花费时间和一定的内存,这有可能会造成内存耗尽的情况并会影响响应性,而且线程的频繁切换会不能有效的利用高速缓存等等情况.因此,实际工作过程中,很少用这种“一客户端一线程”的方式.
  (3)使用线程池。上面已经一客户端一线程的情况下,线程过多时会造成的影响.自然而然的,大家就想到了使用线程池.所谓的线程池处理方法,就是预先创建好一组线程,每个线程都在监听着客户端连接,哪个先监听到连接了,就进行处理.一般代码形式如下:
  Java代码
  
  
  ServerSocket serverSocket = …. [b]for[/b] ( [b]int[/b] i = 0; i 处理clientSocket } } }; t.start(); } ServerSocket serverSocket = …. for ( int i = 0; i 处理clientSocket } } }; t.start(); }
  使用线程池,线程的是先预先创建的,与客户端数量无关,因此,可以控制线程的调度和相关的资源开销.Doug Lea教授为java贡献的java.util.concurrent包里提供了各种优秀的线程池的实现方式.可以直接使用,不是高手就不要乱重新发明轮子了,线程不是一般人玩的.
  你怎么知道NIO的优点呢? 1. 为什么要使用NIO?
  举几种情况说明:
  (1)如果通信的协议采用对应的是长连接的实现,分析前面三种方式的缺点:
  一个主线程的顺序处理每个连接:如果采用这种方式,后面的连接全都需要漫长的等待,这显然不现实;
  每个连接对应一个线程以及使用线程池,显然这两种只是比上面稍微好点,如果连接数不能过多,否则也无济于事;
  (2)通信非常大量的短连接,同样分析前面三种方式的缺点:
  一个主线程顺序处理的最大弊病是后面的都需要等待;
  每个连接对应一个线程以及使用线程池都存在同样的问题,因为每个连接数虽然多,但是处理的时间会很短,这样主要的花费在线程的创建和上下文切换上了,所以说,增加多余的线程不起作用,线程数过小,由于通信的连接很多,又会导致处理的过慢,你可能会想,使用线程池不正是解决这样的问题么?(注意,对于连接短但是数量巨大的情况,线程池大小不是
  这么设置的,因为创建过多的线程,但是这些线程大部分时间是空闲的,你可能会说现在java新的线程池的处理不是很好么,可以根据处理的任务自动增加和减少线程的数量,注意,只要你是使用线程,就有上面说的那些使用线程带来的弊端,线程池真正的用处在你有多个可预计的并行的任务需要执行时。)
  (3)在使用线程时,我们不能精确控制控制线程,比如,你想对哪个线程执行写操作,哪个线程执行读操作,这都很难精确的控制。
  (4) 如果客户端要共享服务器端的一些资源时,同步问题比较难处理。
  这就是为什么要使用NIO的原因,NIO只使用一个单线程,一次轮询一组客户端(这组客户端的数量可以很大,大于1k),找出需要服务的客户端,比如,哪些客户端可以写操作,哪些客户端可以进行读操作,可以将客户端变成可读,或者变成可写等,既然是采用一个单线程,自然而然多个客户端共享服务器端资源的问题也解决了。NIO的核心就是Selector和Channel,这里可以简单的认为Channel就是对应一个Socket连接,同时,NIO一般都是将Channel配置成非阻塞的方式工作的,这也是NIO强大的地方之一,在NIO里数据处理的媒介就是Buffer,各种各样的Buffer,单常用的是ByteBuffer.
  反应器(Reactor):用于事件多路分离和分派的体系结构模式
  通常的,对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞与非阻塞。所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止。而对于非阻塞状态, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待。 在前面的章节中提到的Tcp通信的例子中,就是采用的阻塞式的工作方式:当接收tcp数据时,如果远端没有数据可以读,则会一直阻塞到读到需要的数据为止。这种方式的传输和传统的被动方法的调用类似,非常直观,并且简单有效,但是同样也存在一个效率问题,如果你是开发一个面对着数千个连接的服务器程序,对每一个客户端都采用阻塞的方式通信,如果存在某个非常耗时的读写操作时,其它的客户端通信将无法响应,效率非常低下。 一种常用做法是:每建立一个Socket连接时,同时创建一个新线程对该Socket进行单独通信(采用阻塞的方式通信)。这种方式具有很高的响应速度,并且控制起来也很简单,在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程的无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况。 另一种较高效的做法是:服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时(读就绪),则调用该socket连接的相应读操作;如果发现某个Socket端口上有数据可写时(写就绪),则调用该socket连接的相应写操作;如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高。 系统I/O方式可分为阻塞,非阻塞同步和非阻塞异步三类,三种方式中,非阻塞异步模式的扩展性和性能最好。主要是讲了两种IO多路复用模式:Reactor和Proactor,并对它们进行了比较。 两种I/O多路复用模式:Reactor和Proactor 一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。 在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。 而在Proactor模式中,处理器--或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。 举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。 在Reactor中实现读: - 注册读就绪事件和相应的事件处理器 - 事件分离器等待事件 - 事件到来,激活分离器,分离器调用事件对应的处理器。 - 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。 与如下Proactor(真异步)中的读过程比较: - 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。 - 事件分离器等待操作完成事件 - 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。 - 事件分离器呼唤处理器。 - 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。 Java NIO非堵塞应用通常适用用在I/O读写等方面,我们知道,系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作上,过去,在打开一个I/O通道后,read()将一直等待在端口一边读取字节内容,如果没有内容进来,read()也是傻傻的等,这会影响我们程序继续做其他事情,那么改进做法就是开设线程,让线程去等待,但是这样做也是相当耗费资源的。 Java NIO非堵塞技术实际是采取Reactor模式,或者说是Observer模式为我们监察I/O端口,如果有内容进来,会自动通知我们,这样,我们就不必开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。 Java NIO出现不只是一个技术性能的提高,你会发现网络上到处在介绍它,因为它具有里程碑意义,从JDK1.4开始,Java开始提高性能相关的功能,从而使得Java在底层或者并行分布式计算等操作上已经可以和C或Perl等语言并驾齐驱。 NIO主要原理和适用。 NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,放心,包准能够读到,接着我们可以处理这些数据。 Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。 模式参见 http://www.jdon.com/concurrent/reactor.htm 通常的,对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞与非阻塞。所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止。而对于非阻塞状态, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待。 在前面的章节中提到的Tcp通信的例子中,就是采用的阻塞式的工作方式:当接收tcp数据时,如果远端没有数据可以读,则会一直阻塞到读到需要的数据为止。这种方式的传输和传统的被动方法的调用类似,非常直观,并且简单有效,但是同样也存在一个效率问题,如果你是开发一个面对着数千个连接的服务器程序,对每一个客户端都采用阻塞的方式通信,如果存在某个非常耗时的读写操作时,其它的客户端通信将无法响应,效率非常低下。 一种常用做法是:每建立一个Socket连接时,同时创建一个新线程对该Socket进行单独通信(采用阻塞的方式通信)。这种方式具有很高的响应速度,并且控制起来也很简单,在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程的无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况。 另一种较高效的做法是:服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时(读就绪),则调用该socket连接的相应读操作;如果发现某个Socket端口上有数据可写时(写就绪),则调用该socket连接的相应写操作;如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高。 系统I/O方式可分为阻塞,非阻塞同步和非阻塞异步三类,三种方式中,非阻塞异步模式的扩展性和性能最好。主要是讲了两种IO多路复用模式:Reactor和Proactor,并对它们进行了比较。两种I/O多路复用模式:Reactor和Proactor 一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。 在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。 而在Proactor模式中,处理器--或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。 举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。 在Reactor中实现读: - 注册读就绪事件和相应的事件处理器 - 事件分离器等待事件 - 事件到来,激活分离器,分离器调用事件对应的处理器。 - 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。 与如下Proactor(真异步)中的读过程比较: - 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。 - 事件分离器等待操作完成事件 - 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。 - 事件分离器呼唤处理器。 - 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。 Java NIO非堵塞应用通常适用用在I/O读写等方面,我们知道,系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作上,过去,在打开一个I/O通道后,read()将一直等待在端口一边读取字节内容,如果没有内容进来,read()也是傻傻的等,这会影响我们程序继续做其他事情,那么改进做法就是开设线程,让线程去等待,但是这样做也是相当耗费资源的。Java NIO非堵塞技术实际是采取Reactor模式,或者说是Observer模式为我们监察I/O端口,如果有内容进来,会自动通知我们,这样,我们就不必开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。Java NIO出现不只是一个技术性能的提高,你会发现网络上到处在介绍它,因为它具有里程碑意义,从JDK1.4开始,Java开始提高性能相关的功能,从而使得Java在底层或者并行分布式计算等操作上已经可以和C或Perl等语言并驾齐驱。NIO主要原理和适用。NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,放心,包准能够读到,接着我们可以处理这些数据。Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。模式参见 http://www.jdon.com/concurrent/reactor.htm
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值