从IO到NIO

1.传统的IO

1.1 网络服务的基本结构
当今网络上的各种基于TCP/IP的应用服务,其对1次请求的处理过程的本质流程结构均为

  • 从底层IO读取字节请求
  • 把读取后的字节请求进行解码成为自己的业务请求对象
  • 把解码后的业务请求对象进行业务处理
  • 把处理后的响应编码为底层IO可写入的字节响应
  • 利用底层IO返回(发出)编码后的字节响应





1.2 阻塞I/O下的服务器实现

1)单线程逐个处理所有请求

使用单线程逐个处理所有请求,同一时间只能处理一个请求,等待I/O的过程浪费大量CPU资源,同时无法充分使用多CPU的优势。

2)为每个请求创建一个线程
使用多线程对阻塞I/O模型的改进。一个连接建立成功后,创建一个单独的线程处理其I/O操作。



3)使用线程池处理请求
为了防止连接请求过多,导致服务器创建的线程数过多,造成过多线程上下文切换的开销。可以通过线程池来限制创建的线程数,


1.3 阻塞I/O存在一些缺点

1. 当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间
2. 阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。

在这种情况下非阻塞式I/O就有了它的应用前景。


2.Java NIO

2.1 NIO的工作原理
1. 由一个或几个线程来监听所有的 IO 事件,并负责分发。避免创建大量线程。  
2. 采用I/O多路复用(
事件驱动),保证每次上下文切换都是有意义的 

2.2 NIO框架的事件驱动机制
Selector.open(),方法获取操作系统io多路复用的机制,比如Linux 2.6+平台java提供的是epoll 机制,mac os x 提供的是kqueue机制。 selector上注册要监听的IO事件,NIO框架则回调相关的任务去执行。


2.3 适用场景
如果你需要同时管理成千上万的连接,但是每个连接只发送少量数据,例如一个聊天服务器,用NIO可以避免创建大量的线程和线程切换造成资源的浪费。如果只有少量的连接但是每个连接都占有很高的带宽,同时发送很多数据,传统的IO会更适合




Reactor模式

单线程
同Swing/AWT事件驱动设计类似,Reactor模式也是多生产者/单一消费者模式,多个IO(读 /写)事件,但是处理IO事件却只在单一的EventLoop(事件循环)线程中分发给对应的任务处理器处理。基本的Reactor模式(单线程版)如下所示:


最简单的Reactor模式实现代码如下所示。

public class NIOServer {
  private static final Logger LOGGER = LoggerFactory.getLogger(NIOServer.class);
  public static void main(String[] args) throws IOException {
    Selector selector = Selector.open();
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.configureBlocking(false);
    serverSocketChannel.bind(new InetSocketAddress(1234));
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (selector.select() > 0) {
      Set<SelectionKey> keys = selector.selectedKeys();
      Iterator<SelectionKey> iterator = keys.iterator();
      while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        iterator.remove();
        if (key.isAcceptable()) {
          ServerSocketChannel acceptServerSocketChannel = (ServerSocketChannel) key.channel();
          SocketChannel socketChannel = acceptServerSocketChannel.accept();
          socketChannel.configureBlocking(false);
          LOGGER.info("Accept request from {}", socketChannel.getRemoteAddress());
          socketChannel.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {
          SocketChannel socketChannel = (SocketChannel) key.channel();
          ByteBuffer buffer = ByteBuffer.allocate(1024);
          int count = socketChannel.read(buffer);
          if (count <= 0) {
            socketChannel.close();
            key.cancel();
            LOGGER.info("Received invalide data, close the connection");
            continue;
          }
          LOGGER.info("Received message {}", new String(buffer.array()));
        }
        keys.remove(key);
      }
    }
  }
}


为了方便阅读,上示代码将Reactor模式中的所有角色放在了一个类中。
从上示代码中可以看到,多个Channel可以注册到同一个Selector对象上,实现了一个线程同时监控多个请求状态(Channel)。同时注册时需要指定它所关注的事件


多工作线程
经典Reactor模式中,尽管一个线程可同时监控多个请求(Channel),但是所有读/写请求以及对新连接请求的处理都在同一个线程中处理,无法充分利用多CPU的优势,同时读/写操作也会阻塞对新连接请求的处理。因此可以引入多线程,并行处理多个读/写操作,如下图所示。




多Reactor/多Selector


Netty中使用的Reactor模式,引入了多Reactor,也即一个主Reactor负责监控所有的连接请求,多个子Reactor负责监控并处理读/写请求,减轻了主Reactor的压力,降低了主Reactor压力太大而造成的延迟。
并且每个子Reactor分别属于一个独立的线程,每个成功连接后的Channel的所有操作由同一个线程处理。这样保证了同一请求的所有状态和上下文在同一个线程中,避免了不必要的上下文切换,同时也方便了监控请求响应状态










猜你喜欢

转载自blog.csdn.net/dongnan591172113/article/details/69568900