基于事件的NIO多线程服务器

开发 后端
JDK1.4的NIO有效解决了原有流式IO存在的线程开销的问题,在NIO中使用多线程,主要目的已不是为了应对每个客户端请求而分配独立的服务线程,而是通过多线程充分使用用多个CPU的处理能力和处理中的等待时间,达到提高服务能力的目的。

JDK1.4的NIO有效解决了原有流式IO存在的线程开销的问题,在NIO中使用多线程,主要目的已不是为了应对每个客户端请求而分配独立的服务线程,而是通过多线程充分使用用多个CPU的处理能力和处理中的等待时间,达到提高服务能力的目的。

线程模型

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

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

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

(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; 

2. 事件适配器(EventAdapter):

对Serverlistener接口实现一个适配器(EventAdapter),这样的好处是最终的事件处理器可以只处理所关心的事件。

  1. public abstract class EventAdapter  
  2. implements Serverlistener  
  3. public EventAdapter() {} 
  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 {} 

3. 事件触发器(Notifier):

用于在适当的时候通过触发服务器事件,通知在册的事件处理器对事件做出响应。触发器以Singleton模式实现,统一控制整个服务器端的事件,避免造成混乱。

  1. public class Notifier  
  2. private static Arraylist listeners = null
  3. private static Notifier instance = null
  4.  
  5. private Notifier()  
  6. listeners = new Arraylist(); 
  7.  
  8. /** 
  9. * 获取事件触发器 
  10. * @return 返回事件触发器 
  11. */ 
  12. public static synchronized Notifier  
  13. getNotifier()  
  14. if (instance == null)  
  15. instance = new Notifier(); 
  16. return instance; 
  17. else  
  18. return instance; 
  19.  
  20. /** 
  21. * 添加事件监听器 
  22. * @param l 监听器 
  23. */ 
  24. public void addlistener(Serverlistener l) 
  25. synchronized (listeners) 
  26. if (!listeners.contains(l)) 
  27. listeners.add(l); 
  28.  
  29. public void fireOnAccept()  
  30. throws Exception  
  31. for (int i = listeners.size() - 1;  
  32. i >= 0; i--) 
  33. ( (Serverlistener) listeners. 
  34. get(i)).onAccept(); 
  35. // other fire method 

4. 事件处理器(Handler):

继承事件适配器,对感兴趣的事件进行响应处理,实现业务处理。以下是一个简单的事件处理器实现,它响应onRead事件,在终端打印出从客户端读取的数据。

  1. public class ServerHandler  
  2. extends EventAdapter  
  3. public ServerHandler() {} 
  4.  
  5. public void onRead(Request request)  
  6. throws Exception  
  7. System.out.println("Received: " + 
  8. new String(data)); 

5. 事件处理器的注册。

为了能让事件处理器获得服务线程的事件通知,事件处理器需在触发器中注册。

  1. ServerHandler handler = new ServerHandler(); 
  2. Notifier.addlistener(handler); 

实现NIO多线程服务器

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

1. 主控服务线程(Server):

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

  1. public class Server implements Runnable 
  2. private static List wpool = new LinkedList();  
  3. private static Selector selector; 
  4. private ServerSocketChannel sschannel; 
  5. private InetSocketAddress address; 
  6. protected Notifier notifier; 
  7. private int port; 
  8. private static int MAX_THREADS = 4
  9.  
  10. /** 
  11. * Creat the main thread 
  12. * @param port server port 
  13. * @throws java.lang.Exception 
  14. */ 
  15. public Server(int port) throws Exception 
  16. this.port = port; 
  17.  
  18. // event dispatcher 
  19. notifier = Notifier.getNotifier(); 
  20.  
  21. // create the thread pool for reading and writing 
  22. for (int i = 0; i < MAX_THREADS; i++) 
  23. Thread r = new Reader(); 
  24. Thread w = new Writer(); 
  25. r.start(); 
  26. w.start(); 
  27.  
  28. // create nonblocking socket 
  29. selector = Selector.open(); 
  30. sschannel = ServerSocketChannel.open(); 
  31. sschannel.configureBlocking(false); 
  32. address = new InetSocketAddress(port); 
  33. ServerSocket ss = sschannel.socket(); 
  34. ss.bind(address); 
  35. sschannel.register(selector, SelectionKey.OP_ACCEPT); 
  36.  
  37. public void run() 
  38. System.out.println("Server started "); 
  39. System.out.println("Server listening on port: " + port); 
  40.  
  41. while (true
  42. try 
  43. int num = 0
  44. num = selector.select(); 
  45.  
  46. if (num > 0
  47. Set selectedKeys = selector.selectedKeys(); 
  48. Iterator it = selectedKeys.iterator(); 
  49. while (it.hasNext()) 
  50. SelectionKey key = (SelectionKey) it.next(); 
  51. it.remove(); 
  52.  
  53. if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) 
  54. // Accept the new connection 
  55. ServerSocketChannel ssc =  
  56. (ServerSocketChannel) key.channel(); 
  57. notifier.fireOnAccept(); 
  58.  
  59. SocketChannel sc = ssc.accept(); 
  60. sc.configureBlocking(false); 
  61.  
  62. Request request = new Request(sc); 
  63. notifier.fireOnAccepted(request); 
  64.  
  65. sc.register(selector, SelectionKey.OP_READ,request); 
  66. }  
  67. else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) 
  68. Reader.processRequest(key);  
  69. key.cancel(); 
  70. }  
  71. else if ((key.readyOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) 
  72. Writer.processRequest(key); 
  73. key.cancel(); 
  74. }  
  75. //this selector's wakeup method is invoked 
  76. else 
  77. //register new channel for writing to selector 
  78. addRegister();  
  79. }  
  80. catch (Exception e) 
  81. notifier.fireOnError("Error occured in Server: " 
  82. + e.getMessage()); 
  83. continue
  84.  
  85. private void addRegister() 
  86. synchronized (wpool) 
  87. while (!wpool.isEmpty()) 
  88. SelectionKey key = (SelectionKey) wpool.remove(0); 
  89. SocketChannel schannel = (SocketChannel) key.channel(); 
  90. try 
  91. schannel.register(selector, SelectionKey.OP_WRITE, key 
  92. .attachment()); 
  93. catch (Exception e) 
  94. try 
  95. schannel.finishConnect(); 
  96. schannel.close(); 
  97. schannel.socket().close(); 
  98. notifier.fireOnClosed((Request) key.attachment()); 
  99. }  
  100. catch (Exception e1) 
  101. notifier.fireOnError("Error occured in addRegister: " 
  102. + e.getMessage()); 
  103.  
  104. public static void processWriteRequest(SelectionKey key) 
  105. synchronized (wpool) 
  106. wpool.add(wpool.size(), key); 
  107. wpool.notifyAll(); 
  108. selector.wakeup();  

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. key = (SelectionKey) pool.remove(0); 
  10. // 读取客户端数据,并触发onRead事件 
  11. read(key);  
  12. catch (Exception e)  
  13. continue

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. key = (SelectionKey) pool.remove(0); 
  10.  
  11. write(key);  
  12. catch (Exception e)  
  13. continue

具体应用

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. DateFormat cnDate = DateFormat.getDateTimeInstance(DateFormat.FulL, 
  11. DateFormat.FulL, Locale.CHINA); 
  12. time = cnDate.format(date); 
  13. else  
  14. DateFormat enDate = DateFormat.getDateTimeInstance(DateFormat.FulL, 
  15. DateFormat.FulL, Locale.US); 
  16. time = enDate.format(date); 
  17.  
  18. response.send(time.getBytes()); 

2. 实现日志记录服务的事件处理器(LogHandler):

  1. public class LogHandler extends EventAdapter  
  2. public LogHandler() {} 
  3.  
  4. public void onClosed(Request request)  
  5. throws Exception  
  6. String log = new Date().toString() + " from " + request.getAddress().toString(); 
  7. System.out.println(log); 
  8.  
  9. public void onError(String error)  
  10. System.out.println("Error: " + error); 

3. 启动程序:

  1. public class Start  
  2.  
  3. public static void main(String[] args)  
  4. try  
  5. LogHandler loger = new LogHandler(); 
  6. TimeHandler timer = new TimeHandler(); 
  7. Notifier notifier = Notifier.getNotifier(); 
  8. notifier.addlistener(loger); 
  9. notifier.addlistener(timer); 
  10.  
  11. System.out.println("Server starting "); 
  12. Server server = new Server(5100); 
  13. Thread tServer = new Thread(server); 
  14. tServer.start(); 
  15. catch (Exception e)  
  16. System.out.println("Server error: " + e.getMessage()); 
  17. System.exit(-1); 

小  结

通过例子我们可以看到,基于事件回调的NIO多线程服务器模型,提供了清晰直观的实现方式,可让开发者从NIO及多线程的技术细节中摆脱出来,集中精力关注具体的业务实现。

原文链接:http://www.cnblogs.com/longb/archive/2006/04/04/366800.html

【编辑推荐】

  1. Java NIO的多路复用及reactor
  2. 在Java中使用NIO进行网络编程
  3. Java NIO非阻塞服务器示例
  4. 基于Java NIO的即时聊天服务器模型
  5. Java解读NIO Socket非阻塞模式
责任编辑:林师授 来源: 泾溪的博客
相关推荐

2011-12-08 09:37:36

JavaNIO

2009-02-27 11:15:00

多线程服务器MTS专用服务器

2009-02-18 10:41:16

多线程服务器稳定Java

2010-03-17 17:54:25

java Socket

2010-03-19 14:01:55

Java Socket

2011-06-30 18:03:58

QT 多线程 服务器

2018-12-20 09:36:24

2011-12-07 17:05:45

JavaNIO

2010-03-16 10:50:21

Java多线程服务器

2011-12-15 11:03:21

JavaNIO

2011-12-08 10:12:34

JavaNIO

2015-10-27 09:40:31

TCPIP网络协议

2011-03-11 09:51:47

Java NIO

2010-03-16 13:47:48

Java多线程服务器

2018-11-28 09:53:50

游戏服务器线程

2011-07-01 10:35:20

QT 多线程 TCP

2023-05-10 10:35:14

服务器代码

2021-06-29 07:47:23

多线程协作数据

2018-01-11 08:24:45

服务器模型详解

2009-12-07 10:25:52

服务器安全服务器事件查看器
点赞
收藏

51CTO技术栈公众号