NIO的一些坑

来源:互联网 发布:河北省网络行政学院 编辑:程序博客网 时间:2024/06/04 19:30

转载自:http://blog.csdn.net/u013970991/article/details/52036223

public class EchoServer {      public static int DEFAULT_PORT = 7777;      public static void main(String[] args) throws IOException {          System.out.println("Listening for connection on port " + DEFAULT_PORT);          Selector selector = Selector.open();          initServer(selector);          while (true) {              selector.select();              for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {                  SelectionKey key = (SelectionKey) itor.next();                  itor.remove();                  try {                      if (key.isAcceptable()) {                          ServerSocketChannel server = (ServerSocketChannel) key.channel();                          SocketChannel client = server.accept();                          System.out.println("Accepted connection from " + client);                          client.configureBlocking(false);                          SelectionKey clientKey = client.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);                          ByteBuffer buffer = ByteBuffer.allocate(100);                          clientKey.attach(buffer);                      }                      if (key.isReadable()) {                          SocketChannel client = (SocketChannel) key.channel();                          ByteBuffer buffer = (ByteBuffer) key.attachment();                          client.read(buffer);                      }                      if (key.isWritable()) {                          // System.out.println("is writable...");                          SocketChannel client = (SocketChannel) key.channel();                          ByteBuffer buffer = (ByteBuffer) key.attachment();                          buffer.flip();                          client.write(buffer);                          buffer.compact();                      }                  } catch (IOException e) {                      key.cancel();                      try { key.channel().close(); } catch (IOException ioe) { }                  }              }          }      }      private static void initServer(Selector selector) throws IOException,              ClosedChannelException {          ServerSocketChannel serverChannel = ServerSocketChannel.open();          ServerSocket ss = serverChannel.socket();          ss.bind(new InetSocketAddress(DEFAULT_PORT));          serverChannel.configureBlocking(false);          serverChannel.register(selector, SelectionKey.OP_ACCEPT);      }  }  

上面的代码很典型,运行结果似乎也是正确的。但是如果top用看一下发现服务器进程CPU占用到95%以上,如果取消掉32行的注释,服务器会不断地输出”is writable…”,这是为什么呢?来分析当第一个客户端连接上时发生什么情况。

在连接之前,selector.select()处于阻塞状态。当阻塞时,内核会将这个进程调度至休眠状态,此时基本不耗CPU。当客户端发起一个连接时,服务器检测到客户端连接,selector.select()返回。selector.selectedKeys()返回已就绪的SelectionKey的集合,在这种情况下,它只包含一个key,也就是注册的SelectionKey.OP_ACCEPT。服务器开始运行if (key.isAcceptable())块的代码,server.accept()返回代码客户端连接的socket,之后在socket上注册OP_READ和OP_WRITE,表示当socket可读或者可写时就会通知selector。

接着服务器重新进入循环,尽管这时客户端还没有任何输入,但这时selector.select()不会阻塞,因为socket注册了写操作,而socket只要send buffer不满就可以写,刚开始send buffer为空,socket总是可以写,于是server.select()立即返回,包含SelectionKey.OP_WRITE。由于这个key可写,所以服务器会运行if (key.isReadable())块的代码,但是这时buffer为空,client.write(buffer)没有向socket写任何东西,立即返回0。

接着服务器又回到循环,由于客户端连接socket可以写,这时selector.select()会立即返回,然后又运行if (key.isReadable())块的代码,这样不断循环,服务器却实际没有干事情,却耗大量的CPU。


从上面的分析可以看出问题在于没有数据可写时就在socket上注册了OP_WRITE,导致服务器浪费大量CPU资源。一个简单方法就是不要在同一个socket同时注册多个操作。对于上面的EchoServer来说就是不要同时注册OP_READ和OP_WRITE,要么只注册OP_READ,要么只注册OP_WRITE。下面的EchoServer修正了以上的错误:

public static void main(String[] args) throws IOException {      System.out.println("Listening for connection on port " + DEFAULT_PORT);      Selector selector = Selector.open();      initServer(selector);      while (true) {          selector.select();          for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {              SelectionKey key = (SelectionKey) itor.next();              itor.remove();              try {                  if (key.isAcceptable()) {                      ServerSocketChannel server = (ServerSocketChannel) key.channel();                      SocketChannel client = server.accept();                      System.out.println("Accepted connection from " + client);                      client.configureBlocking(false);                      SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ);                      ByteBuffer buffer = ByteBuffer.allocate(100);                      clientKey.attach(buffer);                  } else if (key.isReadable()) {                      SocketChannel client = (SocketChannel) key.channel();                      ByteBuffer buffer = (ByteBuffer) key.attachment();                      int n = client.read(buffer);                      if (n > 0) {                          buffer.flip();                          key.interestOps(SelectionKey.OP_WRITE);     // switch to OP_WRITE                      }                  } else if (key.isWritable()) {                      System.out.println("is writable...");                      SocketChannel client = (SocketChannel) key.channel();                      ByteBuffer buffer = (ByteBuffer) key.attachment();                      client.write(buffer);                      if (buffer.remaining() == 0) {  // write finished, switch to OP_READ                          buffer.clear();                          key.interestOps(SelectionKey.OP_READ);                      }                  }              } catch (IOException e) {                  key.cancel();                  try { key.channel().close(); } catch (IOException ioe) { }              }          }      }  }  

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

interface Handler {      void execute(Selector selector, SelectionKey key);  }  

再来看main函数:

public static void main(String[] args) throws IOException {      System.out.println("Listening for connection on port " + DEFAULT_PORT);      Selector selector = Selector.open();      initServer(selector);      while (true) {          selector.select();          for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {              SelectionKey key = (SelectionKey) itor.next();              itor.remove();              Handler handler = (Handler) key.attachment();              handler.execute(selector, key);          }      }  }  private static void initServer(Selector selector) throws IOException,          ClosedChannelException {      ServerSocketChannel serverChannel = ServerSocketChannel.open();      ServerSocket ss = serverChannel.socket();      ss.bind(new InetSocketAddress(DEFAULT_PORT));      serverChannel.configureBlocking(false);      SelectionKey serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);      serverKey.attach(new ServerHandler());  }  

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

class ServerHandler implements Handler {      public void execute(Selector selector, SelectionKey key) {          ServerSocketChannel server = (ServerSocketChannel) key.channel();          SocketChannel client = null;          try {              client = server.accept();              System.out.println("Accepted connection from " + client);          } catch (IOException e) {              e.printStackTrace();              return;          }          SelectionKey clientKey = null;          try {              client.configureBlocking(false);              clientKey = client.register(selector, SelectionKey.OP_READ);              clientKey.attach(new ClientHandler());          } catch (IOException e) {              if (clientKey != null)                  clientKey.cancel();              try { client.close(); } catch (IOException ioe) { }          }      }  }  

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

class ClientHandler implements Handler {      private ByteBuffer buffer;      public ClientHandler() {          buffer = ByteBuffer.allocate(100);      }      public void execute(Selector selector, SelectionKey key) {          try {              if (key.isReadable()) {                  readKey(selector, key);              } else if (key.isWritable()) {                  writeKey(selector, key);              }          } catch (IOException e) {              key.cancel();              try { key.channel().close(); } catch (IOException ioe) { }          }      }      private void readKey(Selector selector, SelectionKey key) throws IOException {          SocketChannel client = (SocketChannel) key.channel();          int n = client.read(buffer);          if (n > 0) {              buffer.flip();              key.interestOps(SelectionKey.OP_WRITE);     // switch to OP_WRITE          }      }      private void writeKey(Selector selector, SelectionKey key) throws IOException {          // System.out.println("is writable...");          SocketChannel client = (SocketChannel) key.channel();          client.write(buffer);          if (buffer.remaining() == 0) {  // write finished, switch to OP_READ              buffer.clear();              key.interestOps(SelectionKey.OP_READ);          }      }  }  
原创粉丝点击