異步IO編程在javascript中獲得了普遍的應用,以前也寫過一篇博文進行梳理。
js的異步IO便是異步的,也是非阻塞的。非阻塞的IO須要底層操做系統的支持,好比在linux上的epoll系統調用。javascript
從另一個角度看待的話,底層操做系統對於非阻塞IO的系統調用是一種多路複用機制,js對其進行了比較厚的封裝,轉換成了異步IO。
可是,也能夠進行一層稍微薄點的封裝,保留這種多路複用的模型,好比java的NIO,是一種同步非阻塞的IO模型。
非阻塞IO的一大優點是,性能好,快啊!這在對IO性能要求高的場景獲得了大量應用,好比SOA框架。html
<!--more-->java
傳統的同步IO方式,好比網絡傳輸,好比文件IO,在調用者調用read()時,調用會被一層一層調用下去直到OS的系統調用,調用者的線程會被阻塞。
當讀取完成時,該線程又會被喚醒,read()函數返回IO操做讀取的數據。linux
咱們很容易能發現這種方式的特色及優劣:git
在客戶端編程時,第二點這個問題不大。客戶端程序對IO的併發要求不高,反而由於同步阻塞IO的接口易於編程而可以減輕編程難度,代碼更直觀更可讀,從而變相的提升可調試性和開發效率。github
然而,在服務器端編程的時候,這個劣勢就很明顯了,服務器端程序可能會面臨大量併發IO的考驗。
傳統的同步IO方式,好比說socket編程,服務器端的一個簡單的處理邏輯是這樣的:編程
在實際場景中會有不少優化技術,好比使用線程池。然而線程池僅僅是將TCP鏈接放入一個隊列裏交由線程池中空閒的線程處理。
實質上,即便使用線程池,也改變不了正在被處理的每個請求都須要佔用一個單獨的線程這一事實。
這樣,會形成一些問題:segmentfault
java提供的NIO就是一種多路複用IO方式。
它可以將多個IO操做用一個線程去管理,一個線程便可管理多個IO操做。bash
NIO的操做邏輯是這樣的,首先將須要監控的IO操做註冊到某個地方,並由一個線程管理。
當這些IO操做完成,會以事件的形式產生。該線程可以獲取到完成的事件列表,而且對其進行處理。服務器
java的NIO中有三個重要的概念:
這裏只是作個總結,看下下面的示例代碼就明白了。
private void exec(int port) throws IOException { Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(port)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { int n = selector.select(); // Block Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel = server.accept(); if (channel != null) { channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); onAccept(channel); } } if (key.isReadable()) { SocketChannel socketChannel = (SocketChannel) key.channel(); onRead(socketChannel); } it.remove(); } } }
來一步一步的分析這些代碼。
首先,第3行到第6行是對通道ServerSocketChannel
的操做。
對於這個ServerSocketChannel,首先是設定了它的監聽地址,這個與傳統的阻塞IO一致,給定一些初始的數據。傳統的阻塞IO以後會調用socket.accept()
來獲取客戶端鏈接的TCP鏈接,這是一個阻塞的方法。
可是NIO在這裏把ServerSocketChannel註冊到了Selector上,而且監控OP_ACCEPT事件。這個時候socket能夠認爲已經在監聽了,可是沒有阻塞線程。
以後,若是有TCP鏈接鏈接上,OP_ACCEPT事件就會產生,經過selector便可處理該事件。
所以,NIO的操做邏輯實際上是事件驅動的。
後面的循環則是Selector處理的主邏輯。
第9行,這是一個阻塞的方法。它會等待被註冊的這些IO操做處理完成。一旦有一部分IO操做完成,它就會返回。
經過selector.selectedKeys()
便可得到完成的IO操做的事件。後面的代碼也就是在處理這些事件。
這部分完成的IO事件處理完畢後,就會循環的去處理下一批完成的IO事件,如此往復。
這裏,咱們能夠清晰的看到,經過NIO的多路複用模型,咱們經過一個線程,就能管理多個IO操做。
循環內部處理的邏輯,key.isAcceptable()
能夠認爲是判斷該事件是不是OP_ACCEPT
事件。是的話表示已經有客戶端TCP鏈接鏈接上了,第15行獲取該TCP鏈接的socket對象。因爲是NIO編程,這是獲取到的是SocketChannel
對象。
以後將該對象的OP_READ
註冊到Selector上,發起IO讀操做,而且讓Selector監聽讀完成的事件。
後面的key.isReadable()
也是一樣的道理,這裏只有上面的代碼註冊了OP_READ
事件,所以這裏必定是上面的讀操做完成了產生的事件。
上面的代碼裏,當有新的TCP鏈接連入時,調用回調函數onAccept
;當對方傳輸數據給本身時,數據讀取完成後,調用回調函數onRead
。
下面是這兩個回調函數的實現,它的功能很簡單:
hello\n
給對方。private void onRead(SocketChannel socketChannel) throws IOException { ByteBuffer buffer = ByteBuffer.allocateDirect(1024); int count; while ((count = socketChannel.read(buffer)) > 0) { buffer.flip(); while (buffer.hasRemaining()) { socketChannel.write(buffer); } buffer.clear(); } if (count < 0) { socketChannel.close(); } } private void onAccept(SocketChannel channel) throws IOException { System.out.println(channel.socket().getInetAddress() + "/" + channel.socket().getPort()); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); buffer.put("hello\n".getBytes()); buffer.flip(); channel.write(buffer); }
從上面的代碼能夠看出:
上面經過一個小DEMO,也就是一個簡單的ECHO服務器演示了NIO編程。下面來測試下結果:
frapples:~ ✔> nc -nvv 127.0.0.1 4040 Connection to 127.0.0.1 4040 port [tcp/*] succeeded! hello jfldjfl jfldjfl jfldjflieu jfldjflieu jfldhgldjfljdl jfldhgldjfljdl
效果不錯!不過這還沒完。
嘗試開啓多個終端,同時鏈接服務器,你會驚訝的發現,服務器可以完美的同時和多個客戶端鏈接而不會出現「卡死」的狀況。
回顧剛纔的小DEMO咱們能夠發現,剛纔的DEMO是 單線程 的,可是經過多路複用模型,卻能同時處理多個IO操做。
以前在博文《異步IO和同步IO》中也提到了一些異步IO的操做系統機制。
非阻塞IO須要操做系統機制的支持,在linux系統上,對應的是select/poll系統調用或epoll系統調用。
操做系統的做用之一是對硬件設備的管理,咱們發現,負責運算的部件CPU和負責網絡傳輸的部件網卡,它們是互相獨立的,所以,它們實際上能夠同時執行任務。那麼,底層硬件的支持使得徹底能夠作到如下步驟:
這裏有個小小的問題,在讀取數據的時候,上面的步驟網卡讀取數據時顯然是不經過CPU的。以我我的有限的硬件知識推測,非阻塞IO的機制可能須要用到DMA。
仍然是我的推測,之後有時間去查閱相關資料去解決這個疑惑。
咱們能夠看到,硬件的運做方式自然就是異步的,也所以,操做系統也很是容易基於此進行抽象和封裝,向上提供非阻塞的IO系統調用。
linux操做系統的系統調用提供了多路複用的非阻塞IO的系統調用,這也是java NIO機制實現須要用到的。
在linux2.6以前,採用select/poll系統調用實現,而在linux2.6以後,採用epoll實現,使用紅黑樹優化過,也所以性能更高。
本篇博文梳理的java的NIO機制,這是一種多路複用模型,可以使用一個線程去管理多個IO操做,避免傳統同步IO的線程開銷,大大提高性能。
從我我的的觀點,評判一種模型是否易用,一方面來看該模型是否與實際的問題特色相契合;另一方面,看該模型須要開發者花多少成本在模型自己上而非業務邏輯上。
從這個標準出發,咱們也不難發現,自己異步IO的回調方式就夠讓開發者頭疼的了,然而和異步IO相比,NIO比異步IO還要麻煩。
你須要花大量精力去時間去處理,去理解NIO自己的邏輯。所以,NIO的缺點是較高的開發成本和較晦澀的代碼,不優雅。
NIO在SOA框架,RPC框架等服務器領域有着較大的應用,除了java標準庫的NIO以外,這些實際生產的框架多使用第三方的NIO框架Netty。
緣由之一是,java標準庫的NIO有一個bug,可能形成CPU 100%的佔用。
今天,是我在公司實習呆的最後一天,我花了一個下午的時間去組織這篇博文。
感謝個人老大對個人器重和信任,給予我不少的機會去鍛鍊,也給予了我很大的自由空間去研究技術,自我提高。
也感謝這段時間對我照顧,給予我幫助的同事們,祝福大家!
注:該文於2018-04-13撰寫於個人github靜態頁博客,現同步到個人segmentfault來。