詳解Java輸入輸出數據流模型和Web應用程式開發

2019-12-23   道以致遠



前言

Web應用開發架構技術不斷的演化,從基於通用網關接口CGI開發的能夠在作業系統上運行的獨立組件,到專門的隔離運行的Servlet,一直到到現在我們對複雜應用開發使用Java EE技術或者Spring框架系列,其實其底層的核心邏輯基本上沒有什麼變化。

從總體上說,我們的基於網絡數據流處理的應用程式開發,從單個組件到組合應用再到今天我們將組合應用垂直分割,使其功能趨於過去單組件狀態。

這種從分到合在到分,但是底層技術還是原來的對等點通信。

在從單個組件到集成容器管理多組件並採用前端控制模式的演化過程中,正是由於高級語言運行環境應用比如JVM等的出現,讓我們的組件開發進入了一個受託管,受限制時代。

如此也給了我們一個對運行的作業系統環境進行封裝和隔離管理時代,正是這種封裝隔離管理,讓開發人員在使用像Java這類高級語言時,能夠不用去關心要編寫的應用程式跟不同的作業系統資源之間的交互。

應用程式與計算機硬體

這裡簡單講一下,如果我們要開發一個應用程式需要跟系統的哪些資源進行交互和使用它們呢?

很明顯,我們編寫的任何代碼都是通過CPU和內存來執行的,它們是任何應用程式都必須與之打交道的資源。

除了它們還有,我們的應用程式都是以某種文件結構存儲的,所以需要跟作業系統的文件系統和文件的輸入輸出來打交道。

如果我們編寫的是Web應用,也就是基於網絡數據流通信的應用,那麼就需要跟計算機的網絡資源交互。

而這些資源的使用本質就是數據在這些資源的驅動程序的作用下由作業系統來調度來回的進出拷貝操作。



高級語言數據類型與計算機資源抽象

理解了我們應用程式是如何使用計算機資源後,我們就想明白了,其實我們所有的應用程式無非就是處理數據通過跟作業系統交互來通過作業系統管理的系統資源來進行各類數據處理操作。

首先是對於CPU資源的利用,當然必須配合內存空間的分配和利用。這些資源根據作業系統的實現和管理規則,我們將他們抽象成一些重要的描述類型,比如Thread,Buffer等。我們作業系統的重要環境參數也都出現在比如System等類或者包里。

而對於數據,我們一般將其或分為兩大類,一類是存儲在磁碟上的數據,由作業系統提供的文件系統來管理,它的特點是以固定大小的塊來存儲,所以作業系統在調度處理它時也是以塊為基礎進行的。而另外一類則是來自於外部動態的數據,這類數據以位元組流的方式呈現給作業系統,比如我們的網絡訪問數據流。

為了對這些資源進行管理和控制操作,高級語言抽象出流Stream的概念,這個流最基礎的是位元組流也就是比特數據流,它也是我們網絡硬體層傳輸的數據格式。

關於數據流模型

這個流實質上是對內存一個空間的操作抽象,在對Stream的抽象中其基本的操作讀read和寫write,基本的屬性是容量Capacity,位置Position和限制Limit,其中read和write方法就是告訴CPU去做輸入還是輸出操作。

我們在內存中開闢一塊總長度為Capacity的空間,這塊空間可以放東西的起始位置Position是多少,從這個位置開始有Limit的空間可用。每次操作都是由某個指針來計數標示的。當然我們可以通過其它一些指針標示來記錄這個空間的狀態信息,還可以恢復到某個狀態等。

我們知道作業系統管理的文件系統中存儲的數據都是以固定塊大小存儲在磁碟上的,所以我們一般會將輸入輸出的單位定義成磁碟塊大小的倍數。

如此來方便從磁碟向內存的輸入和輸出操作。作業系統會將有限的物理內存地址通過分頁的形式映射到磁碟固定地址區域分為,通過中斷機制不斷的將磁碟映射的內容調入到物理內存中已提供給CPU計算使用,這就是我們說的計算機虛擬內存技術。

我們在用Java這類高級語言開發應用程式時,涉及到對於文件的讀取和寫入操作,對於網絡數據流的讀取和寫入操作時,一般都是必須先創建這樣一個流模型。

就是在內存中開闢一個緩存的空間,將磁碟文件或者網絡數據流寫入到該緩存空間中,如果還需要進一步的處理,可以開闢更多的緩存空間,在空間轉移過程中對數據進行各類的處理,比如根據某種規則對數據的長度和標記位的規定來編解碼,對數據內容進行過濾和安全校驗等。



高級語言類型與數據流動模型

從我們計算機的硬體結構上來說,CPU,寄存器,主內存,輸入輸出控制器緩存,硬碟,鍵盤,網卡等它們之間通過各類數據總線進行數據輸入和輸出活動。

正是這些數據在各個硬體中間的進出交流才構成了我們應用程式的邏輯計算實現。為了能夠描述它們之間的數據流動,高級語言抽象出了IO模型,從最初的阻塞式輸入輸出模型,比如Java IO,到後來的非阻塞式IO,比如Java NIO/NIO.2以及AIO等。

這些數據模型最基本的是流Stream分為輸入數據流和輸出數據流,它們原生的數據流就是二進位比特流,為了能夠符合我們人類世界的內容表示,我們對其長度和固定數位進行了一些結構化的抽象,也就是我們常用的各種數據類型,包括一些常見數據類型的組合和嵌套,更抽象出對象世界。

其本質就是對於二進位數據流的數位長度規定,數位固定意義的賦予這些操作。

Java語言的I/O模型

隨著Java語言的發展,到了1.4後,開始提出了一個新的輸入輸出模型NIO,它跟原來IO模型的最大區別在於引入了通道Channel和選擇器Selector概念,不再是直接操作流Stream了而是在流的基礎上抽象出通道的概念,基本上就是在系統級別上獨立出一個管理線程,不斷的監控多個內存空間,如果哪個空間數據準備就緒,就會分配CPU去處理該空間的數據。如果沒有,CPU可以不理具體的IO操作,並且現代計算機硬體中提供DMA技術,一些輸入輸出操作可能都不需要CPU的參與。

這樣做的好處是使用極小的CPU線程資源可以管理多個輸入輸出的內存空間資源。

而不是想IO模型中,當我們應用需要一個IO操作時,整個IO操作線程會被阻塞等待IO操作的完成,如果應用程式涉及到大量的輸入輸出操作,由於IO的速度跟CPU跟其高級緩存之間的數據交換速度完全不在一個數量級上,所以這種線程阻塞等待會極大的降低CPU的利用率。

NIO模型最重要的是進一步抽象各種數據流為通道,並且增加了一個獨立的選擇器線程,並且將在數據流動過程中可能出現的比如連接,發送,接收,中斷異常,關閉等行為封裝成事件,通過事件的觸發來告訴CPU對其處理,從而避免了具體IO線程對於CPU資源的長期占用,只藉助輸入輸出控制設備就能完成很多工作。



數據流分類

當然上面談到的數據流並沒有進一步劃分基於什麼的數據流,其實我們知道基於文件塊數據流和基於流的數據流是有差別的。

我們在使用Java語言編程時,要操作某個文件首先需要建立一個基於該文件的數據流,然後在這個流的基礎上創建一個通道,對通道進行指令操作。

不管是讀取還是寫入,其另一端必然是一個內存中開闢的緩衝區,這個內存區域一般是在JVM管理的堆上的,由我們應用程式控制和管理的,但是真正的輸入輸出操作是需要作業系統來完成的,所以作業系統會有自己的緩存空間來進行數據的拷貝,然後將其在拷貝到我們應用程式管理的空間裡,在作業系統緩存到我們的應用程式管理空間的數據轉移就會涉及到編解碼問題。

在過去基於較低級別語言比如C/C++的編程中,我們是需要自己去定義不同的內存空間,並在這些內存空間之間進行數據轉移,而在較低級別語言中內存是屬於系統內存,也就是說我們操作的內存就是作業系統使用的內存,如果不對其進行有效的自我管理,就很容易造成系統的崩潰問題。

而在像高級語言Java等編程時,這些內存空間時JVM管理的堆空間,在我們不在引用這些數據時,有專門的垃圾收集器來根據負責根據需要清理它們。

網絡數據流

同樣基於網絡的數據是以數據流的方式來處理的,這個數據流我們將其抽象為Socket格式,也就是遵循TCP/IP或者UDP協議,它們規定了分割數據流的長度和標記,以及各部分的意義。

TCP與UDP區別在於一個數據流里有目標地址,另一個沒有,一個是定點發送,另一個算是廣播。而對於網絡中相互關聯的對等點計算機是通過InetAddress來抽象的表示的。

而我們的Socket本質就是數據流,它支持讀取和寫入操作,在新的NIO模型中,它有被抽象出了對應的通道,比如ServerSocketChannel,SocketChannel和DatagramChannel等。

我們編寫網絡應程序的底層邏輯處理一般就是先使用網絡節點主機的抽象信息創建一個網絡數據流,即Socket對象,然後通過它來讀取或者寫入數據,在NIO模型里,我們就可以通過Socket對象來獲取其通道對象。

該通道對象分為伺服器套接字通道對象和套接字通道對象,伺服器套接字通道只是一個具備管理能力的套接字通道類。如此有了套接字通道以後,我們就可是通過通道的相關方法來操作網絡數據流。

現在很少有直接通過JDK提供的這些類庫來編寫網絡應用了,目前比較流行的是使用第三方封裝這類網絡通道模型的功能庫比如Netty等。

總結

感覺還沒有說多少就又超過可接受的文章長度了,只能先說到這裡吧。簡單總結一下,其實要想將Web應用程式從最初的系統組件服務到現在的微服務架構的實現歷程說明白是很困難的。但是不管如何其底層有一個不變的主線就是數據的輸入/輸出模型,它是我們開發基於網絡數據流的應用程式的根基。所以,一定要清楚I/O模型的前世今生,以及現在流行的反應式流模型,如此才能更好的使用現在流行的框架和內容開發出高性能的網絡應用程式。