原文链接:JAVA NIO non-blocking模式实现高并发服务器

Java自1.4以后,加入了新IO特性,NIO. 号称new IO. NIO带来了non-blocking特性. 这篇文章主要讲的是如何使用NIO的网络新特性,来构建高性能非阻塞并发服务器.

文章基于个人理解,我也来搞搞NIO.,求指正.

在NIO之前

服务器还是在使用阻塞式的java socket. 以Tomcat最新版本没有开启NIO模式的源码为例, tomcat会accept出来一个socket连接,然后调用processSocket方法来处理socket.

  1. while(true) {
  2. ....
  3. Socket socket = null;
  4. try {
  5. // Accept the next incoming connection from the server
  6. // socket
  7. socket = serverSocketFactory.acceptSocket(serverSocket);
  8. }
  9. ...
  10. ...
  11. // Configure the socket
  12. if (running && !paused && setSocketOptions(socket)) {
  13. // Hand this socket off to an appropriate processor
  14. if (!processSocket(socket)) {
  15. countDownConnection();
  16. // Close socket right away(socket);
  17. closeSocket(socket);
  18. }
  19. }
  20. ....
  21. }

使用ServerSocket.accept()方法来创建一个连接. accept方法是阻塞方法,在下一个connection进来之前,accept会阻塞.

在一个socket进来之后,Tomcat会在thread pool里面拿出一个thread来处理连接的socket. 然后自己快速的脱身去接受下一个socket连接. 代码如下:

  1. protected boolean processSocket(Socket socket) {
  2. // Process the request from this socket
  3. try {
  4. SocketWrapper<Socket> wrapper = new SocketWrapper<Socket>(socket);
  5. wrapper.setKeepAliveLeft(getMaxKeepAliveRequests());
  6. // During shutdown, executor may be null - avoid NPE
  7. if (!running) {
  8. return false;
  9. }
  10. getExecutor().execute(new SocketProcessor(wrapper));
  11. } catch (RejectedExecutionException x) {
  12. log.warn("Socket processing request was rejected for:"+socket,x);
  13. return false;
  14. } catch (Throwable t) {
  15. ExceptionUtils.handleThrowable(t);
  16. // This means we got an OOM or similar creating a thread, or that
  17. // the pool and its queue are full
  18. log.error(sm.getString("endpoint.process.fail"), t);
  19. return false;
  20. }
  21. return true;
  22. }

而每个处理socket的线程,也总是会阻塞在while(true) sockek.getInputStream().read() 方法上.

总结就是, 一个socket必须使用一个线程来处理. 致使服务器需要维护比较多的线程. 线程本身就是一个消耗资源的东西,并且每个处理socket的线程都会阻塞在read方法上,使得系统大量资源被浪费.

以上这种socket的服务方式适用于HTTP服务器,每个http请求都是短期的,无状态的,并且http后台的业务逻辑也一般比较复杂. 使用多线程和阻塞方式是合适的.

倘若是做游戏服务器,尤其是CS架构的游戏.这种传统模式服务器毫无胜算.游戏有以下几个特点是传统服务器不能胜任的:
1, 持久TCP连接. 每一个client和server之间都存在一个持久的连接.当CCU(并发用户数量)上升,阻塞式服务器无法为每一个连接运行一个线程.
2, 自己开发的二进制流传输协议. 游戏服务器讲究响应快.那网络传输也要节省时间. HTTP协议的冗余内容太多,一个好的游戏服务器传输协议,可以使得message压缩到3-6倍甚至以上.这就使得游戏服务器要开发自己的协议解析器.
3, 传输双向,且消息传输频率高.假设一个游戏服务器instance连接了2000个client,每个client平均每秒钟传输1-10个message,一个message大约几百字节或者几千字节.而server也需要向client广播其他玩家的当前信息.这使得服务器需要有高速处理消息的能力.
4, CS架构的游戏服务器端的逻辑并不像APP服务器端的逻辑那么复杂. 网络游戏在client端处理了大部分逻辑,server端负责简单逻辑,甚至只是传递消息.

在Java NIO出现以后

出现了使用NIO写的非阻塞网络引擎,比如Apache Mina, JBoss Netty, Smartfoxserver BitSwarm. 比较起来, Mina的性能不如后两者.Tomcat也存在NIO模式,不过需要人工开启.

首先要说明一下, 与App Server的servlet开发模式不一样, 在Mina, Netty和BitSwarm上开发应用程序都是Event Driven的设计模式.Server端会收到Client端的event,Client也会收到Server端的event,Server端与Client端的都要注册各种event的EventHandler来handle event.

用大白话来解释NIO:
1, Buffers, 网络传输字节存放的地方.无论是从channel中取,还是向channel中写,都必须以Buffers作为中间存贮格式.
2, Socket Channels. Channel是网络连接和buffer之间的数据通道.每个连接一个channel.就像之前的socket的stream一样.
3, Selector. 像一个巡警,在一个片区里面不停的巡逻. 一旦发现事件发生,立刻将事件select出来.不过这些事件必须是提前注册在selector上的. select出来的事件打包成SelectionKey.里面包含了事件的发生事件,地点,人物. 如果警察不巡逻,每个街道(socket)分配一个警察(thread),那么一个片区有几条街道,就需要几个警察.但现在警察巡逻了,一个巡警(selector)可以管理所有的片区里面的街道(socketchannel).

以上把警察比作线程,街道比作socket或socketchannel,街道上发生的一切比作stream.把巡警比作selector,引起巡警注意的事件比作selectionKey.

从上可以看出,使用NIO可以使用一个线程,就能维护多个持久TCP连接.

NIO实例

下面给出NIO编写的EchoServer和Client. Client连接server以后,将发送一条消息给server. Server会原封不懂的把消息发送回来.Client再把消息发送回去.Server再发回来.用不休止. 在性能的允许下,Client可以启动任意多.

以下Code涵盖了NIO里面最常用的方法和连接断开诊断.注释也全.

首先是Server的实现. Server端启动了2个线程,connectionBell线程用于巡逻新的连接事件. readBell线程用于读取所有channel的数据. 注解: Mina采取了同样的做法,只是readBell线程启动的个数等于处理器个数+1. 由此可见,NIO只需要少量的几个线程就可以维持非常多的并发持久连接.

每当事件发生,会调用dispatch方法去处理event. 一般情况,会使用一个ThreadPool来处理event. ThreadPool的大小可以自定义.但不是越大越好.如果处理event的逻辑比较复杂,比如需要额外网络连接或者复杂数据库查询,那ThreadPool就需要稍微大些.(猜测)Smartfoxserver处理上万的并发,也只用到了3-4个线程来dispatch event.

EchoServer

  1. public class EchoServer {
  2. public static SelectorLoop connectionBell;
  3. public static SelectorLoop readBell;
  4. public boolean isReadBellRunning=false;
  5.  
  6. public static void main(String[] args) throws IOException {
  7. new EchoServer().startServer();
  8. }
  9.  
  10. // 启动服务器
  11. public void startServer() throws IOException {
  12. // 准备好一个闹钟.当有链接进来的时候响.
  13. connectionBell = new SelectorLoop();
  14.  
  15. // 准备好一个闹装,当有read事件进来的时候响.
  16. readBell = new SelectorLoop();
  17.  
  18. // 开启一个server channel来监听
  19. ServerSocketChannel ssc = ServerSocketChannel.open();
  20. // 开启非阻塞模式
  21. ssc.configureBlocking(false);
  22.  
  23. ServerSocket socket = ssc.socket();
  24. socket.bind(new InetSocketAddress("localhost",7878));
  25.  
  26. // 给闹钟规定好要监听报告的事件,这个闹钟只监听新连接事件.
  27. ssc.register(connectionBell.getSelector(), SelectionKey.OP_ACCEPT);
  28. new Thread(connectionBell).start();
  29. }
  30.  
  31. // Selector轮询线程类
  32. public class SelectorLoop implements Runnable {
  33. private Selector selector;
  34. private ByteBuffer temp = ByteBuffer.allocate(1024);
  35.  
  36. public SelectorLoop() throws IOException {
  37. this.selector = Selector.open();
  38. }
  39.  
  40. public Selector getSelector() {
  41. return this.selector;
  42. }
  43.  
  44. @Override
  45. public void run() {
  46. while(true) {
  47. try {
  48. // 阻塞,只有当至少一个注册的事件发生的时候才会继续.
  49. this.selector.select();
  50.  
  51. Set<SelectionKey> selectKeys = this.selector.selectedKeys();
  52. Iterator<SelectionKey> it = selectKeys.iterator();
  53. while (it.hasNext()) {
  54. SelectionKey key = it.next();
  55. it.remove();
  56. // 处理事件. 可以用多线程来处理.
  57. this.dispatch(key);
  58. }
  59. } catch (IOException e) {
  60. e.printStackTrace();
  61. } catch (InterruptedException e) {
  62. e.printStackTrace();
  63. }
  64. }
  65. }
  66.  
  67. public void dispatch(SelectionKey key) throws IOException, InterruptedException {
  68. if (key.isAcceptable()) {
  69. // 这是一个connection accept事件, 并且这个事件是注册在serversocketchannel上的.
  70. ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
  71. // 接受一个连接.
  72. SocketChannel sc = ssc.accept();
  73.  
  74. // 对新的连接的channel注册read事件. 使用readBell闹钟.
  75. sc.configureBlocking(false);
  76. sc.register(readBell.getSelector(), SelectionKey.OP_READ);
  77.  
  78. // 如果读取线程还没有启动,那就启动一个读取线程.
  79. synchronized(EchoServer.this) {
  80. if (!EchoServer.this.isReadBellRunning) {
  81. EchoServer.this.isReadBellRunning = true;
  82. new Thread(readBell).start();
  83. }
  84. }
  85.  
  86. } else if (key.isReadable()) {
  87. // 这是一个read事件,并且这个事件是注册在socketchannel上的.
  88. SocketChannel sc = (SocketChannel) key.channel();
  89. // 写数据到buffer
  90. int count = sc.read(temp);
  91. if (count < 0) {
  92. // 客户端已经断开连接.
  93. key.cancel();
  94. sc.close();
  95. return;
  96. }
  97. // 切换buffer到读状态,内部指针归位.
  98. temp.flip();
  99. String msg = Charset.forName("UTF-8").decode(temp).toString();
  100. System.out.println("Server received ["+msg+"] from client address:" + sc.getRemoteAddress());
  101.  
  102. Thread.sleep(1000);
  103. // echo back.
  104. sc.write(ByteBuffer.wrap(msg.getBytes(Charset.forName("UTF-8"))));
  105.  
  106. // 清空buffer
  107. temp.clear();
  108. }
  109. }
  110.  
  111. }
  112.  
  113. }

接下来就是Client的实现.Client可以用传统IO,也可以使用NIO.这个例子使用的NIO,单线程.

  1. public class Client implements Runnable {
  2. // 空闲计数器,如果空闲超过10次,将检测server是否中断连接.
  3. private static int idleCounter = 0;
  4. private Selector selector;
  5. private SocketChannel socketChannel;
  6. private ByteBuffer temp = ByteBuffer.allocate(1024);
  7.  
  8. public static void main(String[] args) throws IOException {
  9. Client client= new Client();
  10. new Thread(client).start();
  11. //client.sendFirstMsg();
  12. }
  13.  
  14. public Client() throws IOException {
  15. // 同样的,注册闹钟.
  16. this.selector = Selector.open();
  17.  
  18. // 连接远程server
  19. socketChannel = SocketChannel.open();
  20. // 如果快速的建立了连接,返回true.如果没有建立,则返回false,并在连接后出发Connect事件.
  21. Boolean isConnected = socketChannel.connect(new InetSocketAddress("localhost", 7878));
  22. socketChannel.configureBlocking(false);
  23. SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);
  24.  
  25. if (isConnected) {
  26. this.sendFirstMsg();
  27. } else {
  28. // 如果连接还在尝试中,则注册connect事件的监听. connect成功以后会出发connect事件.
  29. key.interestOps(SelectionKey.OP_CONNECT);
  30. }
  31. }
  32.  
  33. public void sendFirstMsg() throws IOException {
  34. String msg = "Hello NIO.";
  35. socketChannel.write(ByteBuffer.wrap(msg.getBytes(Charset.forName("UTF-8"))));
  36. }
  37.  
  38. @Override
  39. public void run() {
  40. while (true) {
  41. try {
  42. // 阻塞,等待事件发生,或者1秒超时. num为发生事件的数量.
  43. int num = this.selector.select(1000);
  44. if (num ==0) {
  45. idleCounter ++;
  46. if(idleCounter >10) {
  47. // 如果server断开了连接,发送消息将失败.
  48. try {
  49. this.sendFirstMsg();
  50. } catch(ClosedChannelException e) {
  51. e.printStackTrace();
  52. this.socketChannel.close();
  53. return;
  54. }
  55. }
  56. continue;
  57. } else {
  58. idleCounter = 0;
  59. }
  60. Set<SelectionKey> keys = this.selector.selectedKeys();
  61. Iterator<SelectionKey> it = keys.iterator();
  62. while (it.hasNext()) {
  63. SelectionKey key = it.next();
  64. it.remove();
  65. if (key.isConnectable()) {
  66. // socket connected
  67. SocketChannel sc = (SocketChannel)key.channel();
  68. if (sc.isConnectionPending()) {
  69. sc.finishConnect();
  70. }
  71. // send first message;
  72. this.sendFirstMsg();
  73. }
  74. if (key.isReadable()) {
  75. // msg received.
  76. SocketChannel sc = (SocketChannel)key.channel();
  77. this.temp = ByteBuffer.allocate(1024);
  78. int count = sc.read(temp);
  79. if (count<0) {
  80. sc.close();
  81. continue;
  82. }
  83. // 切换buffer到读状态,内部指针归位.
  84. temp.flip();
  85. String msg = Charset.forName("UTF-8").decode(temp).toString();
  86. System.out.println("Client received ["+msg+"] from server address:" + sc.getRemoteAddress());
  87.  
  88. Thread.sleep(1000);
  89. // echo back.
  90. sc.write(ByteBuffer.wrap(msg.getBytes(Charset.forName("UTF-8"))));
  91.  
  92. // 清空buffer
  93. temp.clear();
  94. }
  95. }
  96. } catch (IOException e) {
  97. e.printStackTrace();
  98. } catch (InterruptedException e) {
  99. e.printStackTrace();
  100. }
  101. }
  102. }
  103.  
  104. }

下载以后粘贴到eclipse中, 先运行EchoServer,然后可以运行任意多的Client. 停止Server和client的方式就是直接terminate server.

JAVA NIO non-blocking模式实现高并发服务器(转)的更多相关文章

  1. JAVA NIO non-blocking模式实现高并发服务器

    JAVA NIO non-blocking模式实现高并发服务器 分类: JAVA NIO2014-04-14 11:12 1912人阅读 评论(0) 收藏 举报 目录(?)[+] Java自1.4以后 ...

  2. JAVA NIO使用非阻塞模式实现高并发服务器

    参考:http://blog.csdn.net/zmx729618/article/details/51860699  https://zhuanlan.zhihu.com/p/23488863 ht ...

  3. 多线程模式下高并发的环境中唯一确保单例模式---DLC双端锁

    DLC双端锁,CAS,ABA问题 一.什么是DLC双端锁?有什么用处? 为了解决在多线程模式下,高并发的环境中,唯一确保单例模式只能生成一个实例 多线程环境中,单例模式会因为指令重排和线程竞争的原因会 ...

  4. 深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析

    前面在学习JVM的知识的时候,一般都需要利用相关参数进行分析,而分析一般都需要用到一些分析的工具,因为一般使用IDEA,而VisualVM对于IDEA也不错,所以就选择VisualVM来分析JVM性能 ...

  5. Linux + C + Epoll实现高并发服务器(线程池 + 数据库连接池)(转)

    转自:http://blog.csdn.net/wuyuxing24/article/details/48758927 一, 背景 先说下我要实现的功能,server端一直在linux平台下面跑,当客 ...

  6. 第15章 高并发服务器编程(1)_非阻塞I/O模型

    1. 高性能I/O (1)通常,recv函数没有数据可用时会阻塞等待.同样,当socket发送缓冲区没有足够多空间来发送消息时,函数send会阻塞. (2)当socket在非阻塞模式下,这些函数不会阻 ...

  7. 为一个支持GPRS的硬件设备搭建一台高并发服务器用什么开发比较容易?

    高并发服务器开发,硬件socket发送数据至服务器,服务器对数据进行判断,需要实现心跳以保持长连接. 同时还要接收另外一台服务器的消支付成功消息,接收到消息后控制硬件执行操作. 查了一些资料,java ...

  8. linux学习之高并发服务器篇(二)

    高并发服务器 1.线程池并发服务器 两种模型: 预先创建阻塞于accept多线程,使用互斥锁上锁保护accept(减少了每次创建线程的开销) 预先创建多线程,由主线程调用accept 线程池 3.多路 ...

  9. PHP写的异步高并发服务器,基于libevent

    PHP写的异步高并发服务器,基于libevent 博客分类: PHP PHPFPSocketLinuxQQ  本文章于2013年11月修改. swoole已使用C重写作为PHP扩展来运行.项目地址:h ...

随机推荐

  1. shared_ptr(作为局部变量返回)

    智能指针:shared_ptr 1.一个局部的shared_ptr 作为返回值过程:当shared_ptr 被创建的时候,自身的引用计数 +1,当前引用计数为 1 , 按值返回以后 引用计数 + 1 ...

  2. NLP自然语言处理系列5-支持向量机(SVM)

    1.什么是支持向量机 支持向量机(Support Vector Machine,SVM)是一种经典的分类模型,在早期的文档分类等领域有一定的应用.了解SVM的推导过程是一个充满乐趣和挑战的过程,耐心的 ...

  3. UML类图之间的关系

    1. 泛化(Generalization) [泛化关系]:是一种继承关系,表示一般与特殊的关系,它指定了子类如何特化父类的所有特征和行为.例如:老虎是动物的一种,即有老虎的特性也有动物的共性. [箭头 ...

  4. 使用 jquery 开发用户通讯录

    由于开发需求,需要做一个通讯录界面,点击右侧首字母菜单,列表会将对应字母列表成员滑动至顶部,效果如下图(包括点击事件+长按事件): 1.需求分析 (1)首先,我们需要把数据里用户名转换为首拼,然后归类 ...

  5. linux环境下source vimrc提示错误unexpected token `"autocmd"'

    编辑完vimrc之后,使用source /etc/vimrc之后报错: $ source /etc/vimrc bash: /etc/vimrc: line 15: syntax error near ...

  6. BZOJ.4180.字符串计数(后缀自动机 二分 矩阵快速幂/倍增Floyd)

    题目链接 先考虑 假设S确定,使构造S操作次数最小的方案应是:对T建SAM,S在SAM上匹配,如果有S的转移就转移,否则操作数++,回到根节点继续匹配S.即每次操作一定是一次极大匹配. 简单证明:假设 ...

  7. BZOJ4268 : 小强的书架

    首先将所有高度乘上10,设f[i]为将前i本书放入书架的最小高度,则 \[\begin{eqnarray*}f[i]&=&\min(f[j-1]+first(j,i)+second(j ...

  8. Codeforces 666E Forensic Examination SAM+权值线段树

    第一次做这种$SAM$带权值线段树合并的题 然而$zjq$神犇看完题一顿狂码就做出来了 $Orz$ 首先把所有串当成一个串建$SAM$ 我们对$SAM$上每个点 建一棵权值线段树 每个叶子节点表示一个 ...

  9. iOS9UICollectionView自定义布局modifying attributes returned by UICollectionViewFlowLayout without copying them

    UICollectionViewFlowLayout has cached frame mismatch This is likely occurring because the flow layou ...

  10. Java怎样处理EXCEL的读取

    须要包:poi-3.5.jar.poi-ooxml-3.5.jar 实例: [java] view plaincopy public class ProcessExcel { private Work ...