美文网首页深入浅出Netty源码剖析netty
Netty学习 - 从TCP服务器到I/O模型

Netty学习 - 从TCP服务器到I/O模型

作者: buzzerrookie | 来源:发表于2018-07-12 00:36 被阅读37次

    学习Netty就不得不从TCP服务器和I/O模型说起,了解TCP服务器架构和I/O模型的演进有助于深入了解Netty。

    TCP服务器的架构

    一般地,TCP服务器有两种套接字,监听套接字和已连接套接字。监听套接字用于TCP的监听,一旦连接建立便产生已连接套接字,服务器利用已连接套接字与客户端进行通信。

    • 迭代服务器
      在迭代服务器中,监听套接字会一直阻塞直到能够接受连接,接受连接后利用已连接套接字与客户端通信,这些工作都是在同一个线程中完成的,示意Java代码如下。这种模式是串行处理,很难应对并发量较大的情况。
      try (ServerSocket serverSocket = new ServerSocket(port)) {
          while (true) {
              Socket socket = serverSocket.accept();
              // ...
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
      
    • 并发服务器
      在并发服务器中,监听套接字会一直阻塞直到能够接受连接,接受连接后,服务器会让子线程/进程去处理已连接套接字,示意Java代码如下。这种模式虽然是并行处理,可以不干扰服务端的监听,但是由于每次新来一个请求就会产生一个新的线程去处理,出于资源的考虑很难应对高并发的情况。
      try (ServerSocket serverSocket = new ServerSocket(port)) {
          while (true) {
              final Socket socket = serverSocket.accept();
              new Thread(() -> {
                  // ...
              }).start();
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
      
    • IO多路复用(事件驱动)
      为了能在一个线程中处理多个连接,可以使用IO多路复用(事件驱动),典型的有Linux C中的select、poll和epoll,Java的Selector类等。以Selector为例,调用者在选择器上为不同的连接注册自己感兴趣的事件(可读/可写/可接受/可连接),然后阻塞在select上,当事件发生时调用者便会得到通知,并且知道是哪个连接触发了事件,以便可以进一步处理。
      Selector实现的HTTP服务器示意如下:
       public class NioServer {
           private static final int BUFFER_SIZE = 512;
           private static final String HTTP_RESPONSE_BODY = "<html><body>Hello wolrd</body></html>\n";
           private static final String HTTP_RESPONSE_HEADER = "HTTP/1.1 200\r\n" +
                   "Content-Type: text/html\r\n" + "Content-Length: " + HTTP_RESPONSE_BODY.length() + "\r\n\r\n";
           private static final String HTTP_RESPONSE = HTTP_RESPONSE_HEADER + HTTP_RESPONSE_BODY;
           
           // IO多路复用
           public void selector(int port) {
               try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                   Selector selector = Selector.open()) {
                   serverSocketChannel.configureBlocking(false);
                   serverSocketChannel.bind(new InetSocketAddress(port));
                   serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
                   while (true) {
                       int readyChannels = selector.select();
                       if (readyChannels == 0) {
                           continue;
                       }
                       Set<SelectionKey> selectedKeys = selector.selectedKeys();
                       Iterator<SelectionKey> iter = selectedKeys.iterator();
                       while (iter.hasNext()) {
                           SelectionKey key = iter.next();
                           if (key.isAcceptable()) {
                               SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
                               System.out.println("accept: " + channel);
                               channel.configureBlocking(false);
                               channel.register(selector, SelectionKey.OP_READ);
                           }
                           if (key.isReadable()) {
                               SocketChannel channel = (SocketChannel) key.channel();
                               System.out.println("read: " + channel);
                               ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
                               int bytesRead = channel.read(buffer);
                               while (bytesRead > 0) {
                                   buffer.flip();
                                   while (buffer.hasRemaining()) {
                                       System.out.print((char) buffer.get());
                                   }
                                   buffer.clear();
                                   bytesRead = channel.read(buffer);
                               }
                               ByteBuffer writeBuf = ByteBuffer.wrap(HTTP_RESPONSE.getBytes());
                               while (writeBuf.hasRemaining()) {
                                   channel.write(writeBuf);
                               }
                               channel.close();
                           }
                           iter.remove();
                       }
                   }
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
       }
      

    I/O模型

    一个输入操作通常包括两个不同的阶段[1][2]

    1. 等待数据准备好
    2. 从内核向进程复制数据
    (1) 阻塞式I/O模型
    阻塞式IO模型.png
    (2) 非阻塞式I/O模型
    非阻塞式IO模型.png
    (3) I/O复用模型
    IO复用模型.png
    (4) 信号驱动式I/O模型
    信号驱动式IO模型.png
    (5) 异步I/O模型
    异步IO模型.png
    同步I/O和异步I/O对比

    POSIX把这两个术语定义如下:

    • 同步I/O操作导致请求进程阻塞,直至I/O操作完成;
    • 异步I/O操作不导致请求进程阻塞。

    根据上述定义,前4种模型——阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型和信号驱动式I/O模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。

    Netty

    Netty是一款异步的事件驱动的网络应用编程框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。与使用阻塞I/O来处理大量事件相比,使用非阻塞I/O来处理更快速、更经济,Netty使用了Reactor模式将业务和网络逻辑解耦,实现关注点分离[3]

    Reactor模式

    Reactor模式(反应堆模式)是一种处理一个或多个客户端并发交付服务请求的事件设计模式。当请求抵达后,服务处理程序使用I/O多路复用策略,然后同步地派发这些请求至相关的请求处理程序[4]
    Reactor模式中的角色:

    • Reactor:监听端口,响应与分发事件;
    • Acceptor:当Accept事件到来时Reactor将Accept事件分发给Acceptor,Acceptor将已连接套接字的通道注册到Reactor上;
    • Handler:已连接套接字做业务处理。
    单Reactor单线程

    在这种模式中,Reactor、Acceptor和Handler都运行在一个线程中。


    单Reactor单线程.png
    单Reactor多线程

    在这种模式中,Reactor和Acceptor运行在同一个线程,而Handler只有在读和写阶段与Reactor和Acceptor运行在同一个线程,读写之间对数据的处理会被Reactor分发到线程池中。


    单Reactor多线程.png
    多Reactor多线程

    在这种模式中,主Reactor负责监听,与Acceptor运行在同一个线程,Acceptor会将已连接套接字的通道注册到从Reactor上,从Reactor负责响应和分发事件,起到类似多线程Reactor的作用。Netty服务端使用了该种模式。


    多Reactor多线程.png

    参考文献

    [1].《UNIX环境高级编程》
    [2].《UNIX网络编程》
    [3].《Netty实战》
    [4]. Scalable IO in Java

    相关文章

      网友评论

        本文标题:Netty学习 - 从TCP服务器到I/O模型

        本文链接:https://www.haomeiwen.com/subject/ocshpftx.html