1. Java的I/O發(fā)展簡史
1.1 JDK 1.0 到 JDK 1.3
Java的I/O類庫都非常原始,很多UNIX網(wǎng)絡(luò)編程中的概念或者接口在I/O類庫中都沒有體現(xiàn)竭望。
1.2 JDK 1.4
2002年發(fā)布JDK 1.4時屡律,NIO以JSR-51的身份正式隨JDK發(fā)布。
它新增了java.nio包,提供了很多進行異步I/O開發(fā)的API和類庫产捞。
1.3 JDK 1.7
2011年7月28日,JDK 1.7 正式發(fā)布哼御。
它的一個比較大的亮點是將原來的NIO類庫進行了升級坯临,被稱為NIO 2.0焊唬。
NIO 2.0由JSR-203演進而來。
2. 傳統(tǒng)的BIO編程
網(wǎng)絡(luò)編程的基本模型是Client/Server模型看靠,也就是兩個進行之間進行相互通信赶促,
其中服務(wù)端提供位置信息(綁定的IP地址和監(jiān)聽端口),客戶端通過連接操作向服務(wù)器監(jiān)聽的地址發(fā)起連接請求挟炬。
通過三次握手建立連接鸥滨,如果連接建立成功,雙方就可以通過網(wǎng)絡(luò)套接字(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主要的問題在于每當(dāng)有一個新的客戶端請求接入時婿滓,
服務(wù)端必須創(chuàng)建一個新的線程處理新接入的客戶端鏈路,
一個線程只能處理一個客戶端連接粥喜。
在高性能服務(wù)器應(yīng)用領(lǐng)域凸主,往往需要面向成千上萬個客戶端的并發(fā)連接,
這種模型顯然無法滿足高性能容客,高并發(fā)接入的場景秕铛。
3. 偽異步I/O模型
為了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題,
后來有人對它的線程模型進行了優(yōu)化缩挑,
后端通過一個線程池來處理多個客戶端的請求接入但两。
通過線程池可以靈活的調(diào)配線程資源,設(shè)置線程的最大值供置,防止由于海量并發(fā)接入導(dǎ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); // 創(chuàng)建IO任務(wù)線程池
while (true) {
socket = server.accept(); // 阻塞
singleExecutor.execute(new TimeServerHandler(socket));
}
分析
偽異步I/O通信框架采用了線程池實現(xiàn),因此避免了為每個請求都創(chuàng)建一個獨立線程造成的線程資源耗盡問題芥丧。
但是由于它底層的通信依然采用同步阻塞模型紧阔,因此無法從根本上解決問題。
4. NIO
Java NIO實現(xiàn)了UNIX網(wǎng)絡(luò)編程中的I/O復(fù)用模型续担,
一個多路復(fù)用器Selector可以同時輪詢多個Channel擅耽,
由于JDK使用了epoll()代替?zhèn)鹘y(tǒng)的select實現(xiàn),所以它并沒有最大連接句柄1024/2048的限制物遇。
這就意味著只需要一個線程負責(zé)Selector輪詢乖仇,就可以接入成千上萬的客戶端,這確實是個非常巨大的進步询兴。
4.1 Server
int port = 8080;
MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
// 構(gòu)造函數(shù)
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); // 監(jiān)聽 接收就緒 事件
}
// 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 {
// 如果發(fā)生了接收就緒事件,就給Selector添加一個讀就緒的監(jiān)聽
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ); // 監(jiān)聽 讀就緒 事件
}
// 如果發(fā)生了讀就緒事件
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();
// 構(gòu)造函數(shù)
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); // 監(jiān)聽 讀就緒 事件
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT); // 監(jiān)聽 連接就緒 事件
}
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引入了新的異步通道的概念警儒,并提供了異步文件通道和異步套接字通信的實現(xiàn)。
NIO 2.0的異步套接字通道是真正的異步非阻塞I/O眶根,對應(yīng)于UNIX網(wǎng)絡(luò)編程中的事件驅(qū)動I/O(AIO)蜀铲。
它不需要通過多路復(fù)用器(Selector)對注冊的通道進行輪詢操作即可實現(xiàn)異步讀寫边琉。
從而簡化了NIO的編程模型。
5.1 Server
int port = 8080;
AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
new Thread(timeServer, "AIO-AsyncTimeServerHandler-001").start();
// 構(gòu)造函數(shù)
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();
// 構(gòu)造函數(shù)
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網(wǎng)絡(luò)編程模型和JDK的實現(xiàn)進行區(qū)分,實際上它只能被稱為非阻塞I/O隆夯,
不能叫異步非阻塞I/O钳恕。
在早期的JDK 1.4和1.5 update10 版本之前,JDK的Selector基于select/poll模型實現(xiàn)蹄衷,
它是基于I/O復(fù)用技術(shù)的非阻塞I/O忧额,不是異步I/O。
在JDK 1.5 update10 和Linux core2.6 以上版本愧口,Sun優(yōu)化了Selector的實現(xiàn)睦番,
它在底層使用epoll替換了select/poll,上層的API沒有變化耍属,
可以認為是JDK NIO的一次性能優(yōu)化,但是它仍舊沒有改變I/O的模型厚骗。
由于JDK 1.7提供的NIO 2.0新增了異步套接字通信示启,它是真正的異步I/O,
在異步I/O操作的時候可以傳遞信號變量,當(dāng)操作完成之后回回調(diào)相關(guān)的方法排霉,異步I/O也被稱為AIO容燕。
6.2 多路復(fù)用器Selector
Java NIO的實現(xiàn)關(guān)鍵是多路復(fù)用I/O技術(shù)蘸秘,多路復(fù)用的核心就是通過Selector來輪詢注冊在其上的Channel官卡,
當(dāng)發(fā)現(xiàn)某個或者多個Channel處于就緒狀態(tài)后,從阻塞狀態(tài)返回就緒的Channel的選擇鍵集合醋虏,進行I/O操作寻咒。
6.3 偽異步I/O
偽異步I/O的概念完全來源于實踐,在JDK NIO編程沒有流行之前颈嚼,
為了解決Tomcat 通信線程同步I/O導(dǎo)致業(yè)務(wù)線程被掛住的問題毛秘,大家想到了一個辦法,
在通信線程和業(yè)務(wù)線程之間做個緩沖區(qū)阻课,這個緩沖區(qū)用于隔離I/O線程和業(yè)務(wù)線程間的直接訪問叫挟,
這樣業(yè)務(wù)線程就不會被I/O線程阻塞。
而對于后端的業(yè)務(wù)側(cè)來說限煞,將消息或者Task 放到線程池后就返回了抹恳,
它不再直接訪問I/O線程或者進行I/O讀寫,這樣也就不會被同步阻塞晰骑。
類似的設(shè)計還包括前端啟動一組線程适秩,將接受到的客戶端封裝成Task,
放到后端線程池執(zhí)行硕舆,用于解決一連接一線程問題秽荞。
對比
同步阻塞I/O(BIO) | 偽異步I/O | 非阻塞I/O(NIO) | 異步I/O(AIO) | |
---|---|---|---|---|
客戶端個數(shù):I/O線程 | 1:1 | M:N(其中M可以大于N) | M:1(1個I/O線程處理多個客戶端連接) | M:0(不需要啟動額外的I/O線程,被動回調(diào)) |
I/O類型(阻塞) | 阻塞 | 阻塞 | 非阻塞 | 非阻塞 |
I/O類型(同步) | 同步 | 同步 | 同步(I/O多路復(fù)用) | 異步 |
API使用難度 | 簡單 | 簡單 | 非常復(fù)雜 | 復(fù)雜 |
調(diào)試難度 | 簡單 | 簡單 | 復(fù)雜 | 復(fù)雜 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |