学习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) 阻塞式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
网友评论