Java1.4以前的早期版本,Java對I/O的支持並不完善,開發人員再開發高性能I/O程序的時候,會面臨一些巨大的挑戰和困難,主要有如下一些問題:java
(1)沒有數據緩衝區,I/O性能存在問題正則表達式
(2)沒有C或者C++中的Channel概念,只有輸入和輸出流編程
(3)同步阻塞式I/O通訊(BIO),一般會致使通訊線程被長時間阻塞服務器
(4)支持的字符集有限,硬件可移植性很差網絡
本篇從底層的Linux操做系統I/O模型的系統調用開始提及,逐步介紹Java的I/O的發展及相關核心概念。多線程
Linux的內核將全部外部設備都看做一個文件來操做,對一個文件的讀寫操做都會調用內核提供的系統命令,返回一個file descriptor(fd,文件描述符)。而對一個socket的讀寫也會有相應的描述符,稱爲socketfd(socket描述符),描述符就是一個數字,它指向內核中的一個結構體(文件路徑,數據區等一些屬性)。併發
根據UNIX網絡編程對I/O模型的分類,UNIX提供了5種I/O模型,分別以下:異步
(1)阻塞I/O模型:最經常使用的I/O模型就是阻塞I/O模型,缺省情形下,全部文件操做都是缺省的。以套接字接口爲例來說解此模型:在進程空間中調用recvfrom,其系統調用直到數據包到達且被複制到應用進程的緩衝區中或者發生錯誤時才返回,在此期間一直會等待,進程在從調用recvfrom 開始到它返回的整段時間都是被阻塞的,所以被稱爲阻塞I/O模型。socket
(2)非阻塞I/O模型:recvfrom從應用層到內核的時候,若是該緩衝區沒有數據的話,就直接返回一個EWOULDBLOCK錯誤,通常都對非阻塞I/O模型進行輪詢檢查這個狀態,看內核是否是有數據到來。函數
(3)I/O複用模型:Linux提供select/poll,進程經過將一個或多個fd傳遞給select或poll系統調用,阻塞在select操做上,這樣select/poll能夠幫助咱們偵測多個fd是否處於就緒狀態。select/poll是順序掃描fd是否就緒,並且支持的fd數量有限,所以它的使用受到了一些制約。Linux還提供了一個epoll系統調用,epoll使用基於事件驅動方式代替順序掃描,所以性能高。當有fd就緒時,當即回調函數rollback。
(4)信號驅動I/O模型:首先開啓套接口信號驅動I/O功能,並經過系統調用sigaction執行一個信號處理函數(此係統調用當即返回,進程繼續工做,它是非阻塞的)。當數據準備就緒時,就爲該進程生成一個SIGIO信號,經過信號回調通知應用程序調用recvfrom來讀取數據,並通知主循環函數處理數據。
(5)異步I/O:告知內核啓動某個操做,並讓內核在整個操做完成後(包括將數據從內核複製到用戶本身的緩衝區)通知咱們。這種模型與信號驅動模型的主要區別是:信號驅動I/O由內核通知咱們能夠開始一個I/O操做;異步I/O模型由內核通知咱們I/O操做合適操做完成。
從上述咱們能夠知道,其實操做系統對於異步IO是支持的,只不過Java在很長的一段時間並無提供異步IO通訊的類庫。因爲Java NIO的核心類庫多路複用器Selector就是基於epoll的多路複用技術實現,下面會重點分析IO多路複用技術。
在I/O編程過程當中,當須要同時處理多個客戶端接入請求時,能夠利用多線程或者I/O多路複用技術進行處理。I/O多路複用技術經過把多個I/O的阻塞複用到同一個select的阻塞上,從而使得系統在單線程的狀況下能夠同時處理多個客戶端請求。與傳統的多線程/多進程模型比,I/O多路複用的最大優點是系統開銷小,系統不須要建立新的額外進程或者線程,也不須要維護這些進程和線程的運行,降底了系統的維護工做量,節省了系統資源,I/O多路複用的主要應用場景以下:
(1)服務器須要同時處理多個處於監聽狀態或者多個鏈接狀態的套接字。
(2)服務器須要同時處理多種網絡協議的套接字。
目前支持I/O多路複用的系統調用有 select,pselect,poll,epoll,在Linux網絡編程過程當中,很長一段時間都使用select作輪詢和網絡事件通知,然而select的一些固有缺陷致使了它的應用受到了很大的限制,最終Linux不得不在新的內核版本中尋找select的替代方案,最終選擇了epoll。epoll與select的原理比較相似,爲了克服select的缺點,epoll做了不少重大改進,現總結以下:
(1)支持一個進程打開的socket描述符(FD)不受限制(僅受限於操做系統的最大文件句柄數):
select最大的缺陷就是單個進程所打開的FD是有必定限制的,它由FD_SETSIZE設置,默認值是1024。對於那些須要支持上萬個TCP鏈接的大型服務器來講顯然太少了。能夠選擇修改這個宏,而後從新編譯內核,不過這會帶來網絡效率的降低。咱們也能夠經過選擇多進程的方案(傳統的Apache方案)解決這個問題,不過雖然在Linux上建立進程的代價比較小,但仍舊是不可忽視的,另外,進程間的數據交換很是麻煩,對於Java因爲沒有共享內存,須要經過Socket通訊或者其餘方式進行數據同步,這帶來了額外的性能損耗,增長了程序複雜度,因此也不是一種完美的解決方案。值得慶幸的是,epoll並無這個限制,它所支持的FD上限是操做系統的最大文件句柄數,這個數字遠遠大於1024。例如,在1GB內存的機器上大約是10萬個句柄左右,具體的值能夠經過cat/proc/sys/fs/filemax察看,一般狀況下這個值跟系統的內存關係比較大。
(2) I/O效率不會隨着FD數目的增長而線性降低:
傳統的select/poll另外一個致命弱點就是當你擁有一個很大的socket集合,因爲網絡延時或者鏈路空閒,任一時刻只有少部分的socket是「活躍」的,可是select/poll每次調用都會線性掃描所有集合,致使效率呈現線性降低。epoll不存在這個問題,它只會對「活躍」的socket進行操做-這是由於在內核實現中epoll是根據每一個fd上面的callback函數實現的,那麼,只有「活躍」的socket纔會主動的去調用callback函數,其餘idle狀態socket則不會。在這點上,epoll實現了一個僞AIO。針對epoll和select性能對比的benchmark測試代表:若是全部的socket都處於活躍態。例如一個高速LAN環境,epoll並不比select/poll效率高太多;相反,若是過多使用epoll_ctl,效率相比還有稍微的降低。可是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。
(3)使用mmap加速內核與用戶空間的消息傳遞:
不管是select,poll仍是epoll都須要內核把FD消息通知給用戶空間,如何避免沒必要要的內存複製就顯得很是重要,epoll是經過內核和用戶空間mmap使用同一塊內存實現。
(4) epoll的API更加簡單:
用來克服select/poll缺點的方法不僅有epoll,epoll只是一種Linux的實現方案。在freeBSD下有kqueue,而dev/poll是最古老的Solaris的方案,使用難度依次遞增。但epoll更加簡單。
在JDK1.4推出Java NIO以前,基於Java的全部Socket通訊都採用了同步阻塞模式(BIO),這種一請求一應答的通訊模型簡化了上層的應用開發,可是在性能和可靠性方面卻存在着巨大的瓶頸。所以,在很長一段時間裏,大型的應用服務器都採用C或者C++語言開發,由於它們能夠直接使用操做系統提供的異步I/O或者AIO能力。當併發訪問量增大、響應時間延遲增大以後,採用Java BIO開發的服務端軟件只有經過硬件的不斷擴容來知足高併發和延時,極大地增長了企業的成本,而且隨着集羣規模的不斷膨脹,系統的可維護性也面臨巨大的挑戰,只能經過採購性能更高的硬件服務器來解決問題,這會致使惡性循環。
正是因爲Java傳統BIO的拙劣表現,才使得Java支持非阻塞I/O的呼聲日漸高漲,最終,JDK1.4版本提供了新的NIO類庫,Java終於也能夠支持非阻塞I/O了。
JDK1.4推出 NIO 1.0 。新增java.nio包,提供了不少進行異步I/O開發的API和類庫,主要的類和接口以下。
新的NIO類庫的提供,極大地促進了基於Java的異步非阻塞編程的發展和應用,可是,它依然有不完善的地方,特別是對文件系統的處理能力仍顯不足,主要問題以下。
JDK1.7推出 NIO 2.0。主要提供了以下三個方面的改進。
參考文獻:
《Netty權威指南》
《UNIX網絡編程》
https://my.oschina.net/u/3729778/blog/1798339