深入分析 I/O 的工作機制
不管是磁盤還是網(wǎng)絡傳輸,最小的存儲單元都是字節(jié),而不是字符,所以I/O操作的都是字節(jié)而不是字符。但是我們的程序中通常操作的數(shù)據(jù)都是字符形式的。
基于字節(jié)的I/O操作接口輸入和輸出分別是InputStream和OutputStream
寫字符的I/O操作接口涉及的是write(char[] buf, int off, int len)
讀字符的I/O操作是read(char[] buf, int off, int len)
字節(jié)與字符的轉化接口
數(shù)據(jù)持久化或網(wǎng)絡傳輸都是以字節(jié)進行的,所以必須要有字符到字節(jié)或者字節(jié)到字符的轉化。
幾種訪問文件的方式
讀取和寫入文件I/O操作都調用操作系統(tǒng)提供的接口,因為磁盤設備是由操作系統(tǒng)管理的,應用程序要訪問物理設備只能通過系統(tǒng)調用的方式來工作。
只要是系統(tǒng)調用就可能存在內核空間地址和用戶空間地址切換的問題,這是操作系統(tǒng)為了保護系統(tǒng)本身的運行安全而將內核程序運行使用的內存空間和用戶程序運行的內存空間隔離造成的。這樣雖然保證了內核程序運行的安全性,但是也必然存在數(shù)據(jù)可能需要從內核空間向用戶空間復制的問題。
如果遇到非常耗時的操作,如磁盤I/O,數(shù)據(jù)從磁盤復制到內核空間,然后又從內核空間復制到用戶空間,將會非常緩慢。這時操作系統(tǒng)為了加速I/O訪問,在內核空間使用緩存機制,也就是將從磁盤讀取的文件按照一定的組織方式進行緩存。
標準訪問文件方式
當應用程序調用read()接口時,操作系統(tǒng)檢查內核的告訴緩存中有沒有需要的數(shù)據(jù)。如果已經(jīng)緩存了,那么就直接從緩存中返回;如果沒有,從磁盤中讀取,然后緩存在操作系統(tǒng)的緩存中。
當應用程序調用write()接口時,將數(shù)據(jù)從用戶地址空間復制到內核地址空間的緩存中。這時,對用戶程序來說,寫操作就已經(jīng)完成了,至于什么時候再寫到磁盤中是有操作系統(tǒng)決定的,除非顯式地調用sync同步命令
直接I/O方式
應用程序直接訪問磁盤數(shù)據(jù),而不經(jīng)過操作系統(tǒng)內核數(shù)據(jù)緩沖區(qū),這樣做的目的就是減少一次從內核緩沖區(qū)到用戶程序緩存的數(shù)據(jù)復制。
這種訪問文件的方式通常是在對數(shù)據(jù)的緩存管理由應用程序實現(xiàn)的數(shù)據(jù)庫管理程序中。(如數(shù)據(jù)庫管理系統(tǒng)中,系統(tǒng)明確地知道應該緩存哪些數(shù)據(jù),應該失效哪些數(shù)據(jù),還可以對一些熱點數(shù)據(jù)做預加載,提前將熱點數(shù)據(jù)加載到內存,可以加速數(shù)據(jù)的訪問效率;而操作系統(tǒng)并不知道哪些是熱點數(shù)據(jù),只是簡單地緩存最近一次從磁盤讀取的數(shù)據(jù))
缺點:如果訪問的數(shù)據(jù)不在應用程序緩存中,那么每次數(shù)據(jù)都會直接從磁盤加載。這種直接加載會非常緩慢。
同步訪問文件方式
數(shù)據(jù)的讀取和寫入都是同步操作的,它與標準訪問文件方式不同的是,只有當數(shù)據(jù)被成功寫到磁盤時才返回給應用程序成功標志。
這種訪問文件方式性能比較差,只有在一些對數(shù)據(jù)安全性要求比較高的場景中才會使用,而且通常這種操作方式的硬件都是定制的。
異步訪問文件方式
當訪問數(shù)據(jù)的線程發(fā)出請求之后,線程會接著去處理其他事情,而不是阻塞等待,當請求的數(shù)據(jù)返回后繼續(xù)處理下面的操作。這種訪問文件的方式可以明顯地提高應用程序的效率,但是不會改變訪問文件的效率。
內存映射方式
內存映射方式是指操作系統(tǒng)將內存中的某一塊區(qū)域與磁盤中的文件關聯(lián)起來,當要訪問內存中一段數(shù)據(jù)時,轉換為訪問文件的某一段數(shù)據(jù)。這種方式的目的同樣是減少數(shù)據(jù)從內核空間緩存到用戶空間緩存的數(shù)據(jù)復制操作,因為這兩個空間的數(shù)據(jù)是共享的。
Java訪問磁盤文件
數(shù)據(jù)在磁盤中的唯一最小描述就是文件,也就是說上層應用程序只能通過文件來操作磁盤上的數(shù)據(jù),文件也是操作系統(tǒng)和磁盤驅動器交互的最小單元。
Java中通常的File并不代表一個真實存在的文件對象,當你指定一個路徑描述符時,它就會返回一個代表這個路徑的一個虛擬對象,這個可能是一個真實存在的文件或者是一個包含多個文件的目錄。
如何從磁盤讀取一段文本字符:
當傳入一個文件路徑時,將會根據(jù)這個路徑創(chuàng)建一個File對象來標識這個文件,然后根據(jù)這個File對象創(chuàng)建真正讀取文件的操作對象,這時將會真正創(chuàng)建一個關聯(lián)真實存在的磁盤文件的文件描述符FileDescriptor,通過這個對象可以直接控制這個磁盤文件。
由于我們需要讀取的是字符格式,所以需要StreamDecoder類將byte解碼為char格式。
Java序列化
Java序列化就是將一個對象轉化成一串二進制表示的字節(jié)數(shù)組,通過保存或轉移這些字節(jié)數(shù)據(jù)來達到持久化的目的。需要持久化,對象必須繼承java.io.Serializable接口。
反序列化則是相反的過程,將這個字節(jié)數(shù)組再重新構造成對象。
網(wǎng)絡I/O工作機制
TCP狀態(tài)轉化

1、CLOSED:起始點,在超時或者連接關閉時進入此狀態(tài)
2、LISTEN:Server端在等待連接時的狀態(tài),Server端為此要調用Scok
影響網(wǎng)絡傳輸?shù)囊蛩?/span>
將一份數(shù)據(jù)從一個地方正確地傳輸?shù)搅硪粋€地方所需要的時間我們稱為響應時間。影響這個響應時間的因素有很多。
網(wǎng)絡帶寬
傳輸距離
TCP擁塞控制
TCP傳輸是一個停-等-停-等協(xié)議,傳輸放和接受方的步調要一致,要達到這個步調一致就要通過擁塞控制來調節(jié)。TCP在傳輸時會設定一個窗口(BDP,Brandwidth Delay Product),這個窗口的大小是由帶寬和RTT(Round-Trip Time,數(shù)據(jù)在兩端的來回時間,也就是響應時間)決定的。計算的公式是帶寬(b/s)?*?RTT(s)。通過這個值可以得出理論上最優(yōu)的TCP緩沖區(qū)的大小。
Java Socket的工作機制
Socket描述計算機之間完成相互通信的一種抽象功能。
打個比方,可以把Socket比作兩個城市之間的交通工具,有了它,就可以在城市之間來回穿梭了、交通工具有多種,每種交通工具也有相應的交通規(guī)則。Socket也一樣,也有多種。大部分情況我們使用的是基于TCP/IP的流套接字,它是一種穩(wěn)定的通信協(xié)議。

主機A的應用程序要能和主機B的應用程序通信,必須通過Socket建立連接,而建立Socket連接必須由底層TCP/IP協(xié)議來建立TCP連接。建立TCP連接需要底層IP協(xié)議來尋址網(wǎng)絡中的主機。網(wǎng)絡層使用的IP協(xié)議可以幫助我們根據(jù)IP地址來找到目標主機,但是一臺主機上可能運行著多個應用程序,如何才能與指定的應用程序通信就要通過TCP或UDP的地址,也就是端口號來指定了。
建立通信鏈路
當客戶端要與服務端通信時,客戶端首先要創(chuàng)建一個Socket實例,操作系統(tǒng)將為這個Socket實例分配一個沒有被使用的本地端口號,并創(chuàng)建一個包含本地和遠程地址和端口號的套接字數(shù)據(jù)結構,這個數(shù)據(jù)結構將一直保存在系統(tǒng)中直到這個連接關閉。
在創(chuàng)建Socket實例的構造函數(shù)正確返回之前,將要進行TCP的三次握手協(xié)議,TCP握手協(xié)議完成后,Socket實例對象將創(chuàng)建完成,否則將拋出IOException錯誤。
數(shù)據(jù)傳輸
當連接已經(jīng)建立成功,服務端和客戶端都會擁有一個Socket實例,每個Socket實例都有一個InputStream和OutputStream,并通過這兩個對象來交換數(shù)據(jù)。
當創(chuàng)建Socket對象時,操作系統(tǒng)將會為InputStream和OutputStream分別分配一定大小的緩存區(qū),數(shù)據(jù)的寫入和讀取都是通過這個緩存區(qū)完成的。
寫入端將數(shù)據(jù)寫到OutputStream對應的SendQ隊列中,當隊列填滿時,數(shù)據(jù)將被轉移到另一端InputStream的RecvQ隊列中,如果這時RecvQ已經(jīng)滿了,那么OutputStream的write方法將會阻塞知道RecvQ隊列有足夠的空間容納SendQ發(fā)送的數(shù)據(jù)。
NIO的工作方式
BIO帶來的挑戰(zhàn)
BIO即阻塞IO,不管是磁盤IO還是網(wǎng)絡IO,數(shù)據(jù)在寫入OutputStream或者從InputStream讀取時都有可能會阻塞,一旦有阻塞,線程將會失去CPU的使用權。
NIO的工作機制

這里的Channel可以比作某種具體的交通工具,如汽車或高鐵;
而Selector可以比作一個車站的車輛運行調度系統(tǒng),它將負責監(jiān)控每輛車的當前運行狀態(tài),是已經(jīng)出站,還是在路上的。也就是它可以輪訓每個Channel的狀態(tài)。
Buffer可以比作車上的座位。信息已經(jīng)封裝在了Socket里面,對你是透明的。
public void selector() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if ((key.readOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel sc = ssChannel.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
it.remove();
}
}
}
}
調用
Selector的靜態(tài)工廠創(chuàng)建一個選擇器創(chuàng)建一個服務端的
Channel,綁定到一個Socket對象,并把這個通信信道注冊到選擇器上,把這個通信信道設置為非阻塞模式然后就可以調用
Selector的selectedKeys方法來檢查已經(jīng)注冊在這個選擇器上的所有通信信道是否有需要的事件發(fā)生。如果有某個事件發(fā)生,將會返回所有的
SelectionKey,通過這個對象Channel方法就可以取得這個通信信道對象,從而可以讀取通信的數(shù)據(jù)而這里讀取的數(shù)據(jù)是
Buffer,這個Buffer是我們可以控制的緩沖器
Selector可以同時監(jiān)聽一組通信信道(Channel)上的IO狀態(tài),前提是這個Selector已經(jīng)注冊到這些通信信道中。選擇器
Selector可以調用select()方法檢查已經(jīng)注冊的通信信道上IO是否已經(jīng)準備好,如果沒有一個信道IO狀態(tài)有變化,那么select方法會阻塞等待或在超時后返回0。如果有多個信道有數(shù)據(jù),那么將會把這些數(shù)據(jù)分配到對應的數(shù)據(jù)
Buffer中。所以關鍵的地方是,有一個線程來處理所有連接的數(shù)據(jù)交互,每個連接的數(shù)據(jù)交互都不是阻塞方式,所以可以同時處理大量的連接請求。
Buffer的工作方式
Selector檢測到通信信道IO有數(shù)據(jù)傳輸時,通過select()取得SocketChannel,將數(shù)據(jù)讀取或寫入Buffer緩沖區(qū)。
Buffer可以簡單地理解為一組基本數(shù)據(jù)類型的元素列表,它通過幾個變量來保存這個數(shù)據(jù)的當前位置狀態(tài),也就是有四個索引。
capacity:緩沖區(qū)數(shù)組的總長度position:下一個要操作的數(shù)據(jù)元素的位置limit:緩沖區(qū)數(shù)組中不可操作的下一個元素的位置,limit<=capacitymark:用于記錄當前position的前一個位置或者默認是0

我們通過ByteBuffer.allocate(11)方法創(chuàng)建一個11個byte的數(shù)組緩沖區(qū),初始狀態(tài)時,position為0,capactiy和limit默認都是數(shù)組長度。
當我們寫入5個字節(jié)時,位置變化如下:

這時,我們需要將緩沖區(qū)的5個字節(jié)數(shù)據(jù)寫入Channel通信信道,所以我們調用byteBuffer.flip()方法

這時,底層操作系統(tǒng)就可以從緩沖區(qū)中正確讀取這5個字節(jié)數(shù)據(jù),并發(fā)送出去了。在下一次寫數(shù)據(jù)之前,我們再調一下clear()方法,緩沖區(qū)的索引狀態(tài)又回到初始位置。
當我們調用mark()時,它將記錄當前position的前一個位置,當我們調用reset時,position將恢復mark記錄下來的值。
通過Channel獲取的IO數(shù)據(jù)首先要經(jīng)過操作系統(tǒng)的Socket緩沖區(qū)再將數(shù)據(jù)復制到Buffer中,這個操作系統(tǒng)緩沖區(qū)就是底層的TCP協(xié)議關聯(lián)的RecvQ或者SendQ隊列。
從操作系統(tǒng)緩沖區(qū)到用戶緩沖區(qū)復制數(shù)據(jù)比較耗性能,Buffer提供了另外一種直接操作操作系統(tǒng)緩沖區(qū)的方式,即ByteBuffer.allocateDirector(size),這個方法返回的DirectByteBuffer就是與底層存儲空間關聯(lián)的緩沖區(qū),它通過Native代碼操作非JVM堆的內存空間。
NIO的數(shù)據(jù)訪問方式
FileChannel.transferXXX
FileChannel.transferXXX與傳統(tǒng)的訪問文件方式相比可以減少數(shù)據(jù)從內核到用戶空間的復制,數(shù)據(jù)直接在內核空間中移動。


FileChannel.map
將文件按照一定大小映射為內存區(qū)域,當程序訪問這個內存區(qū)域時將直接操作這個文件數(shù)據(jù),這種方式省去了數(shù)據(jù)從內核空間向用戶空間復制的損耗。這種方式適合對大文件的只讀性操作,如大文件的MD5校驗。
適配器模式裝飾器模式的區(qū)別
適配器模式的意義是要將一個接口轉變成另外一個接口,它的目的是通過改變接口來達到重復使用的目的
裝飾器模式不是要改變被裝飾對象的接口,而是要保持原有的接口,但是增強原有對象的功能,或者改變原有對象的處理方式而提升性能。
source: //nathanchen.github.io/14588873943004.html
