Unix 下共有五种 I/O 模型:
阻塞 I/O
非阻塞 I/O
I/O 多路复用(select和poll)
信号驱动 I/O(SIGIO)
异步 I/O(Posix.1的aio_系列函数)
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
同步和异步是相对于应用和内核的交互方式而言的,同步需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
选择器(Selector)
Java NIO的选择器允许一个单独的线程同时监视多个通道,可以注册多个通道到同一个选择器上,然后使用一个单独的线程来“选择”已经就绪的通道。这种“选择”机制为一个单独线程管理多个通道提供了可能。
零拷贝
Java NIO中提供的FileChannel拥有transferTo和transferFrom两个方法,可直接把FileChannel中的数据拷贝到另外一个Channel,或者直接把另外一个Channel中的数据拷贝到FileChannel。该接口常被用于高效的网络/文件的数据传输和大文件拷贝。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于Java IO中提供的方法。
使用FileChannel的零拷贝将本地文件内容传输到网络的示例代码如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
class NIOClient {
public static void main(String[] args) throws IOException, InterruptedException {
SocketChannel socketChannel = SocketChannel.open();
InetSocketAddress address =
new InetSocketAddress(
1234);
socketChannel.connect(address);
RandomAccessFile file =
new RandomAccessFile(
NIOClient.class.getClassLoader().getResource(
"test.txt").getFile(),
"rw");
FileChannel channel = file.getChannel();
channel.transferTo(
0, channel.size(), socketChannel);
channel.close();
file.close();
socketChannel.close();
}
}
|
阻塞I/O下的服务器实现
单线程逐个处理所有请求
使用阻塞I/O的服务器,一般使用循环,逐个接受连接请求并读取数据,然后处理下一个请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public
class IOServer {
private
static
final Logger LOGGER = LoggerFactory.getLogger(IOServer.class);
public static void main(String[] args) {
ServerSocket serverSocket =
null;
try {
serverSocket =
new ServerSocket();
serverSocket.bind(
new InetSocketAddress(
2345));
}
catch (IOException ex) {
LOGGER.error(
"Listen failed", ex);
return;
}
try{
while(
true) {
Socket socket = serverSocket.accept();
InputStream inputstream = socket.getInputStream();
LOGGER.info(
"Received message {}", IOUtils.toString(
new InputStreamReader(inputstream)));
}
}
catch(IOException ex) {
try {
serverSocket.close();
}
catch (IOException e) {
}
LOGGER.error(
"Read message failed", ex);
}
}
}
|
为每个请求创建一个线程
上例使用单线程逐个处理所有请求,同一时间只能处理一个请求,等待I/O的过程浪费大量CPU资源,同时无法充分使用多CPU的优势。下面是使用多线程对阻塞I/O模型的改进。一个连接建立成功后,创建一个单独的线程处理其I/O操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public
class IOServerMultiThread {
private
static
final Logger LOGGER = LoggerFactory.getLogger(IOServerMultiThread.class);
public static void main(String[] args) {
ServerSocket serverSocket =
null;
try {
serverSocket =
new ServerSocket();
serverSocket.bind(
new InetSocketAddress(
2345));
}
catch (IOException ex) {
LOGGER.error(
"Listen failed", ex);
return;
}
try{
while(
true) {
Socket socket = serverSocket.accept();
new Thread( () -> {
try{
InputStream inputstream = socket.getInputStream();
LOGGER.info(
"Received message {}", IOUtils.toString(
new InputStreamReader(inputstream)));
}
catch (IOException ex) {
LOGGER.error(
"Read message failed", ex);
}
}).start();
}
}
catch(IOException ex) {
try {
serverSocket.close();
}
catch (IOException e) {
}
LOGGER.error(
"Accept connection failed", ex);
}
}
}
|
使用线程池处理请求
为了防止连接请求过多,导致服务器创建的线程数过多,造成过多线程上下文切换的开销。可以通过线程池来限制创建的线程数,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public
class IOServerThreadPool {
private
static
final Logger LOGGER = LoggerFactory.getLogger(IOServerThreadPool.class);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
ServerSocket serverSocket =
null;
try {
serverSocket =
new ServerSocket();
serverSocket.bind(
new InetSocketAddress(
2345));
}
catch (IOException ex) {
LOGGER.error(
"Listen failed", ex);
return;
}
try{
while(
true) {
Socket socket = serverSocket.accept();
executorService.submit(() -> {
try{
InputStream inputstream = socket.getInputStream();
LOGGER.info(
"Received message {}", IOUtils.toString(
new InputStreamReader(inputstream)));
}
catch (IOException ex) {
LOGGER.error(
"Read message failed", ex);
}
});
}
}
catch(IOException ex) {
try {
serverSocket.close();
}
catch (IOException e) {
}
LOGGER.error(
"Accept connection failed", ex);
}
}
}
|
Reactor模式
精典Reactor模式
在Reactor模式中,包含如下角色
- Reactor
将I/O事件发派给对应的Handler - Acceptor
处理客户端连接请求 - Handlers
执行非阻塞读/写
最简单的Reactor模式实现代码如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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 keys = selector.selectedKeys();
Iterator iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel acceptServerSocketChanne
SocketChannel socketChannel = acceptServerSocketChanne
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)。同时注册时需要指定它所关注的事件,例如上示代码中socketServerChannel对象只注册了OP_ACCEPT事件,而socketChannel对象只注册了OP_READ事件。
selector.select()
是阻塞的,当有至少一个通道可用时该方法返回可用通道个数。同时该方法只捕获Channel注册时指定的所关注的事件。
多工作线程Reactor模式
经典Reactor模式中,尽管一个线程可同时监控多个请求(Channel),但是所有读/写请求以及对新连接请求的处理都在同一个线程中处理,无法充分利用多CPU的优势,同时读/写操作也会阻塞对新连接请求的处理。因此可以引入多线程,并行处理多个读/写操作,如下图所示。
多线程Reactor模式示例代码如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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 (
true) {
if(selector.selectNow() <
0) {
continue;
}
Set keys = selector.selectedKeys();
Iterator iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel acceptServerSocketChanne
SocketChannel socketChannel = acceptServerSocketChanne
socketChannel.configureBlocking(
false);
LOGGER.info(
"Accept request from {}", socketChannel.getRemoteAddress());
SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ);
readKey.attach(
new Processor());
}
else
if (key.isReadable()) {
Processor processor = (Processor) key.attachment();
processor.process(key);
}
}
}
}
}
|
从上示代码中可以看到,注册完SocketChannel的OP_READ事件后,可以对相应的SelectionKey attach一个对象(本例中attach了一个Processor对象,该对象处理读请求),并且在获取到可读事件后,可以取出该对象。
注:attach对象及取出该对象是NIO提供的一种操作,但该操作并非Reactor模式的必要操作,本文使用它,只是为了方便演示NIO的接口。
具体的读请求处理在如下所示的Processor类中。该类中设置了一个静态的线程池处理所有请求。而process方法并不直接处理I/O请求,而是把该I/O操作提交给上述线程池去处理,这样就充分利用了多线程的优势,同时将对新连接的处理和读/写操作的处理放在了不同的线程中,读/写操作不再阻塞对新连接请求的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public
class Processor {
private
static
final Logger LOGGER = LoggerFactory.getLogger(Processor.class);
private
static
final ExecutorService service = Executors.newFixedThreadPool(
16);
public void process(SelectionKey selectionKey) {
service.submit(() -> {
ByteBuffer buffer = ByteBuffer.allocate(
1024);
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int count = socketChannel.read(buffer);
if (count <
0) {
socketChannel.close();
selectionKey.cancel();
LOGGER.info(
"{}\t Read ended", socketChannel);
return
null;
}
else
if(count ==
0) {
return
null;
}
LOGGER.info(
"{}\t Read message {}", socketChannel,
new String(buffer.array()));
return
null;
});
}
}
|
多Reactor
Netty中使用的Reactor模式,引入了多Reactor,也即一个主Reactor负责监控所有的连接请求,多个子Reactor负责监控并处理读/写请求,减轻了主Reactor的压力,降低了主Reactor压力太大而造成的延迟。
并且每个子Reactor分别属于一个独立的线程,每个成功连接后的Channel的所有操作由同一个线程处理。这样保证了同一请求的所有状态和上下文在同一个线程中,避免了不必要的上下文切换,同时也方便了监控请求响应状态。
多Reactor示例代码如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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);
int coreNum = Runtime.getRuntime().availableProcessors();
Processor[] processors =
new Processor[coreNum];
for (
int i =
0; i < processors.length; i++) {
processors[i] =
new Processor();
}
int index =
0;
while (selector.select() >
0) {
Set keys = selector.selectedKeys();
for (SelectionKey key : keys) {
keys.remove(key);
if (key.isAcceptable()) {
ServerSocketChannel acceptServerSocketChanne
SocketChannel socketChannel = acceptServerSocketChanne
socketChannel.configureBlocking(
false);
LOGGER.info(
"Accept request from {}", socketChannel.getRemoteAddress());
Processor processor = processors[(
int) ((index++) / coreNum)];
processor.addChannel(socketChannel);
}
}
}
}
}
|
如上代码所示,本文设置的子Reactor个数是当前机器可用核数的两倍(与Netty默认的子Reactor个数一致)。对于每个成功连接的SocketChannel,通过round robin的方式交给不同的子Reactor。
子Reactor对SocketChannel的处理如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public
class Processor {
private
static
final Logger LOGGER = LoggerFactory.getLogger(Processor.class);
private
static
final ExecutorService service =
Executors.newFixedThreadPool(
2 * Runtime.getRuntime().availableProcessors());
private Selector selector;
public Processor() throws IOException {
this.selector = SelectorProvider.provider().openSelector();
start();
}
public void addChannel(SocketChannel socketChannel) throws ClosedChannelException {
socketChannel.register(
this.selector, SelectionKey.OP_READ);
}
public void start() {
service.submit(() -> {
while (
true) {
if (selector.selectNow() <=
0) {
continue;
}
Set keys = selector.selectedKeys();
Iterator iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(
1024);
SocketChannel socketChannel = (SocketChannel) key.channel();
int count = socketChannel.read(buffer);
if (count <
0) {
socketChannel.close();
key.cancel();
LOGGER.info(
"{}\t Read ended", socketChannel);
continue;
}
else
if (count ==
0) {
LOGGER.info(
"{}\t Message size is 0", socketChannel);
continue;
}
else {
LOGGER.info(
"{}\t Read message {}", socketChannel,
new String(buffer.array()));
}
}
}
}
});
}
}
|
在Processor中,同样创建了一个静态的线程池,且线程池的大小为机器核数的两倍。每个Processor实例均包含一个Selector实例。同时每次获取Processor实例时均提交一个任务到该线程池,并且该任务正常情况下一直循环处理,不会停止。而提交给该Processor的SocketChannel通过在其Selector注册事件,加入到相应的任务中。由此实现了每个子Reactor包含一个Selector对象,并由一个独立的线程处理。