NIO 是一种同步非阻塞模型(Non-blocking IO),也是 IO 多路复用的基础。在了解 NIO 之前我们先回顾一下我们传统 IO 的相关知识。
首先我们通过一段传统的 BIO 代码来进行回顾。
public class BioServer {
private final ExecutorService executorService;
private final int port;
public BioServer(int port) {
executorService = Executors.newFixedThreadPool(100);
this.port = port;
}
public void startServer() throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(port));
while (!Thread.currentThread().isInterrupted()) {
Socket socket = serverSocket.accept();
executorService.submit(new IoHandler(socket));
}
}
private static class IoHandler implements Runnable {
private Socket socket;
public IoHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted() && !socket.isClosed()) {
// 读取数据
String data = readData(socket.getInputStream());
// 处理数据
String response = handlerData(data);
// 写入数据
writeResponse(socket.getOutputStream(), response);
}
} catch (Exception e) {
System.out.println("io handler occur error. " + e.getMessage());
}
}
private void writeResponse(OutputStream out, String data) throws IOException {
out.write(data.getBytes());
}
private String handlerData(String data) {
System.out.println(data);
return data;
}
private String readData(InputStream input) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input));
StringBuilder builder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
builder.append(line);
}
return builder.toString();
}
}
}
上述代码是我们传统的 SockerServer,是一种阻塞性的 IO。之所以称他为阻塞性 IO 是因为接收连接,读取数据、写回数据都会阻塞我们的线程。上述代码我们开启了一个线程池用来处理 Server 与客户端连接建立后的操作,这样可以避免我们的主线程一直阻塞只能处理单个连接。上述模型的主要缺点就是:
IO 操作其实就分为两个步骤,等待和操作。等待的意思其实就是等待连接建立,等待数据可读,等待数据可写,而操作则指的是读数据写数据。
微信截图_20200530215806.png
上述图很好的说明当下 5 种 IO 模型的阻塞特点。
int select (int n, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *timeout);
select 只有一个函数,调用 select 时,需要将监听句柄和最大等待时间作为参数传递进去,select 会发生阻塞,直到一个事件发生了,或者等到最大 1 秒钟(tv 定义了这个时间长度)就返回。select 主要有以下缺点:
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
poll 优化了 select 的一些问题,参数变得简单一些,没有了 1024 的限制。缺点:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents,
int timeout);
使用 epoll 时,需要先使用函数 epoll_create()在内核层创建了一个数据表,接口参数是一个表达要监听事件列表的长度的数值(会动态改变的)。这样一来,不用每次监听都要传一遍 fd(传递 fd 会导致 fd 数据从用户态复制到内核态)。创建完数据表,就可以使用另外一个函数 epoll_ctl()来管理数据表,对监听的 fd 执行增删改操作。最后再调用 epoll_wait()方法等待事件的发生。这样做的优点是:
通过 IO 多路复用我们可以实现一个线程处理多个 IO 操作,虽然单线程 IO 效率很高,没有上下文切换,但是在实际使用中单线程不可能满足我们的需求,后面就延伸出了 Reactor 模型,这个下节讲述。