java I/O工作機制
摘要:IO問題可以說是當今web應用中面臨的主要問題之一。因為在這個數據爆發的時代,海量的數據在網絡到處流動,而在這個過程中都會涉及IO問題,可以說IO問題已經成為web應用的瓶頸之一。如何優化?以此提高效率,了解IO的工作機制就顯得尤為重要了。讀《深入分析JavaWeb技術內幕》隨筆,如果給你帶來了思考或者是幫助,推薦一下!歡迎指教。
一、Java I/O 類庫基本架構
java的io類庫在java.io包下,大概將近80個類,按照功能大致可以分為一下4組:
-
- 基於字節操作的IO接口:InputStream/OutputStream
- 基於字符操作的IO接口:Writer/Reader
- 基於磁盤操作的IO接口:File
- 基於網絡操作的IO接口:Socket
前兩組按照數據傳輸的格式劃分,後兩組按照數據傳輸的方式劃分。(雖然Socket不是java.io包下類庫,但是它和數據的傳輸方式息息相關,因此在此一起分析)
1.1 基於字節的IO操作接口
基於字節操作的IO接口分別是InputStream,OutputStream. InputStream的類層次結構如下:
輸入根據操作類型和操作方式又被分為若幹個子類,每個子類分別對應不同的數據類型和操作方式。OutputStream的類層次結構如下:
由於本篇不是講解IO的具體操作,而是站在更高的抽象位置分析java IO,所以就不講如何使用了。但是需要說明的是,java IO 不僅僅是操作本地磁盤的文件,也可以把網絡傳輸作為數據的目的地。
1.2 基於字符的IO操作接口
不管是磁盤傳輸還是網絡傳輸,最小的存儲單元都是字節,而不是字符。因此java IO操作的也是字節,之所以有字符的操作接口,完全是為了方便我們在程序中的使用,因為我們平時處理的多是字符類型的數據。底層存儲的依然是以字節為單位。由於字節到字符的的轉換涉及到編碼問題,所以我們在使用的過程中一定註意字符編碼的問題。
輸出字符的操作接口Writer的類層次結構如下:
輸入字符的接口Reader的類層次結構如下:
1.3 字節與字符的轉換接口
數據持久化或者是網絡傳輸都是以字節為單位的,但是我們在程序中處理的卻是字符問題,所有必須要有字節和字符相互轉換的類庫。字符解碼的相關類的層次結構如下:
InputStreamReader是字節到字符的轉換橋梁,從InputStream 到Reader 的過程中需要指定字符的編碼格式,否則采用系統默認的話很可能出現亂碼。
相應的寫操作也有響應轉化接口,OutputStreamWriter的類層次結構如下:
OutputStreamWriter 完成字符到字節的轉換操作。
二、磁盤IO工作機制
2.1 程序訪問文件的幾種方式
程序關於IO的操作——讀寫文件都是都是通過系統調用的方式來工作的,因為磁盤設備是由操作系統(OS)維護的,讀寫文件通過系統調用的read()和write()方式,但是只要是系統調用,就會存在內核空間和用戶空間地址的轉換和數據的復制,如果IO請求很大的時候,就會造成系統緩慢,為此操作系統引入了緩存的概念。以此提高IO的響應時間。那麽訪問文件的方式有哪些呢?一一道來。
a)標準訪問文件的方式
當程序調用read() 接口時,OS檢查緩存中是否有目標數據,有直接返回,沒有的話從磁盤讀取並放入緩存。write()的時候,OS將數據從用戶空間復制到內核空間的緩存中,此時對於用戶來說寫入已經完成,接下來由OS決定何時持久化,如需立即同步,顯示調用sync;
b)直接IO的方式
程序直接訪問磁盤數據而不經過OS內核的高速緩存,減少了用戶空間到內核空間的復制,這樣的應用的程序通常是自身控制數據的緩存機制,它自己知道哪些數據應該內緩存,哪些不用。甚至可以提前把熱點數據加載到內存中,以此來提高訪問的效率。典型的實現是數據庫管理系統。
c)同步訪問文件的方式
數據的讀取和寫入都是同步的,它與標準方式訪問文件不同的是,只有當數據成功持久化到磁盤後才返回,這樣大大的降低了系統性能只有對數據安全性考慮較高的情況下使用。
d)異步訪問文件的方式
當數據訪問的線程發出後,線程轉而去做其他的事情,而不是阻塞等待。這種方式可以明顯的提高系統的效率,但是不能根本的解決數據訪問的問題。
e) 內存映射訪問的方式
OS將內存中的某一塊區域與磁盤的文件相關聯起來,將訪問內存中的數據轉換為訪問文件的數據,這種方式減少了數據復制的操作。
2.2 java訪問磁盤文件
前面介紹的java IO的操作接口,主要定義了如何操作數據,並將數據持久化到某個目的地。比如說以持久化到本地磁盤為例:磁盤中最小的數據描述是文件,因為我們只有通過讀取文件來操作磁盤的數據,在java中,使用File類來和磁盤文件打交道,File類是一種抽象,不僅僅是代表的本地磁盤的一個文件,還有可能是包含多個文件的目錄,當我們在程序中 new File ("path") 時,其實並不關心這個文件是否有效,何時關心?當我們執行 new FIleInputStream() 對象真正操作的時候,才真正關心這個文件是否真的可用。
以一個程序從磁盤讀取一段文本字符為例,說明java訪問磁盤數的過程:
代碼示例:
public class ReadFileDemo { public static void main(String[] args) { // 此時的程序並不關心這個文件是否存在 File file = new File("D:/test.txt"); // 緩存數組,防止文件過大 char[] c = new char[1024]; Reader reader = null; StringBuffer str = new StringBuffer(); try { /** * 當我們創建 FileInputStream 對象真正操作文件時,才真正關心文件的有效性 因此拋出運行時異常 FileNotFoundException */ reader = new InputStreamReader(new FileInputStream(file)); // 真正的開始讀取文件時。也會拋出 IOException while (reader.read(c) != -1) { str.append(c); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (reader != null) reader.close(); } catch (IOException e) { e.printStackTrace(); } } System.out.println(str); } }
2.3 java序列化技術
java序列化是將一個對象轉換為二進制表示的字節數組,通過保存或者轉移達到對象持久化的目的。某個java對象需要序列化,那麽需要實現java.io.Serializable 接口,反序列化則是相反的過程,但是我們需要有原始類作為模板,才能將對象反序列化。但我們在應用中使用序列化技術時,需要註意幾個問題:
-
- 當父類繼承了Serializable 接口,那麽它的子類都可以被序列化。
- 當子類實現了Serializable 接口,但是父類沒有實現時,父類的屬性不能被序列化(不會報錯,但是數據會丟失),子類屬性可以正常序列化。
- 如果序列化的屬性是對象,那麽這個對象也必須實現Serializable 接口,否則會報錯。
- 在反序列化時,如果對象的屬性有變化或者修改,則修改的部分屬性會丟失,但是不會報錯。
- 在反序列化時,如果序列化的ID修改了,那麽反序列化也會失敗。
在純java環境中,序列化可以很好地工作,但是在多語言環境下,java序列化的對象用其他語言很難還原,因此盡量使用通用的數據結構,如json,xml,或者其他的序列化技術。
三、網絡IO工作機制
數據從一臺主機發送到網絡中的另一臺主機的過程,首先需要和目標主機建立起連接,和使用相同的通信協議和交流語言,才能完成數據的傳輸。因此在介紹java 中Socket傳輸數據之前,需要先討論一下網絡。
如何建立和關閉一個TCP連接,TCP的連接狀態轉換有哪些?
3.1 TCP連接狀態轉換
序號 | 狀態 | 描述 |
1 | CLOSED | 起始點,連接超時或者連接關閉時,進入此狀態。 |
2 | LISTEN | Server端在等待連接時的狀態,Server端為此要調用Socket,bind,listen。稱為被動打開。 |
3 | SYN-SENT | Client發送SYN給Server,如果不能連接那麽直接進入CLOSED狀態。 |
4 | SYN-RCVD | 與3對應,Server端接收Client的SYN,由LISTEN變為此狀態,並給Client回應一個 ACK,一個SYN.另外一種情況是Client在等待Server的ACK的時候,同時受到Server的SYN,那麽Client也會進入此狀態。 |
5 | ESTABLISHED | Server與Client在完成三次握手後進入的狀態,此時說明已經可以傳輸數據了。 |
6 | FIN-WAIT-1 | 主動發起關閉的一方,由狀態5進入此狀態,具體動作是發送FIN給對方 |
7 | FIN-WAIT-2 | 主動關閉的一方,在收到對方FIN-ACK後進入此狀態。此時不能再接收對方的數據,但是可以向對方發送數據 |
8 | CLOSED-WAIT | 收到FIN後,被動關閉的一方進入此狀態,動作是收到FIN的同時給對方回應ACK |
9 | LAST-ACK | 被動關閉的一方,發起關閉請求,由8進入此狀態,動作是發送FIN給對方,同時在接收到ACK時進入CLOSED狀態 |
10 | CLOSING | 兩邊同時發起關閉請求時,會由FIN-WAIT-1進入此狀態,動作是收到FIN請求,同時響應一個ACK. |
11 | TIME-WAIT | 可由三種狀態轉換為此狀態。 |
哪幾種情況會讓TCP連接進入TIME-WAIT狀態?
1. 雙方不同時發起FIN的情況下,由FIN-WAIT-2 轉換為TIME-WAIT,主動關閉的一方在完成自身發起的請求後,接收到被動一方的FIN後進入的狀態。
2. 雙方同時關閉的情況下,由CLOSING轉換到TIME-WAIT,雙方都發起了FIN,同時接收到了FIN並回應ACK的情況下,進入TIME-WAIT狀態。
3. 同時收到FIN和ACK,它與上面情況不同的是本身發起的FIN回應的ACK先於對方的FIN請求到達。
3.2 影響網絡傳輸的因素
將一臺主機中的數據傳到網絡中的另一臺主機鎖需要的時間我們叫做響應時間,影響這個響應時間的因素有很多,常見的有如下幾種:
a).網絡帶寬:帶寬就是一條物理鏈路在1s內能夠傳輸的最大比特數。註意是比特,而不是字節。
b).傳輸距離:傳輸的距離,國內和國外肯定是不一樣的。
c).TCP擁堵控制
四、java Socket的工作機制
Socket的概念沒有對應到一個具體的實體,它描述的是計算機之間相互通信的一種抽象功能。一般情況下,我們使用的 Socket都是基於TCP/IP的流套接字,它是一種穩定的通信協議。下圖是一個典型的基於Socket通信的場景。
上圖中主機A要和主機B建立通信,必須通過Socket建立通信,而在Socket連接底層是基於TCP/IP的TCP連接,TCP連接通過IP在網絡中尋找目標主機,主機上不同的應用程序又通過本地端口號來區分,那麽基於套接字(192.168.10.2:8080)這樣的ip:port 創建的唯一Socket就代表主機上一個應用程序的通信鏈路了。
4.1 建立通信鏈路
當Client需要與Server通信的時候,Client首先建立一個指定套接字的Socket示例,此時操作系統將為這個Socket示例分配一個包含本地地址,遠程地址,端口號的數據結構,這個數據結構將一直保存到當前連接關閉。在創建Socket示例的構造函數正確返回前,會進行TCP的3次握手協議,握手完成後,正確返回Socket,否則將拋出異常IOException;
與之對應的Server將創建一個ServerSocket示例,創建ServerSocket比較簡單,只要保證指定監聽的端口號沒有被占用,一般都沒有問題。同樣的操作系統也會其分配一個數據結構(但是不同的是,在沒有Client連接之前,該數據結構只包含了監聽的端口號和匹配的地址符)。然後調用accept()進入阻塞狀態,等待Client的連接;當Client請求的時候,此時操作系統為該連接實例分配一個數據結構並放在未完成的數據結構列表中,註意,此時的套接字數據結構並沒有完成,還需要等到Client的3次握手完成後,Server端的Socket實例才能返回,並從未完成的列表中移動到已經完成的列表中。所以ServerSocket關聯的列表中,每一個數據結構都代表了一個Client的連接實例。
4.2 數據傳輸
建立連接的目的是傳輸數據,那麽連接成功後,數據傳輸的基本流程和方式是在怎麽樣的呢?
當連接成功後,Server和Client都有都會擁有一個Socket實例,每個Socket實例都有一個InputStream和OutputStream,並通過通過這兩個對象來交換數據。同時網絡IO是以字節流來傳輸數據的,當創建Socket對象時,系統會為InputStream和OutputStream分配一個緩沖區,數據的寫入和讀取都是通過這個緩沖區完成的。寫入端將數據寫入到OutputStream的SendQ隊列中,當隊列滿時,數據將被轉移到另一端InputStream的RevcvQ中,如果這個RecvQ也滿了,那麽OutputStream將會的write方法將會阻塞,知道RecvQ隊列有空閑的空間容納SendQ發送的數據。需要註意的是,這個緩沖區的大小及寫入端和讀取端的速度直接影響了這個連接的傳輸效率,由於可能會發生阻塞,所以網絡IO和磁盤IO最大的不同就是數據的寫入和讀取還需要一個協調的過程。如果兩邊同時傳輸數據就很可能會造成死鎖。NIO解決了數據傳輸阻塞的問題。
欲知後事如何,請聽下回分解。
java I/O工作機制