本章先將Java中IO類型和底層的實現方式。
一、Java IO
Java IO即Java 輸入輸出系統。在Java IO中,流從概念上來說是一個連續的數據流,既可以從流中讀取數據,也可以往流中寫數據。IO相關的媒介包括:
- 文件
- 管道
- 網絡連接
- 內存緩存
- System.in, System.out
IO的設計,主要是解決IO相關的操作。從數據傳輸的方式上,分爲字節流和字符流。字節流一次性讀取傳輸一個字節,而字符流則是以字符爲單位進行讀取傳輸。
- 文件類型:FileInputStream,FileOutputStream、FileReader、FileWriter
- 數組類型:ByteArrayInputStream、ByteArrayOutputStream,CharArrayReader, CharArrayWriter
- 管道操作:PipedInputStream, PipedOutputStream, Pipedreader, PipedWriter
- 基本數據類型:DataInputStream、DataOutputStream
- 緩衝操作:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
- 打印:PrintStream、PrintWriter
- 對象序列化反序列化:ObjectInputStream、ObjectOutputStream
- 轉換:InputStreamReader、OutputStreWriter
二、什麼是NIO
NIO即new IO,是在JDK1.4引入的,NIO和IO有相同的作用和目的,但實現方式不同,NIO主要用到的是塊,所以NIO的效率要比IO高很多。
NIO的核心對象包括:
- Buffer:在NIO中,所有的數據都是用Buffer處理的,它是NIO讀寫數據的中轉池。Buffer實質上是一個數組,通常是一個字節數據,但也可以是其他類型的數組。
- Channel:是一個對象,可以通過channel讀取和寫入數據,是IO中流的抽象。但是channel是雙向的,也可以是異步讀寫,並且channel讀寫必須通過buffer。
- Selector:是一個對象,可以同時監聽多個channel上發生的事件,並且能夠根據事件情況決定Channel讀寫。
// 打開Selector
Selector selector = Selector.open();
// 將channel註冊到selector
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Selector感興趣的事件有SelectionKey.OP_CONNECT, SelectionKey.OP_ACCEPT, SelectionKey.OP_READ, SelectionKey.OP_WRITE。
SelectionKey表示通道channel在Selector上的註冊,事件的傳遞是通過SelectionKey,也可以通過selectionKey獲取註冊的channel和對應綁定的selector。
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
一旦向selector註冊一個或者多個通道後,就可以調用重載的select()方法,select()方法會返回讀事件已經就緒的那些通道
- int select():阻塞到至少有一個通道的事件就緒
- int select(long timeout):與select一樣,多個一個超時時間
- int selectNow():不會阻塞,不管什麼通道就緒都立刻返回,如果沒有通道可選擇,就返回0.
一旦調用了select()
方法,它就會返回一個數值,表示一個或多個通道已經就緒,然後你就可以通過調用selector.selectedKeys()
方法返回的SelectionKey集合來獲得就緒的Channel。某個線程調用select()方法後阻塞了,即使沒有通道就緒,也有辦法讓其從select()方法返回。
- 讓其它線程在調用select方法的對象上調用
Selector.wakeup()
方法即可,阻塞在select()方法上的線程會立馬返回。 - 如果其它線程調用了wakeup(),但是當前沒有線程阻塞在select上,下一個調用select阻塞的線程會被立即喚醒。
三、BIO, NIO, AIO的區別於聯繫
阻塞和非阻塞
- 阻塞操作時,當前線程會處於阻塞狀態,無法進行其他任務,只有當滿足一定條件時,才繼續執行;
- 非阻塞:非阻塞狀態,不會去等待IO操作結束,會立即返回。 線程通常將非阻塞IO的空閒時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。
BIO:同步並阻塞,服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善。BIO方式適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,但程序直觀簡單易理解。
NIO:同步非阻塞,服務器實現模式爲一個請求一個線程,即客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有I/O請求時才啓動一個線程進行處理。NIO方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,併發侷限於應用中,編程比較複雜,JDK1.4開始支持。
AIO:異步非阻塞,服務器實現模式爲一個有效請求一個線程,客戶端的I/O請求都是由OS先完成了再通知服務器應用去啓動線程進行處理.AIO方式使用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與併發操作,編程比較複雜,JDK7開始支持。
四、Java NIO和Netty
直接使用Java NIO的缺點:
- NIO的類庫和API繁雜,需要熟練掌握Selector,ServerSocketChannel、SocketChannel、ByteBuffer等才能很好使用。
- 可靠性較弱,需要自行維護,工作量大,例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等問題;
- JDK NIO的BUG,例如epoll bug,它會導致Selector空輪詢,最終導致CPU 100%。
Netty是對Java NIO的封裝框架,簡化了NIO的使用難度,Netty特性總結如下:
- API使用簡單,開發門檻低
- 功能強大,預置了多種編解碼功能,支持多種主流協議
- 定製能力強,可以通過ChannelHandler對通信框架進行靈活地擴展
- 性能高,通過與其他業界主流的NIO框架對比,Netty的綜合性能最優
- 成熟、穩定,Netty修復了已經發現的所有JDK NIO BUG,業務開發人員不需要再爲NIO的BUG而煩惱
- 社區活躍,版本迭代週期短,發現的BUG可以被及時修復,同時更多的新功能會加入
- 經歷了大規模的商業應用考驗,質量得到驗證。
五、select、poll、epoll之間的區別
select具有O(n)的無差別輪詢複雜度,同時處理的流越多,無差別輪詢時間就越長。
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd對應的設備狀態, 但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的。
epoll可以理解爲event poll,是事件驅動(每個事件關聯上fd)的。
select:
select本質上是通過設置或者檢查存放fd標誌位的數據結構來進行下一步處理。這樣所帶來的缺點是:
1、 單個進程可監視的fd數量被限制,即能監聽端口的大小有限。
一般來說這個數目和系統內存關係很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
2、 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低:
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字註冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。
3、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大
poll:
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。
它沒有最大連接數的限制,原因是它是基於鏈表來存儲的,但是同樣有一個缺點:
1、大量的fd的數組被整體複製於用戶態和內核地址空間之間,而不管這樣的複製是不是有意義。
2、poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。
epoll有EPOLLLT和EPOLLET兩種觸發模式,LT是默認的模式,ET是“高速”模式。epoll的優點:
1、沒有最大併發連接的限制,能打開的FD的上限遠大於1024(1G的內存上能監聽約10萬個端口);
2、效率提升,不是輪詢的方式,不會隨着FD數目的增加效率下降。只有活躍可用的FD纔會調用callback函數;
3、用MMP加速內核與用戶空間的消息傳遞。
即Epoll最大的優點就在於它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環境中,Epoll的效率就會遠遠高於select和poll。
EPOLLLT模式下,系統中一旦有大量不需要讀寫的就緒文件描述符,它們每次調用epoll_wait都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率.。
採用EPOLLET這種邊沿觸發模式的話,當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩衝區太小),那麼下次調用epoll_wait()時,它不會通知,直到該文件描述符上出現第二次可讀寫事件纔會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符