NIO 的选择器采用了多路复用(Multiplexing)技术,可在一个选择器上处理多个套接字, 通过获取读写通道来进行 IO 操作。由于网络带宽等原因,在通道的读、写操作中是容易出现等待的, 所以在读、写操作中引入多线程,对性能提高明显,而且可以提高客户端的感知服务质量。所以本文的模型将主要通过使用读、写线程池 来提高与客户端的数据交换能力。

如下图所示,服务端接受客户端请求后,控制线程将该请求的读通道交给读线程池,由读线程池分配线程完成对客户端数据的读取操作;当读线程完成读操作后,将数据返回控制线程,进行服务端的业务处理;完成 业务处理后,将需回应给客户端的数据和写通道提交给写线程池,由写线程完成向客户端发送回应数据的操作。

(NIO 多线程服务器模型)

同时整个服务端的流程处理,建立于事件机制上。在 [接受连接->读->业务处理->写 >关闭连接 ]这个 过程中,触发器将触发相应事件,由事件处理器对相应事件分别响应,完成服务器端的业务处理。 
下面我们就来详细看一下这个模型的各个组成部分。

 

相关事件定义 在这个模型中,我们定义了一些基本的事件:

(1)onAccept:当服务端收到客户端连接请求时,触发该事件。通过该事件我们可以知道有新的客户端呼入。该事件可用来控制服务端的负载。例如,服务器可设定同时只为一定数量客户端提供服务,当同时请求数超出数量时,可在响应该事件时直接抛出异常,以拒绝新的连接。

(2)onAccepted:当客户端请求被服务器接受后触发该事件。该事件表明一个新的客户端与服务器正式建立连接。

(3)onRead:当客户端发来数据,并已被服务器控制线程正确读取时,触发该事件 。该事件通知各事件处理器可以对客户端发来的数据进行实际处理了。需要注意的是,在本模型中,客户端的数据读取是由控制线程交由读线程完成的,事件处理器不需要在该事件中进行专门的读操作,而只需将控制线程传来的数据进行直接处理即可。

(4)onWrite:当客户端可以开始接受服务端发送数据时触发该事件,通过该事件,我们可以向客户端发送回应数据。 在本模型中,事件处理器只需要在该事件中设置

(5)onClosed:当客户端与服务器断开连接时触发该事件。

(6)onError:当客户端与服务器从连接开始到最后断开连接期间发生错误时触发该事件。通过该事件我们可以知道有什么错误发生。

 

事件回调机制的实现

在这个模型中,事件采用广播方式,也就是所有在册的事件处理器都能获得事件通知。这样可以将不同性质的业务处理,分别用不同的处理器实现,使每个处理器的业务功能尽可能单一。 
如下图:整个事件模型由监听器、事件适配器、事件触发器、事件处理器组成。

(事件模型)

  1. 监听器(Serverlistener):这是一个事件接口,定义需监听的服务器事件,如果您需要定义更多的事件,可在这里进行扩展。

    1. public interface Serverlistener {
    2. public void onError(String error);
    3. public void onAccept() throws Exception;
    4. public void onAccepted(Request request) throws Exception;
    5. public void onRead(Request request) throws Exception;
    6. public void onWrite(Request request, Response response) throws Exception;
    7. public void onClosed(Request request) throws Exception;
    8. }
  2. 事件适配器(EventAdapter):对 Serverlistener 接口实现一个适配器 (EventAdapter),这样的好处是最终的事件处理器可以只处理所关心的事件。 
    1. public abstract class EventAdapter implements Serverlistener {
    2. public EventAdapter() {
    3. }
    4. public void onError(String error) {}
    5. public void onAccept() throws Exception {}
    6. public void onAccepted(Request request) throws Exception {}
    7. public void onRead(Request request) throws Exception {}
    8. public void onWrite(Request request, Response response) throws Exception {}
    9. public void onClosed(Request request) throws Exception {}
    10. }
  3. 事件触发器(Notifier):用于在适当的时候通过触发服务器事件,通知在册的事件处理器对事件做出响应。触发器以 Singleton 模式实现,统一控制整个服务器端的事件,避免造成混乱。 
    1. public class Notifier {
    2. private static Arraylist listeners = null;
    3. private static Notifier instance = null;
    4. private Notifier() {
    5. listeners = new Arraylist();
    6. }
    7. /**
    8. * 获取事件触发器
    9. * @return 返回事件触发器
    10. */
    11. public static synchronized Notifier getNotifier() {
    12. if (instance == null) {
    13. instance = new Notifier();
    14. return instance;
    15. }
    16. else return instance;
    17. }
    18. /**
    19. * 添加事件监听器
    20. * @param l 监听器
    21. */
    22. public void addlistener(Serverlistener l) {
    23. synchronized (listeners) {
    24. if (!listeners.contains(l))
    25. listeners.add(l);
    26. }
    27. }
    28. public void fireOnAccept() throws Exception {
    29. for (int i = listeners.size() - 1; i >= 0; i--)
    30. ( (Serverlistener) listeners.get(i)).onAccept();
    31. }
    32. ....// other fire method
    33. }
  4. 事件处理器(Handler):继承事件适配器,对感兴趣的事件进行响应处理,实现业务处理。以下是一个简单的事件处理器实现,它响应 onRead 事件,在终端打印出从客户端读取的数据。 
    1. public class ServerHandler extends EventAdapter {
    2. public ServerHandler() {
    3. }
    4. public void onRead(Request request) throws Exception {
    5. System.out.println("Received: " + new String(data));
    6. }
    7. }
  5. 事件处理器的注册。为了能让事件处理器获得服务线程的事件通知,事件处理器需在触发器中注册。 
    1. ServerHandler handler = new ServerHandler();
    2. Notifier.addlistener(handler);
 

实现 NIO 多线程服务器

NIO 多线程服务器主要由主控服务线程、读线程和写线程组成。

(线程模型)

  1. 主控服务线程(Server):主控线程将创建读、写线程池,实现监听、接受客户端请求,同时将读、写通道提交由相应的读线程(Reader)和写服务线程 (Writer) ,由读写线程分别完成对客户端数据的读取和对客户端的回应操作。

    1. public class Server implements Runnable {
    2. ....
    3. private static int MAX_THREADS = 4;
    4. public Server(int port) throws Exception {
    5. ....
    6. // 创建无阻塞网络套接
    7. selector = Selector.open();
    8. sschannel = ServerSocketChannel.open();
    9. sschannel.configureBlocking(false);
    10. address = new InetSocketAddress(port);
    11. ServerSocket ss = sschannel.socket();
    12. ss.bind(address);
    13. sschannel.register(selector, SelectionKey.OP_ACCEPT);
    14. }
    15. public void run() {
    16. System.out.println("Server started ...");
    17. System.out.println("Server listening on port: " + port);
    18. // 监听
    19. while (true) {
    20. try {
    21. int num = 0;
    22. num = selector.select();
    23. if (num > 0) {
    24. Set selectedKeys = selector.selectedKeys();
    25. Iterator it = selectedKeys.iterator();
    26. while (it.hasNext()) {
    27. SelectionKey key = (SelectionKey) it.next();
    28. it.remove();
    29. // 处理 IO 事件
    30. if ( (key.readyOps() & SelectionKey.OP_ACCEPT) ==
    31. SelectionKey.OP_ACCEPT) {
    32. // Accept the new connection
    33. ServerSocketChannel ssc =
    34. (ServerSocketChannel) key.channel();
    35. notifier.fireOnAccept();
    36. SocketChannel sc = ssc.accept();
    37. sc.configureBlocking(false);
    38. // 触发接受连接事件
    39. Request request = new Request(sc);
    40. notifier.fireOnAccepted(request);
    41.  
    42. // 注册读操作 , 以进行下一步的读操作
    43. sc.register(selector, SelectionKey.OP_READ, request);
    44. }
    45. else if ( (key.readyOps() & SelectionKey.OP_READ) ==
    46. SelectionKey.OP_READ ) {
    47. // 提交读服务线程读取客户端数据
    48. Reader.processRequest(key);
    49. key.cancel();
    50. }
    51. else if ( (key.readyOps() & SelectionKey.OP_WRITE) ==
    52. SelectionKey.OP_WRITE ) {
    53. // 提交写服务线程向客户端发送回应数据
    54. Writer.processRequest(key);
    55. key.cancel();
    56. }
    57. }
    58. }
    59. else {
    60. addRegister(); // 在 Selector 中注册新的写通道
    61. }
    62. }
    63. catch (Exception e) {
    64. notifier.fireOnError("Error occured in Server: " + e.getMessage());
    65. continue;
    66. }
    67. }
    68. }
    69. ....
    70. }
  2. 读线程(Reader):使用线程池技术,通过多个线程读取客户端数据,以充分利用网络数据传输的时间,提高读取效率。
    1. public class Reader extends Thread {
    2. public void run() {
    3. while (true) {
    4. try {
    5. SelectionKey key;
    6. synchronized (pool) {
    7. while (pool.isEmpty()) {
    8. pool.wait();
    9. }
    10. key = (SelectionKey) pool.remove(0);
    11. }
    12. // 读取客户端数据,并触发 onRead 事件
    13. read(key);
    14. }
    15. catch (Exception e) {
    16. continue;
    17. }
    18. }
    19. }
    20. ....
    21. }
  3. 写线程(Writer):和读操作一样,使用线程池,负责将服务器端的数据发送回客户端。
    1. public final class Writer extends Thread {
    2. public void run() {
    3. while (true) {
    4. try {
    5. SelectionKey key;
    6. synchronized (pool) {
    7. while (pool.isEmpty()) {
    8. pool.wait();
    9. }
    10. key = (SelectionKey) pool.remove(0);
    11. }
    12. // 向客户端发送数据,然后关闭连接,并分别触发 onWrite,onClosed 事件
    13. write(key);
    14. }
    15. catch (Exception e) {
    16. continue;
    17. }
    18. }
    19. }
    20. ....
    21. }
 

具体应用

NIO 多线程模型的实现告一段落,现在我们可以暂且将 NIO 的各个 API 和烦琐的调用方法抛于脑后,专心于我们的实际应用中。 
我们用一个简单的 TimeServer(时间查询服务器)来看看该模型能带来多么简洁的开发方式。 
在这个 TimeServer 中,将提供两种语言(中文、英文)的时间查询服务。我们将读取客户端的查询命令(GB/EN),并回应相应语言格式的当前时间。在应答客户的请求的同时,服务器将进行日志记录。做为示例,对日志记录,我们只是简单地将客户端的访问时间和 IP 地址输出到服务器的终端上。

  1. 实现时间查询服务的事件处理器(TimeHandler):

    1. public class TimeHandler extends EventAdapter {
    2. public TimeHandler() {
    3. }
    4. public void onWrite(Request request, Response response) throws Exception {
    5. String command = new String(request.getDataInput());
    6. String time = null;
    7. Date date = new Date();
    8. // 判断查询命令
    9. if (command.equals("GB")) {
    10. // 中文格式
    11. DateFormat cnDate = DateFormat.getDateTimeInstance(DateFormat.FulL,
    12. DateFormat.FulL, Locale.CHINA);
    13. time = cnDate.format(date);
    14. }
    15. else {
    16. // 英文格式
    17. DateFormat enDate = DateFormat.getDateTimeInstance(DateFormat.FulL,
    18. DateFormat.FulL, Locale.US);
    19. time = enDate.format(date);
    20. }
    21. response.send(time.getBytes());
    22. }
    23. }
  2. 实现日志记录服务的事件处理器(LogHandler):
    1. public class LogHandler extends EventAdapter {
    2. public LogHandler() {
    3. }
    4. public void onClosed(Request request) throws Exception {
    5. String log = new Date().toString() + " from " + request.getAddress()
    6. .toString();
    7. System.out.println(log);
    8. }
    9. public void onError(String error) {
    10. System.out.println("Error: " + error);
    11. }
    12. }
  3. 启动程序:
    1. public class Start {
    2. public static void main(String[] args) {
    3. try {
    4. LogHandler loger = new LogHandler();
    5. TimeHandler timer = new TimeHandler();
    6. Notifier notifier = Notifier.getNotifier();
    7. notifier.addlistener(loger);
    8. notifier.addlistener(timer);
    9. System.out.println("Server starting ...");
    10. Server server = new Server(5100);
    11. Thread tServer = new Thread(server);
    12. tServer.start();
    13. }
    14. catch (Exception e) {
    15. System.out.println("Server error: " + e.getMessage());
    16. System.exit(-1);
    17. }
    18. }
    19. }
  1. 代码:JAVA_NIO_结合多线程.zip

JAVA NIO 结合多线程的更多相关文章

  1. Java多线程:Linux多路复用,Java NIO与Netty简述

    JVM的多路复用器实现原理 Linux 2.5以前:select/poll Linux 2.6以后: epoll Windows: IOCP Free BSD, OS X: kqueue 下面仅讲解L ...

  2. Java NIO学习与记录(八): Reactor两种多线程模型的实现

    Reactor两种多线程模型的实现 注:本篇文章例子基于上一篇进行:Java NIO学习与记录(七): Reactor单线程模型的实现 紧接着上篇Reactor单线程模型的例子来,假设Handler的 ...

  3. Java Nio 多线程网络下载

    --> 默认最多50个线程 同一文件下载失败延迟超过30秒就结束下载 --> 下载5分钟超时时间,假设5分钟内未下载完就结束下载 --> 依赖 commons-httpclient ...

  4. JAVA NIO Socket通道

      DatagramChannel和SocketChannel都实现定义读写功能,ServerSocketChannel不实现,只负责监听传入的连接,并建立新的SocketChannel,本身不传输数 ...

  5. Java NIO (转)

    Java NIO提供了与标准IO不同的IO工作方式: Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(B ...

  6. Java - NIO

    java.nio:NIO-2: NIO 面向流的IO体系一次只能处理一个或多个字节/字符,直至读取所有字节/符,且流中的数据不能前后移动.效率低,当数据源中没有数据时会阻塞线程.Java-4提供的新A ...

  7. 【转】java NIO 相关知识

    原文地址:http://www.iteye.com/magazines/132-Java-NIO Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的 ...

  8. Java NIO:浅析I/O模型

    也许很多朋友在学习NIO的时候都会感觉有点吃力,对里面的很多概念都感觉不是那么明朗.在进入Java NIO编程之前,我们今天先来讨论一些比较基础的知识:I/O模型.下面本文先从同步和异步的概念 说起, ...

  9. Java NIO:NIO概述

    Java NIO:NIO概述 在上一篇博文中讲述了几种IO模型,现在我们开始进入Java NIO编程主题.NIO是Java 4里面提供的新的API,目的是用来解决传统IO的问题.本文下面分别从Java ...

随机推荐

  1. MINA学习之体系介绍

    基于MINA应用程序结构图: 我们可以看出,MINA是应用程序(客户端或服务端)和底层基于TCP,UDP等通讯协议的网络层之间的粘合剂.而且各个模块之间是相互独立的,你只需要在MINA体 系基础上设计 ...

  2. 一个免费的自动化跨平台测试JavaScript的工具——BrowserSwarm

    BrowserSwarm是一个免费工具,能够自动化跨平台测试JavaScript.

  3. IntelliJ IDEA 配置Jetty

    jetty是google app engine 在大量使用的一款服务器软件,不过当然目前还撼动不了tomcat的地位,但是jetty相当轻量级,可以自己灵活定制 资源占用少 ,所以还是有吸引力的,接下 ...

  4. 有用的javascript外部文件或其他外部文件引用

    1.<link href='http://fonts.googleapis.com/css?family=PT+Sans+Narrow' rel='stylesheet' type='text/ ...

  5. 開始折腾cocos2d-x,使用批处理来创建项目

    開始抽出时间学习cocos2d-x了.尽管C和C++还都不咋地.只是在开发中学习记忆也许会更深吧. so决定从今天開始正式学习的用自己的空暇时间折腾它了.正好这个五一没什么事.昨天搭建了一下开发环境. ...

  6. delegate和event

    经过查阅资料和自己的理解整理出来的,欢迎大家指教. delegate和event 何时使用: 异步的时候,比如加载完成通知. 需要回调的时候,比如按钮点击.动画播放结束等. 发送事件通知的时候. 比如 ...

  7. css标准导航代码

    <!-- 例子解析: --> --> <!-- list-style-type:none - 移除列表前小标志.一个导航栏并不需要列表标记 --> <!-- 移除浏 ...

  8. H TML5 之 (2) 小试牛刀

    基本的HTML都认识到了,就开始运用下了,大的程序的开始,都是从一个一个表达式慢慢的堆积起来的 想开始玩HTML5,就开始理解,它所提供的画布函数各有什么作用,API的具体使用,才能完成自己想要完成的 ...

  9. WPF Radio组的绑定

    都是控件编,RadioButtion 简单绑定使用,model.cs下边定义属性 private int _isSuccess; public int IsSuccess { get { return ...

  10. (转)VS自带工具:dumpbin的使用

    有时候我们想查看一个exe引用了哪些动态库,或者我们想看某个动态库包含哪些接口函数,这个时候可以使用dumpbin.exe工具: 1.输入Dumpbin -imports calldll.exe查看它 ...