`
flychao88
  • 浏览: 753122 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

跟我学系列之NIO的那些坑

 
阅读更多

  1. public
     class EchoServer {  
  2.     public static int DEFAULT_PORT = 7777;  
  3.   
  4.     public static void main(String[] args) throws IOException {  
  5.         System.out.println("Listening for connection on port " + DEFAULT_PORT);  
  6.   
  7.         Selector selector = Selector.open();  
  8.         initServer(selector);  
  9.   
  10.         while (true) {  
  11.             selector.select();  
  12.   
  13.             for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {  
  14.                 SelectionKey key = (SelectionKey) itor.next();  
  15.                 itor.remove();  
  16.                 try {  
  17.                     if (key.isAcceptable()) {  
  18.                         ServerSocketChannel server = (ServerSocketChannel) key.channel();  
  19.                         SocketChannel client = server.accept();  
  20.                         System.out.println("Accepted connection from " + client);  
  21.                         client.configureBlocking(false);  
  22.                         SelectionKey clientKey = client.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);  
  23.                         ByteBuffer buffer = ByteBuffer.allocate(100);  
  24.                         clientKey.attach(buffer);  
  25.                     }  
  26.                     if (key.isReadable()) {  
  27.                         SocketChannel client = (SocketChannel) key.channel();  
  28.                         ByteBuffer buffer = (ByteBuffer) key.attachment();  
  29.                         client.read(buffer);  
  30.                     }  
  31.                     if (key.isWritable()) {  
  32.                         // System.out.println("is writable...");  
  33.                         SocketChannel client = (SocketChannel) key.channel();  
  34.                         ByteBuffer buffer = (ByteBuffer) key.attachment();  
  35.                         buffer.flip();  
  36.                         client.write(buffer);  
  37.                         buffer.compact();  
  38.                     }  
  39.                 } catch (IOException e) {  
  40.                     key.cancel();  
  41.                     try { key.channel().close(); } catch (IOException ioe) { }  
  42.                 }  
  43.             }  
  44.         }  
  45.     }  
  46.   
  47.     private static void initServer(Selector selector) throws IOException,  
  48.             ClosedChannelException {  
  49.         ServerSocketChannel serverChannel = ServerSocketChannel.open();  
  50.         ServerSocket ss = serverChannel.socket();  
  51.         ss.bind(new InetSocketAddress(DEFAULT_PORT));  
  52.         serverChannel.configureBlocking(false);  
  53.         serverChannel.register(selector, SelectionKey.OP_ACCEPT);  
  54.     }  
  55. }  



上面的代码很典型,运行结果似乎也是正确的。 

Java代码  收藏代码
  1. marlon$ java EchoServer&  
  2. --> Listening for connection on port 7777  
  3. marlon$ telnet localhost 7777  
  4. --> Accepted connection from java.nio.channels.SocketChannel[connected local=/127.0.0.1:7777 remote=/127.0.0.1:65030]  
  5. hello  
  6. --> hello  
  7. world  
  8. -->world  


但是如果你这时top用看一下发现服务器进程CPU占用到95%以上,如果取消掉32行的注释,服务器会不断地输出"is writable...",这是为什么呢?让我们来分析当第一个客户端连接上时发生什么情况。 

  1. 在连接之前,服务器第11行:selector.select()处阻塞。当阻塞时,内核会将这个进程调度至休眠状态,此时基本不耗CPU。
  2. 当客户端发起一个连接时,服务器检测到客户端连接,selector.select()返回。selector.selectedKeys()返回已就绪的SelectionKey的集合,在这种情况下,它只包含一个key,也就是53行注册的acceptable key。服务器开始运行17-25行的代码,server.accept()返回代码客户端连接的socket,第22行在socket上注册OP_READ和OP_WRITE,表示当socket可读或者可写时就会通知selector。
  3. 接着服务器又回到第11行,尽管这时客户端还没有任何输入,但这时selector.select()不会阻塞,因为22行在socket注册了写操作,而socket只要send buffer不满就可以写,刚开始send buffer为空,socket总是可以写,于是server.select()立即返回,包含在22行注册的key。由于这个key可写,所以服务器会运行31-38行的代码,但是这时buffer为空,client.write(buffer)没有向socket写任何东西,立即返回0。
  4. 接着服务器又回到第11行,由于客户端连接socket可以写,这时selector.select()会立即返回,然后运行31-38行的代码,像步骤3一样,由于buffer为空,服务器没有干任何事又返回到第11行,这样不断循环,服务器却实际没有干事情,却耗大量的CPU。



从上面的分析可以看出问题在于我们在没有数据可写时就在socket上注册了OP_WRITE,导致服务器浪费大量CPU资源,解决办法是只有数据可以写时才注册OP_WRITE操作。上面的版本还不只浪费CPU那么简单,它还可能导致潜在的死锁。虽然死锁在我的机器上没有发生,对于这个简单的例子似乎也不大可能发生在别的机器上,但是在对于复杂的情况,比如我写的端口转发工具中就发生了,这还依赖于jdk的实现。对于上面的EchoServer,出现死锁的场景是这样的: 

  1. 假设服务器已经启动,并且已经有一个客户端与它相连,此时正如上面的分析,服务器在不断地循环做无用功。这时用户在客户端输入"hello"。
  2. 当服务器运行到第11行:selector.select()时,这时selector.selectedKeys()会返回一个代表客户端连接的key,显然这时客户端socket是既可读又可写,但jdk却并不保证能够检测到两种状态。如果它检测到key既可读又可写,那么服务器会执行26-38行的代码。如果只检测到可读,那么服务器会执行26-30行的代码。如果只检测到可写,那么会执行31-38行的代码。对于前两种情况,不会造成死锁,因为当执行完29行,buffer会读到用户输入的内容,下次再运行到36行就可以将用户输入内容echo回。但是对最后一种情况,服务器完全忽略了客户端发过来的内容,如果每次selector.select()都只能检测到socket可写,那么服务器永远不能将echo回客户端输入的内容。



避免死锁的一个简单方法就是不要在同一个socket同时注册多个操作。对于上面的EchoServer来说就是不要同时注册OP_READ和OP_WRITE,要么只注册OP_READ,要么只注册OP_WRITE。下面的EchoServer修正了以上的错误: 

Java代码  收藏代码
  1. public static void main(String[] args) throws IOException {  
  2.     System.out.println("Listening for connection on port " + DEFAULT_PORT);  
  3.   
  4.     Selector selector = Selector.open();  
  5.     initServer(selector);  
  6.   
  7.     while (true) {  
  8.         selector.select();  
  9.   
  10.         for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {  
  11.             SelectionKey key = (SelectionKey) itor.next();  
  12.             itor.remove();  
  13.             try {  
  14.                 if (key.isAcceptable()) {  
  15.                     ServerSocketChannel server = (ServerSocketChannel) key.channel();  
  16.                     SocketChannel client = server.accept();  
  17.                     System.out.println("Accepted connection from " + client);  
  18.                     client.configureBlocking(false);  
  19.                     SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ);  
  20.                     ByteBuffer buffer = ByteBuffer.allocate(100);  
  21.                     clientKey.attach(buffer);  
  22.                 } else if (key.isReadable()) {  
  23.                     SocketChannel client = (SocketChannel) key.channel();  
  24.                     ByteBuffer buffer = (ByteBuffer) key.attachment();  
  25.                     int n = client.read(buffer);  
  26.                     if (n > 0) {  
  27.                         buffer.flip();  
  28.                         key.interestOps(SelectionKey.OP_WRITE);     // switch to OP_WRITE  
  29.                     }  
  30.                 } else if (key.isWritable()) {  
  31.                     System.out.println("is writable...");  
  32.                     SocketChannel client = (SocketChannel) key.channel();  
  33.                     ByteBuffer buffer = (ByteBuffer) key.attachment();  
  34.                     client.write(buffer);  
  35.                     if (buffer.remaining() == 0) {  // write finished, switch to OP_READ  
  36.                         buffer.clear();  
  37.                         key.interestOps(SelectionKey.OP_READ);  
  38.                     }  
  39.                 }  
  40.             } catch (IOException e) {  
  41.                 key.cancel();  
  42.                 try { key.channel().close(); } catch (IOException ioe) { }  
  43.             }  
  44.         }  
  45.     }  
  46. }  



主要变化,在第19行接受客户端连接时只注册OP_READ操作,第28行当读到数据时才切换到OP_WRITE操作,第35-38行,当写操作完成时再切换到OP_READ操作。由于一个key同时只能执行一个操作,我将原来三个并行if换成了if...else。 

上面的代码不够优雅,它将处理服务器Socket和客户连接Socket的代码搅在一起,对于简单的EchoServer这样做没什么问题,当服务器变得复杂,使用命令模式将它们分开变显得非常必要。首先创建一个接口来抽象对SelectionKey的处理。 

Java代码  收藏代码
  1. interface Handler {  
  2.     void execute(Selector selector, SelectionKey key);  
  3. }  


再来看main函数: 

Java代码  收藏代码
  1. public static void main(String[] args) throws IOException {  
  2.     System.out.println("Listening for connection on port " + DEFAULT_PORT);  
  3.   
  4.     Selector selector = Selector.open();  
  5.     initServer(selector);  
  6.   
  7.     while (true) {  
  8.         selector.select();  
  9.   
  10.         for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {  
  11.             SelectionKey key = (SelectionKey) itor.next();  
  12.             itor.remove();  
  13.             Handler handler = (Handler) key.attachment();  
  14.             handler.execute(selector, key);  
  15.         }  
  16.     }  
  17. }  
  18.   
  19. private static void initServer(Selector selector) throws IOException,  
  20.         ClosedChannelException {  
  21.     ServerSocketChannel serverChannel = ServerSocketChannel.open();  
  22.     ServerSocket ss = serverChannel.socket();  
  23.     ss.bind(new InetSocketAddress(DEFAULT_PORT));  
  24.     serverChannel.configureBlocking(false);  
  25.     SelectionKey serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);  
  26.     serverKey.attach(new ServerHandler());  
  27. }  


main函数非常简单,迭代SelectionKey,对每个key的attachment为Handler,调用它的execute的方法,不用管它是服务器Socket还是客户Socket。注意initServer方法将serverKey附加了一个ServerHandler。下面是ServerHandler的代码: 

Java代码  收藏代码
  1. class ServerHandler implements Handler {  
  2.     public void execute(Selector selector, SelectionKey key) {  
  3.         ServerSocketChannel server = (ServerSocketChannel) key.channel();  
  4.         SocketChannel client = null;  
  5.         try {  
  6.             client = server.accept();  
  7.             System.out.println("Accepted connection from " + client);  
  8.         } catch (IOException e) {  
  9.             e.printStackTrace();  
  10.             return;  
  11.         }  
  12.           
  13.         SelectionKey clientKey = null;  
  14.         try {  
  15.             client.configureBlocking(false);  
  16.             clientKey = client.register(selector, SelectionKey.OP_READ);  
  17.             clientKey.attach(new ClientHandler());  
  18.         } catch (IOException e) {  
  19.             if (clientKey != null)  
  20.                 clientKey.cancel();  
  21.             try { client.close(); } catch (IOException ioe) { }  
  22.         }  
  23.     }  
  24. }  


ServerHandler接收连接,为每个客户Socket注册OP_READ操作,返回的clientKey附加上ClientHandler。 

Java代码  收藏代码
  1. class ClientHandler implements Handler {  
  2.     private ByteBuffer buffer;  
  3.       
  4.     public ClientHandler() {  
  5.         buffer = ByteBuffer.allocate(100);  
  6.     }  
  7.       
  8.     public void execute(Selector selector, SelectionKey key) {  
  9.         try {  
  10.             if (key.isReadable()) {  
  11.                 readKey(selector, key);  
  12.             } else if (key.isWritable()) {  
  13.                 writeKey(selector, key);  
  14.             }  
  15.         } catch (IOException e) {  
  16.             key.cancel();  
  17.             try { key.channel().close(); } catch (IOException ioe) { }  
  18.         }  
  19.     }  
  20.       
  21.     private void readKey(Selector selector, SelectionKey key) throws IOException {  
  22.         SocketChannel client = (SocketChannel) key.channel();  
  23.         int n = client.read(buffer);  
  24.         if (n > 0) {  
  25.             buffer.flip();  
  26.             key.interestOps(SelectionKey.OP_WRITE);     // switch to OP_WRITE  
  27.         }  
  28.     }  
  29.       
  30.     private void writeKey(Selector selector, SelectionKey key) throws IOException {  
  31.         // System.out.println("is writable...");  
  32.         SocketChannel client = (SocketChannel) key.channel();  
  33.         client.write(buffer);  
  34.         if (buffer.remaining() == 0) {  // write finished, switch to OP_READ  
  35.             buffer.clear();  
  36.             key.interestOps(SelectionKey.OP_READ);  
  37.         }  
  38.     }  
  39. }  


这个代码没有什么新内容,只是将根据key是可读还可写拆分为两个方法,代码结构显得更清晰。对于EchoServer,这么做确实有些过度工程,对于稍微复杂一点的服务器这么做是很值得的。 

分享到:
评论

相关推荐

    黄金档 » Java NIO 那些躲在角落的细节

    黄金档 » Java NIO 那些躲在角落的细节黄金档 » Java NIO 那些躲在角落的细节

    JavaNIO chm帮助文档

    Java NIO系列教程(一) Java NIO 概述 Java NIO系列教程(二) Channel Java NIO系列教程(三) Buffer Java NIO系列教程(四) Scatter/Gather Java NIO系列教程(五) 通道之间的数据传输 Java NIO系列教程(六)...

    JAVA NIO 学习资料

    Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java从JDK 1.4版本开始引入的一种新的IO模型,它为Java应用程序提供了更高效的数据传输方式,尤其适用于高并发、大数据量的网络服务。与传统的IO...

    Java NIO系列教程(一) Java NIO 概述

    ### Java NIO 系列教程(一):Java NIO 概述 #### 一、引言 Java NIO(New IO)是Java SE 1.4版本引入的一个新的I/O处理框架,它提供了比传统Java IO包更高效的数据处理方式。NIO的核心在于其三大组件:Channels...

    大数据学习之旅——NIO源码

    在大数据学习之旅中,理解NIO的这些核心概念是基础。首先,我们需要了解`java.nio`包中的各类通道类,如FileChannel、SocketChannel和ServerSocketChannel等,它们分别对应于文件操作、网络连接的客户端和服务端。...

    NIO学习系列:连网和异步IO

    在IT行业中,网络编程是构建分布式系统和网络应用的基础,而Java NIO(Non-blocking Input/Output)则是Java提供的一种高效、低延迟的I/O模型。本篇文章将深入探讨NIO在连网和异步IO方面的应用,以及如何通过源码...

    JAVA NIO学习网站

    Java NIO(New IO)是Java 1.4版本引入的一个新API,全称为New Input/Output,是对传统IO(Old IO)的扩展。...对于从事Java后端开发或者网络编程的工程师来说,深入理解并熟练运用Java NIO是必备的技能之一。

    基于Groovy的NIO框架,仅供学习Java NIO使用。.zip

    总的来说,"基于Groovy的NIO框架"提供了一种学习和实践Java NIO技术的新途径,尤其是对于那些熟悉Groovy的开发者来说,他们可以利用Groovy的便利性来优化和简化NIO应用的开发。通过深入理解并应用上述知识点,可以...

    java NIO学习系列 笔记

    Java NIO(New Input/Output)是Java标准库在JDK 1.4版本中引入的一个新特性,它提供了一种不同于传统IO流的高效I/O处理方式。NIO的核心概念包括通道(Channel)和缓冲区(Buffer),这两个组件使得数据以块的形式...

    apache nio 很好的学习资料

    Apache NIO,全称为Non-blocking Input/Output,是Java提供的一种高效、灵活的I/O模型,与传统的BIO( Blocking I/O)模型相比,它在处理高并发、大数据量的网络应用时具有显著优势。本学习资料将深入探讨Apache NIO...

    NIO系列DATASHEET.pdf

    ### NIO系列DATASHEET.pdf知识点总结 #### 一、NIO系列产品概述 **NIO系列DATASHEET.pdf**提供了NIO系列产品的详细介绍和技术规格,该文档覆盖了多个型号的产品,包括不同类型的输入输出模块、电源设备以及网络...

    NIO学习系列:核心概念及基本读写

    在Java世界中,NIO(New Input/Output)是一个重要的编程接口,它为开发者提供了非阻塞I/O操作的能力,极大地提高了程序的性能。NIO并非完全替代传统的IO(-blocking I/O),而是作为其补充,提供了另一种处理I/O...

    NIO思维导图学习资料

    nio思维导图:适用于有一定编程基础的朋友,想系统学习NIO这块知识的朋友。知识点大体分3块:1:&gt;概念了解(各类IO) 2&gt;NIO的核心(缓存区,通道等) 3&gt;网络IO

    Java NIO学习资料+代码.zip

    Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java在JDK 1.4版本引入的一种新的I/O模型,它为Java开发者提供了更高效、更灵活的I/O操作方式。相比传统的IO模型,NIO具有多路复用、非阻塞、缓冲...

    java NIO 学习 聊天室程序 (3)

    Java NIO(New IO)是Java 1.4版本引入的一个新特性,是对传统IO模型的重大改进。在传统的IO模型中,数据传输基于字节流或字符流,使用阻塞IO,即当读写操作进行时,如果数据没有准备好,线程会被阻塞,直到数据准备...

    NIO netty开发

    netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty开发之nio netty...

    NIO学习总结

    《NIO学习总结》 NIO(Non-blocking I/O,非阻塞I/O)是Java在JDK 1.4引入的一种新的I/O模型,它为Java提供了更高效的数据处理方式,尤其适用于高并发、大数据量的网络应用。相较于传统的BIO(Blocking I/O)模型,...

Global site tag (gtag.js) - Google Analytics