[Java] I/O:BIO, NIO, AIO
1. Java的I/O发展简史
1.1 JDK 1.0 到 JDK 1.3
Java的I/O类库都非常原始,很多UNIX网络编程中的概念或者接口在I/O类库中都没有体现。
1.2 JDK 1.4
2002年发布JDK 1.4时,NIO以JSR-51的身份正式随JDK发布。
它新增了java.nio包,提供了很多进行异步I/O开发的API和类库。
1.3 JDK 1.7
2011年7月28日,JDK 1.7 正式发布。
它的一个比较大的亮点是将原来的NIO类库进行了升级,被称为NIO 2.0。
NIO 2.0由JSR-203演进而来。
2. 传统的BIO编程
网络编程的基本模型是Client/Server模型,也就是两个进行之间进行相互通信,
其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务器监听的地址发起连接请求。
通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
2.1 Server
int port = 8080;
server = new ServerSocket(port); // The time server is started in port 8080
Socket socket = null;
while (true) {
socket = server.accept(); // 阻塞
new Thread(new TimeServerHandler(socket)).start();
}
2.2 Client
socket = new Socket("127.0.0.1", port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
out.println("QUERY TIME ORDER"); // Send order 2 server succeed.
String resp = in.readLine();
System.out.println("Now is : " + resp);
分析
BIO主要的问题在于每当有一个新的客户端请求接入时,
服务端必须创建一个新的线程处理新接入的客户端链路,
一个线程只能处理一个客户端连接。
在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,
这种模型显然无法满足高性能,高并发接入的场景。
3. 伪异步I/O模型
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,
后来有人对它的线程模型进行了优化,
后端通过一个线程池来处理多个客户端的请求接入。
通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
Server
int port = 8080;
ServerSocket server = new ServerSocket(port); // The time server is start in port 8080
Socket socket = null;
TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(50, 10000); // 创建IO任务线程池
while (true) {
socket = server.accept(); // 阻塞
singleExecutor.execute(new TimeServerHandler(socket));
}
分析
伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。
但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。
4. NIO
Java NIO实现了UNIX网络编程中的I/O复用模型,
一个多路复用器Selector可以同时轮询多个Channel,
由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。
这就意味着只需要一个线程负责Selector轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。
4.1 Server
int port = 8080;
MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
// 构造函数
public MultiplexerTimeServer(int port) {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false); // 非阻塞方式
servChannel.socket().bind(new InetSocketAddress(port), 1024);
servChannel.register(selector, SelectionKey.OP_ACCEPT); // 监听 接收就绪 事件
}
// run 方法
@Override
public void run() {
white(!stop) {
selector.select(1000); // 休眠1s,Selector每隔一秒被唤醒一次
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
handleInput(key);
}
}
}
// handleInput 方法
private void handleInput(SelectionKey key) throws IOException {
// 如果发生了接收就绪事件,就给Selector添加一个读就绪的监听
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);
...
}
}
4.2 Client
int port = 8080;
new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient-001").start();
// 构造函数
public TimeClientHandle(String host, int port) {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
}
// run 方法
@Override
public void run() {
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ); // 监听 读就绪 事件
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT); // 监听 连接就绪 事件
}
white(!stop) {
selector.select(1000); // 休眠1s,Selector每隔一秒被唤醒一次
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
handleInput(key);
}
}
}
5. AIO
NIO 2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通信的实现。
NIO 2.0的异步套接字通道是真正的异步非阻塞I/O,对应于UNIX网络编程中的事件驱动I/O(AIO)。
它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写。
从而简化了NIO的编程模型。
5.1 Server
int port = 8080;
AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
new Thread(timeServer, "AIO-AsyncTimeServerHandler-001").start();
// 构造函数
public AsyncTimeServerHandler(int port) {
asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
asynchronousServerSocketChannel.bind(new InetSocketAddress(port)); // The time server is start in port 8080
}
// run 方法
@Override
public void run() {
latch = new CountDownLatch(1);
asynchronousServerSocketChannel.accept(this, new AcceptCompletionHandler());
latch.await();
}
// AcceptCompletionHandler
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler> {
@Override
public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler attachment) {
attachment.asynchronousServerSocketChannel.accept(attachment, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
result.read(buffer, buffer, new ReadCompletionHandler(result));
}
@Override
public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
exc.printStackTrace();
attachment.latch.countDown();
}
}
5.2 Client
int port = 8080;
new Thread(new AsyncTimeClientHandler("127.0.0.1", port), "AIO-AsyncTimeClientHandler-001").start();
// 构造函数
public AsyncTimeClientHandler(String host, int port) {
client = AsynchronousSocketChannel.open();
}
// run 方法
@Override
public void run() {
latch = new CountDownLatch(1);
client.connect(new InetSocketAddress(host, port), this, this);
latch.await();
}
// completed 方法
public void completed(Void result, AsyncTimeClientHandler attachment) {
...
client.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
...
client.read(readBuffer, readBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
...
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
...
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
...
}
});
}
6. 4种I/O对比
6.1 异步非阻塞I/O
很多人喜欢将JDK 1.4提供的NIO框架称为异步非阻塞I/O,
但是,如果严格按照UNIX网络编程模型和JDK的实现进行区分,实际上它只能被称为非阻塞I/O,
不能叫异步非阻塞I/O。
在早期的JDK 1.4和1.5 update10 版本之前,JDK的Selector基于select/poll模型实现,
它是基于I/O复用技术的非阻塞I/O,不是异步I/O。
在JDK 1.5 update10 和Linux core2.6 以上版本,Sun优化了Selector的实现,
它在底层使用epoll替换了select/poll,上层的API没有变化,
可以认为是JDK NIO的一次性能优化,但是它仍旧没有改变I/O的模型。
由于JDK 1.7提供的NIO 2.0新增了异步套接字通信,它是真正的异步I/O,
在异步I/O操作的时候可以传递信号变量,当操作完成之后回回调相关的方法,异步I/O也被称为AIO。
6.2 多路复用器Selector
Java NIO的实现关键是多路复用I/O技术,多路复用的核心就是通过Selector来轮询注册在其上的Channel,
当发现某个或者多个Channel处于就绪状态后,从阻塞状态返回就绪的Channel的选择键集合,进行I/O操作。
6.3 伪异步I/O
伪异步I/O的概念完全来源于实践,在JDK NIO编程没有流行之前,
为了解决Tomcat 通信线程同步I/O导致业务线程被挂住的问题,大家想到了一个办法,
在通信线程和业务线程之间做个缓冲区,这个缓冲区用于隔离I/O线程和业务线程间的直接访问,
这样业务线程就不会被I/O线程阻塞。
而对于后端的业务侧来说,将消息或者Task 放到线程池后就返回了,
它不再直接访问I/O线程或者进行I/O读写,这样也就不会被同步阻塞。
类似的设计还包括前端启动一组线程,将接受到的客户端封装成Task,
放到后端线程池执行,用于解决一连接一线程问题。
对比
同步阻塞I/O(BIO) | 伪异步I/O | 非阻塞I/O(NIO) | 异步I/O(AIO) | |
---|---|---|---|---|
客户端个数:I/O线程 | 1:1 | M:N(其中M可以大于N) | M:1(1个I/O线程处理多个客户端连接) | M:0(不需要启动额外的I/O线程,被动回调) |
I/O类型(阻塞) | 阻塞 | 阻塞 | 非阻塞 | 非阻塞 |
I/O类型(同步) | 同步 | 同步 | 同步(I/O多路复用) | 异步 |
API使用难度 | 简单 | 简单 | 非常复杂 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 | 复杂 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |