本文通过实现一个简单的时间服务器和客户端,分别对JDK的BIO、NIO和JDK1.7中的NIO 2.0的使用进行介绍和对比,了解基于java的网络编程的发展。本文内容主要参考《Netty权威指南》。
BIO即同步阻塞IO,采用BIO通信方式的服务器,对于每一个连接请求都由服务器创建一个新的线层来接受和处理请求,处理完成后销毁线程。这就是典型的一请求一应答的模型。
同步阻塞IO服务端通信模型图
这种方式的坏处是缺乏弹性伸缩的能力,当客户端并发访问量很大时,服务器需要创建大量线程来处理请求,而在短时间内创建大量线程就会导致服务器资源不足,进而引起僵死甚至宕机,无法提供服务。
package bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
//use the default value;
}
}
try (ServerSocket server = new ServerSocket(port)) {
System.out.println("The time server started on port : " + port);
Socket socket = null;
while (true) {
socket = server.accept();
new Thread(new TimeServerHandler(socket)).start();
}
} finally {
System.out.println("The time server closed.");
}
}
}
服务器端程序启动后,在一个无限循环中接收来自客户端的连接,当没有客户端连接时,主线程则会阻塞在accept操作上。当有新的客户端接入的时候,主线程以该socket为参数创建一个TimeServerHandler对象,在新的线程中处理这条socket。
package bio;
import java.io.*;
import java.net.Socket;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeServerHandler implements Runnable {
private Socket socket;
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()))) {
String currentTime = null;
String body = null;
while (true) {
body = in.readLine();
if (body == null) {
break;
}
System.out.println("The time server received order : " + body);
if ("query time order".equalsIgnoreCase(body.trim())) {
currentTime = new java.util.Date().toString();
} else {
currentTime = "bad order";
}
out.println(currentTime);
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
if (socket != null) {
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
socket = null;
}
}
}
}
TimeServerHanler包含处理请求的逻辑:读取输入并判断是否为合法查询,如果是合法查询就返回服务器当前时间,否则返回"bad order"。
package bio;
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeClient {
public static void main(String[] args){
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
//use the default value;
}
}
try(Socket socket = new Socket("127.0.0.1", port);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()))){
out.println("QUERY TIME ORDER");
out.flush();
System.out.println("send order to server");
String resp = in.readLine();
System.out.println("Now is : " + resp);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
imeClient创建了连接到服务器的socket之后,向服务器发送请求,将收到的返回结果打印之后退出。
BIO的主要问题在于每一个客户端连接都需要一个线程来处理,一个处理线程也只能处理一个客户端连接。而在高性能服务器应用领域,往往要面对成千上万个并发连接,这种模型显然无法满足高性能、高并发的接入场景。
为了解决BIO方式中每一个请求都需要一个线程来处理的问题,有人对它的线程模型进行了优化,在服务器端采取线程池的形式来处理客户端的多个请求,这样就可以有M个服务器线程来处理N个客户端请求。这里的M可以远远大于N,这样一来可以根据服务器情况灵活配置M和N的比例,防止创建海量的并发访问耗尽服务器资源。
当一个用户请求来到服务器时,服务器会将客户端Socket封装成一个Task(继承了java.lang.Runnable接口),然后将Task交由服务器端的线程池处理。服务器维护一个消息队列和若干个worker线程,worker线程从消息队列中取出Task执行。由于消息队列和worker线程的数量都是灵活可控的,它们占用的资源也是可控的,所以不用担心会耗尽服务器资源。
伪异步IO服务端通信模型
这种解决方案来自于在JDK NIO没有流行之前,为了解决Tomcat通信线程同步I/O导致业务线程被挂住的情况,实现者在通信线程和业务线程之间加了一个缓冲区,用于隔离I/O线程和业务线程间的直接访问,这样业务线程就不会被I/O线程阻塞。
采用线程池做缓冲的TimeServer
package fnio;
import bio.TimeServerHandler;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
//use the default value;
}
}
try (ServerSocket server = new ServerSocket(port)) {
System.out.println("The time server started on port : " + port);
TimeServerExecutePool singleExecutor = new TimeServerExecutePool(50,
1000);
Socket socket = null;
while (true) {
socket = server.accept();
singleExecutor.execute(new TimeServerHandler(socket));
}
} finally {
System.out.println("The time server closed");
}
}
}
package fnio;
import bio.TimeServerHandler;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeServerExecutePool {
private Executor executor;
public TimeServerExecutePool(int maxPoolSize, int queueSize) {
this.executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L,
TimeUnit.SECONDS, new ArrayBlockingQueue(queueSize));
}
public void execute(TimeServerHandler timeServerHandler) {
executor.execute(timeServerHandler);
}
}
TimeServer的主函数实现发生了一点变化,首先创建了一个线程池TimeServerExecutePool,每次接收一个socket,就将其封装为Task,交由线程池处理,从而避免了每一个连接都创建新线程的操作。
虽然通过线程池的方式避免了过多的线程耗尽资源,但这种实现方式底层通信仍是同步阻塞模型,在处理输入和输出的时候,调用的仍然是会发生阻塞的api。当消息的接收方处理较慢的时候,TCP缓冲区剩余空间会减少,接收窗口的大小也会不断减小,知道为零。当双方处于keep-alive状态,这时发送方将无法再向TCP缓冲区写入数据,这时调用阻塞IO,write操作将会无限期阻塞,直到发送窗口大于零或者发生IO异常。
jdk1.4版本推出了NIO,意思是new I/O,NIO最大的特性是支持了非阻塞I/O,所以NIO也可以解释为non-blocking I/O,在这里采用的是后面一种意思。
NIO弥补了原来同步阻塞I/O的不足,提供了高效的,面向块的I/O,为此NIO引入了一些新的概念。
采用NIO实现的TimeServer
NIO服务端通信序列图
package nio;
import java.io.IOException;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
//use the default value;
}
}
MultiplexerTimeServer server = new MultiplexerTimeServer(port);
new Thread(server, "NIO-MultiplexerTimeServer--001").start();
}
}
TimeServer在主函数中创建了一个MultiplexerTimeSever的多路复用类,它的是一个独立的线程,负责轮询Selector,处理多个客户端的并发接入。
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
/**
* @author elricboa on 2018/1/10.
*/
public class MultiplexerTimeServer implements Runnable{
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean stop;
public MultiplexerTimeServer(int port) {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket()
.bind(new InetSocketAddress(port), 1024);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server started on port : " + port);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
try {
selector.select(1000);
Set selectionKeySet = selector.selectedKeys();
SelectionKey key = null;
for (Iterator itr = selectionKeySet
.iterator(); itr.hasNext(); ) {
key = itr.next();
itr.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
if (key.isAcceptable()) {
ServerSocketChannel ssc = ((ServerSocketChannel) key.channel());
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel sc = ((SocketChannel) key.channel());
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println(
"The time server received order : " + body);
String currentTime = "query time order"
.equalsIgnoreCase(body.trim()) ?
new Date().toString() :
"bad order";
doWrite(sc, currentTime);
} else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
private void doWrite(SocketChannel channel, String response)
throws IOException {
if (channel != null && response != null
&& response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
}
}
MultiplexerTimeServer在初始化时,会创建Selector和SeverSocketChannel,然后将ServerSocketChannel注册到Selector上,监听SelectionKey.OP_ACCEPT事件。在run方法遍历selector,无论是否有事件发生,selector会每隔一秒被唤醒,如果有事件发生,则对就绪状态的Channel集合进行迭代操作。在handleInput方法中处理客户端的请求信息,先根据当前的SelectionKey判断事件类型,然后建立SocketChannel,创建ByteBuffer接收客户端的请求数据,解码后判断请求数据的合法性,最后将响应数据异步地发送给客户端。
NIO客户端序列图
package nio;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeClient {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
//use the default value;
}
}
new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient-001")
.start();
}
}
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* @author elricboa on 2018/1/11.
*/
public class TimeClientHandle implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public TimeClientHandle(String host, int port) {
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
@Override public void run() {
try {
doConnect();
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
while (!stop) {
try {
selector.select(1000);
Set selectionKeySet = selector.selectedKeys();
SelectionKey key = null;
for (Iterator itr = selectionKeySet
.iterator(); itr.hasNext(); ) {
key = itr.next();
itr.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void doConnect() throws IOException {
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel sc = ((SocketChannel) key.channel());
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else {
System.exit(1);
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Now is : " + body);
this.stop = true;
} else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
private void doWrite(SocketChannel sc) throws IOException {
byte[] request = "query time order".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(request.length);
writeBuffer.put(request);
writeBuffer.flip();
sc.write(writeBuffer);
if(!writeBuffer.hasRemaining()){
System.out.println("send order to server succeed");
}
}
}
客户端与服务端实现类似,不再赘述。
注意:由于NIO中的SocketChannel是异步非阻塞的,在执行发送操作时不能保证将需要发送的数据发送完,所以会存在“半包写”的问题,我们需要注册OP_WRITE,不断轮询selector将未发送完的数据发送完毕,可以通过ByteBuffer的hasRemaining方法判断发送是否完成。因为此处只是简单示例,并没有处理这个问题,后续文章会有详细介绍。
严格来说,NIO只能被称为非阻塞IO,而并不能被称为异步非阻塞IO。因为在JDK1.5 update10之前,它的底层实现是基于I/O复用技术的非阻塞I/O,而不是异步I/O。在JDK1.5 update10和Linux core2.6以上版本,sun优化了Selector的实现,在底层使用epoll替换了select/poll,上层的API并没有变化,可以认为是一次性能优化,但并没有改变其I/O模型。
NIO的优点总结:1.客户端建立连接的过程是非阻塞的。建立连接时只需在Selector上注册相应的事件,而不需要同步阻塞。2.网络读写是非阻塞的,如果没有可用的数据不会等待,而是直接返回,这样I/O通信线程可以处理其他链路,不必等到这个链路可用。3.线程模型的优化:JDK的Selector在Unix等主流系统的底层实现为epoll,这样的好处是它没有连接句柄数的限制,这意味着一个Selector可以处理成千上万个连接而性能不会随着连接数上升而线性下降。
jdk1.7版本升级了NIO类库,被称为NIO 2.0,它是真正的异步IO,在异步操作是可以通过传递信号量,当操作完成时会执行相关的回调方法。NIO 2.0提供了真正的异步文件I/O操作,并提供了与Unix网络编程事件驱动I/O对应的AIO。NIO 2.0提供了异步文件通道和异步套接字通道的实现,异步通道通过两种方式提供操作的返回结果:
AIO 实现的TimeServer
package aio;
/**
* @author elricboa on 2018/1/11.
*/
public class TimeServer {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
//use the default value;
}
}
AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
new Thread(timeServer, "AIO-AsyncTimeServerHandler-001").start();
}
}
package aio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.CountDownLatch;
/**
* @author elricboa on 2018/1/11.
*/
public class AsyncTimeServerHandler implements Runnable {
private int port;
CountDownLatch latch;
AsynchronousServerSocketChannel asynchronousServerSocketChannel;
public AsyncTimeServerHandler(int port) {
this.port = port;
try {
asynchronousServerSocketChannel = AsynchronousServerSocketChannel
.open();
asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
doAccept();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void doAccept() {
asynchronousServerSocketChannel
.accept(this, new AcceptCompletionHandler());
}
}
AsyncTimeServerHandler在构造方法中先创建一个异步的服务端通道AsynchronousServerSocketChannel,然后绑定端口。在run方法中,初始化一个CountDownLatch对象,以阻塞当前线程,防止主线程在执行完成前退出。doAccept方法用于接收客户端连接,在异步操作是传入一个CompletionHandler的实例,用于接收accpet操作成功的通知消息,
package aio;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
/**
* @author elricboa on 2018/1/11.
*/
public class AcceptCompletionHandler implements
CompletionHandler {
@Override public void completed(AsynchronousSocketChannel result,
AsyncTimeServerHandler attachment) {
attachment.asynchronousServerSocketChannel.accept(attachment, this);
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
result.read(readBuffer, readBuffer, new ReadCompletionHandler(result));
}
@Override public void failed(Throwable exc,
AsyncTimeServerHandler attachment) {
exc.printStackTrace();
attachment.latch.countDown();
}
}
package aio;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.Date;
/**
* @author elricboa on 2018/1/11.
*/
public class ReadCompletionHandler implements CompletionHandler {
private AsynchronousSocketChannel channel;
public ReadCompletionHandler(AsynchronousSocketChannel result) {
if (this.channel == null) {
this.channel = result;
}
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] body = new byte[attachment.remaining()];
attachment.get(body);
try {
String request = new String(body, "UTF-8");
System.out.println("The time server received order : " + request);
String currentTime = "query time order"
.equalsIgnoreCase(request.trim()) ?
new Date().toString() :
"bad order";
System.out.println(currentTime);
doWrite(currentTime);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
private void doWrite(String currentTime) {
if(currentTime != null && currentTime.trim().length() > 0) {
byte[] response = currentTime.getBytes();
final ByteBuffer writeBuffer = ByteBuffer.allocate(response.length);
writeBuffer.put(response);
writeBuffer.flip();
channel.write(writeBuffer, writeBuffer, new CompletionHandler() {
@Override public void completed(Integer result,
ByteBuffer attachment) {
if(attachment.hasRemaining()){
channel.write(attachment, attachment, this);
}
}
@Override public void failed(Throwable exc,
ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
ReadCompletionHandler用于异步读取客户端的请求数据,进行合法性判断之后再异步地写入返回数据。
TimeClient实现:
package aio;
/**
* @author elricboa on 2018/1/10.
*/
public class TimeClient {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
//use the default value;
}
}
new Thread(new AsyncTimeClientHandler("127.0.0.1", port),
"AIO-AsyncTimeClientHandler-001").start();
}
}
AsyncTimeClientHandler.java实现:
package aio;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;
/**
* @author elricboa on 2018/1/11.
*/
public class AsyncTimeClientHandler implements CompletionHandler, Runnable {
private AsynchronousSocketChannel client;
private String host;
private int port;
private CountDownLatch latch;
public AsyncTimeClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
client = AsynchronousSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
client.connect(new InetSocketAddress(host, port), this, this);
try {
latch.await();
client.close();
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
}
@Override
public void completed(Void result,
AsyncTimeClientHandler attachment) {
byte[] request = "query time order".getBytes();
final ByteBuffer writeBuffer = ByteBuffer.allocate(request.length);
writeBuffer.put(request);
writeBuffer.flip();
client.write(writeBuffer, writeBuffer,
new CompletionHandler() {
@Override
public void completed(Integer result,
ByteBuffer attachment) {
if (writeBuffer.hasRemaining()) {
client.write(writeBuffer, writeBuffer, this);
} else {
final ByteBuffer readBuffer = ByteBuffer
.allocate(1024);
client.read(readBuffer, readBuffer,
new CompletionHandler() {
@Override
public void completed(
Integer result,
ByteBuffer attachment) {
attachment.flip();
byte[] bytes = new byte[attachment
.remaining()];
readBuffer.get(bytes);
try {
String body = new String(bytes,
"UTF-8");
System.out.println(
"Now is : " + body);
latch.countDown();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void failed(
Throwable exc,
ByteBuffer attachment) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
@Override
public void failed(Throwable exc,
ByteBuffer attachment) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc,
AsyncTimeClientHandler attachment) {
exc.printStackTrace();
try {
client.close();
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
}
对比
|
同步阻塞I/O
|
带缓冲区的同步I/O
|
非阻塞I/O
|
异步I/O
|
---|---|---|---|---|
客户端数:I/O线程 | 1:1 | M:N(其中M可以大于N) | M:1(1个I/O线程处理多个连接) | M:0(无需额外的I/O线程,被动回调) |
I/O类型(阻塞) | 阻塞 | 阻塞 | 非阻塞 | 非阻塞 |
I/O类型(同步) | 同步 | 同步 | 同步(I/O多路复用) | 异步 |
API使用难度 | 简单 | 简单 | 复杂 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 | 复杂 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |