摘要:依照該方案,虛擬內(nèi)存空間的頁(yè)面能夠繼續(xù)存在于外部磁盤存儲(chǔ),這樣就為物理內(nèi)存中的其他虛擬頁(yè)面騰出了空間。造成頁(yè)錯(cuò)誤的用戶進(jìn)程對(duì)此不會(huì)有絲毫察覺,一切都在不知不覺中進(jìn)行。虛擬內(nèi)存系統(tǒng)俘獲頁(yè)錯(cuò)誤,安排頁(yè)面調(diào)入,從磁盤上讀取頁(yè)內(nèi)容,使頁(yè)有效。
本筆記主要針對(duì)JAVA NIO第1-4章,做一下總結(jié),豆瓣評(píng)分7.5,但本人還是強(qiáng)烈推薦的.對(duì)JDK 1.4的NIO接口做了很充分的講解.
I/O概念所謂“I(輸入)/O(輸出)”講的無非就是把數(shù)據(jù)移進(jìn)或移出緩沖區(qū).
進(jìn)程執(zhí)行 I/O 操作,歸結(jié)起來,也就是向操作系統(tǒng)發(fā)出請(qǐng)求,讓它要么把緩沖區(qū)里的數(shù)據(jù)排干 (寫),要么用數(shù)據(jù)把緩沖區(qū)填滿(讀).
緩沖區(qū)操作
如上圖所示,進(jìn)程使用read( )系統(tǒng)調(diào)用,要求其緩沖區(qū)被填滿。內(nèi)核隨即向磁盤控制硬件發(fā)出命令,要求其從磁盤讀取數(shù)據(jù)。磁盤控制器把數(shù)據(jù)直接寫入內(nèi)核內(nèi)存緩沖區(qū),這一步通過 DMA 完成,無需主 CPU 協(xié)助。一旦磁盤控制器把緩沖區(qū)裝滿,內(nèi)核即把數(shù)據(jù)從內(nèi)核空間的臨時(shí)緩沖區(qū)拷貝到進(jìn)程執(zhí)行read( )調(diào)用時(shí)指定的緩 沖區(qū)。
注意圖中用戶空間和內(nèi)核空間的概念。用戶空間是常規(guī)進(jìn)程所在區(qū)域。JVM 就是常規(guī)進(jìn)程,駐守于用戶空間。用戶空間是非特權(quán)區(qū)域:比如,在該區(qū)域執(zhí)行的代碼就不能直接訪問硬件設(shè)備。 內(nèi)核空間是操作系統(tǒng)所在區(qū)域。內(nèi)核代碼有特別的權(quán)力:它能與設(shè)備控制器通訊,控制著用戶區(qū)域 進(jìn)程的運(yùn)行狀態(tài),等等。最重要的是,所有 I/O 都直接(如這里所述)或間接通過內(nèi)核空間。
為什么不直接 讓磁盤控制器把數(shù)據(jù)送到用戶空間的緩沖區(qū)呢?
1.硬件通常不能直接訪問 用戶空間. 2.像磁盤這樣基于塊存儲(chǔ)的硬件設(shè)備操作的是固定大小的數(shù)據(jù)塊,而用戶進(jìn)程請(qǐng) 求的可能是任意大小的或非對(duì)齊的數(shù)據(jù)塊。在數(shù)據(jù)往來于用戶空間與存儲(chǔ)設(shè)備的過程中,內(nèi)核負(fù)責(zé)數(shù)據(jù)的分解、再組合工作,因此充當(dāng)著中間人的角色
發(fā)散/匯聚
如上圖所示,發(fā)散/匯聚就是操作系統(tǒng)為了提升性能同時(shí)操縱多個(gè)緩存區(qū)的情況,這樣就不用多次執(zhí)行系統(tǒng)調(diào)用了.
虛擬內(nèi)存
虛擬內(nèi)存意為使用虛假(或虛擬)地址取代物理(硬件RAM)內(nèi)存地址
優(yōu)點(diǎn):
1.一個(gè)以上的虛擬地址可指向同一個(gè)物理內(nèi)存地址 2.虛擬內(nèi)存空間可大于實(shí)際可用的硬件內(nèi)存
利用一個(gè)以上的虛擬地址可指向同一個(gè)物理內(nèi)存地址把內(nèi)核空間地址與用戶空間的虛擬地址映射到同一個(gè)物理地址,這樣, DMA 硬件(只能訪問物理內(nèi)存地址)就可以填充對(duì)內(nèi)核與用戶空間進(jìn)程同時(shí)可見的緩沖區(qū)(上圖)。
前提條件是,內(nèi)核與用戶緩沖區(qū)必須使用相同的頁(yè)對(duì)齊,緩沖區(qū)的大小還必須是磁盤控制器塊大小(通常為 512 字節(jié)磁盤扇區(qū))的倍數(shù)。
內(nèi)存頁(yè)面調(diào)度
為了支持虛擬內(nèi)存的第二個(gè)特性(尋址空間大于物理內(nèi)存),就必須進(jìn)行虛擬內(nèi)存分頁(yè)(經(jīng)常稱為交換,雖然真正的交換是在進(jìn)程層面完成,而非頁(yè)層面)。依照該方案,虛擬內(nèi)存空間的頁(yè)面能夠繼續(xù)存在于外部磁盤存儲(chǔ),這樣就為物理內(nèi)存中的其他虛擬頁(yè)面騰出了空間。從本質(zhì)上說,物理內(nèi)存充當(dāng)了分頁(yè)區(qū)的高速緩存;而所謂分頁(yè)區(qū),即從物理內(nèi)存置換出來,轉(zhuǎn)而存儲(chǔ)于磁盤上的內(nèi)存頁(yè)面.
MMU:現(xiàn)代 CPU 包含一個(gè)稱為內(nèi)存管理單元(MMU)的子系統(tǒng),邏輯上位于 CPU 與物理內(nèi)存之間。該設(shè)備包含虛擬地址向物理內(nèi)存地址轉(zhuǎn)換時(shí)所需映射信息。當(dāng) CPU 引用某內(nèi)存地址時(shí),MMU負(fù)責(zé)確定該地址所在頁(yè)(往往通過對(duì)地址值進(jìn)行移位或屏蔽位操作實(shí)現(xiàn)),并將虛擬頁(yè)號(hào)轉(zhuǎn)換為物理頁(yè)號(hào)(這一步由硬件完成,速度極快)。如果當(dāng)前不存在與該虛擬頁(yè)形成有效映射的物理內(nèi)存頁(yè),MMU 會(huì)向CPU交一個(gè)頁(yè)錯(cuò)誤.
頁(yè)錯(cuò)誤隨即產(chǎn)生一個(gè)陷阱(類似于系統(tǒng)調(diào)用),把控制權(quán)移交給內(nèi)核,附帶導(dǎo)致錯(cuò)誤的虛擬地址信息,然后內(nèi)核采取步驟驗(yàn)證頁(yè)的有效性。內(nèi)核會(huì)安排頁(yè)面調(diào)入操作,把缺失的頁(yè)內(nèi)容讀回物理內(nèi)存。這往往導(dǎo)致別的頁(yè)被移出物理內(nèi)存,好給新來的頁(yè)讓地方。在這種情況下,如果待移出的頁(yè)已經(jīng)被碰過了(自創(chuàng)建或上次頁(yè)面調(diào)入以來,內(nèi)容已發(fā)生改變),還必須首先執(zhí)行頁(yè)面調(diào)出,把頁(yè)內(nèi)容拷貝到磁盤上的分頁(yè)區(qū)。
如果所要求的地址不是有效的虛擬內(nèi)存地址(不屬于正在執(zhí)行的進(jìn)程的任何一個(gè)內(nèi)存段),則該頁(yè)不能通過驗(yàn)證,段錯(cuò)誤隨即產(chǎn)生。于是,控制權(quán)轉(zhuǎn)交給內(nèi)核的另一部分,通常導(dǎo)致的結(jié)果就是進(jìn)程被強(qiáng)令關(guān)閉。
一旦出錯(cuò)的頁(yè)通過了驗(yàn)證,MMU隨即更新,建立新的虛擬到物理的映射(如有必要,中斷被移出頁(yè)的映射),用戶進(jìn)程得以繼續(xù)。造成頁(yè)錯(cuò)誤的用戶進(jìn)程對(duì)此不會(huì)有絲毫察覺,一切都在不知 不覺中進(jìn)行。
#### 文件I/O
文件 I/O 屬文件系統(tǒng)范疇,文件系統(tǒng)與磁盤迥然不同。磁盤把數(shù)據(jù)存在扇區(qū)上,通常一個(gè)扇區(qū)512字節(jié)。磁盤屬硬件設(shè)備,對(duì)何謂文件一無所知,它只是 供了一系列數(shù)據(jù)存取窗口。在這點(diǎn)上,磁盤扇區(qū)與內(nèi)存頁(yè)頗有相似之處:都是統(tǒng)一大小,都可作為大的數(shù)組被訪問。
文件系統(tǒng)是更高層次的抽象,是安排、解釋磁盤(或其他隨機(jī)存取塊設(shè)備)數(shù)據(jù)的一種獨(dú)特方式。您所寫代碼幾乎無一例外地要與文件系統(tǒng)打交道,而不是直接與磁盤打交道。是文件系統(tǒng)定義了文件名、路徑、文件、文件屬性等抽象概念。
采用分頁(yè)技術(shù)的操作系統(tǒng)執(zhí)行 I/O 的全過程可總結(jié)為以下幾步:
確定請(qǐng)求的數(shù)據(jù)分布在文件系統(tǒng)的哪些頁(yè)(磁盤扇區(qū)組)。磁盤上的文件內(nèi)容和元數(shù)據(jù)可能跨越多個(gè)文件系統(tǒng)頁(yè),而且這些頁(yè)可能也不連續(xù)。
在內(nèi)核空間分配足夠數(shù)量的內(nèi)存頁(yè),以容納得到確定的文件系統(tǒng)頁(yè)。
在內(nèi)存頁(yè)與磁盤上的文件系統(tǒng)頁(yè)之間建立映射。
為每一個(gè)內(nèi)存頁(yè)產(chǎn)生頁(yè)錯(cuò)誤。
虛擬內(nèi)存系統(tǒng)俘獲頁(yè)錯(cuò)誤,安排頁(yè)面調(diào)入,從磁盤上讀取頁(yè)內(nèi)容,使頁(yè)有效。
一旦頁(yè)面調(diào)入操作完成,文件系統(tǒng)即對(duì)原始數(shù)據(jù)進(jìn)行解析,取得所需文件內(nèi)容或?qū)傩孕畔ⅰ?/p>
內(nèi)存映射文件傳統(tǒng)的文件I/O是通過用戶進(jìn)程發(fā)布read()和write()系統(tǒng)調(diào)用來傳輸數(shù)據(jù)的。為了在內(nèi)核空間的文件系統(tǒng)頁(yè)與用戶空間的內(nèi)存區(qū)之間移動(dòng)數(shù)據(jù),一次以上的拷貝操作幾乎總是免不了的。這是因?yàn)?,在文件系統(tǒng)頁(yè)與用戶緩沖區(qū)之間往往沒有一一對(duì)應(yīng)關(guān)系。但是,還有一種大多數(shù)操作系統(tǒng)都支持的特殊類型的 I/O 操作,允許用戶進(jìn)程最大限度地利用面向頁(yè)的系統(tǒng)I/O特性,并完全摒棄緩沖區(qū)拷貝。這就是內(nèi)存映射 I/O,如圖下所示。
內(nèi)存映射 I/O 使用文件系統(tǒng)建立從用戶空間直到可用文件系統(tǒng)頁(yè)的虛擬內(nèi)存映射.
優(yōu)點(diǎn):
用戶進(jìn)程把文件數(shù)據(jù)當(dāng)作內(nèi)存,所以無需發(fā)布read()或write()系統(tǒng)調(diào)用。
當(dāng)用戶進(jìn)程碰觸到映射內(nèi)存空間,頁(yè)錯(cuò)誤會(huì)自動(dòng)產(chǎn)生,從而將文件數(shù)據(jù)從磁盤讀進(jìn)內(nèi)存。如果用戶修改了映射內(nèi)存空間,相關(guān)頁(yè)會(huì)自動(dòng)標(biāo)記為臟,隨后刷新到磁盤,文件得到更新。
操作系統(tǒng)的虛擬內(nèi)存子系統(tǒng)會(huì)對(duì)頁(yè)進(jìn)行智能高速緩存,自動(dòng)根據(jù)系統(tǒng)負(fù)載進(jìn)行內(nèi)存管理。
數(shù)據(jù)總是按頁(yè)對(duì)齊的,無需執(zhí)行緩沖區(qū)拷貝。
大型文件使用映射,無需耗費(fèi)大量?jī)?nèi)存,即可進(jìn)行數(shù)據(jù)拷貝
Buffer
Buffer對(duì)應(yīng)于上節(jié)所述概念中的緩存區(qū).包在一個(gè)對(duì)象內(nèi)的基本數(shù)據(jù)元素?cái)?shù)組。Buffer 類相比一個(gè)簡(jiǎn)單數(shù)組的優(yōu)點(diǎn)是它將關(guān)于數(shù)據(jù)的數(shù)據(jù)內(nèi)容和信息包含在一個(gè)單一的對(duì)象中。Buffer類以及它專有的子類定義了一個(gè)用于處理數(shù)據(jù)緩沖區(qū)的 API.有7種主要的緩沖區(qū)類,每一種都具有一種 Java 語言中的非布 類型的原始類型數(shù)據(jù)。
屬性:
//標(biāo)記:一個(gè)備忘位置。調(diào)用mark( )來設(shè)定mark=postion。調(diào)用reset( )設(shè)定position= mark。標(biāo)記在設(shè)定前是未定義的(undefined) private int mark = -1; //位置:下一個(gè)要被讀或?qū)懙脑氐乃饕?。位置?huì)自動(dòng)由相應(yīng)的get()和 put()函數(shù)更新。 private int position = 0; //上界:緩沖區(qū)的第一個(gè)不能被讀或?qū)懙脑?。或者說,緩沖區(qū)中現(xiàn)存元素的計(jì)數(shù)。 private int limit; //容量:緩沖區(qū)能夠容納的數(shù)據(jù)元素的最大數(shù)量。這一容量在緩沖區(qū)創(chuàng)建時(shí)被設(shè)定,并且 遠(yuǎn)不能被改變 private int capacity;
上述屬性有以下關(guān)系
0 <= mark <= position <= limit <= capacity
基本方法:
//返回容量 public final int capacity(); //返回位置 public final int position(); //設(shè)置容量 public final Buffer position(int newPosition); //返回上屆 public final int limit() ; //標(biāo)記當(dāng)前position為mark public final Buffer mark(); //重回mark位置 public final Buffer reset(); //一般在把數(shù)據(jù)寫入Buffer前調(diào)用 public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; } //翻轉(zhuǎn):將緩存字節(jié)數(shù)組的指針設(shè)置為數(shù)組的開始序列即數(shù)組下標(biāo)0。這樣就可以從buffer開頭,對(duì)該buffer進(jìn)行遍歷(讀?。┝? //buf.put(magic); Prepend header //in.read(buf); Read data into rest of buffer //buf.flip(); Flip buffer //out.write(buf); Write header + data to channel< public final Buffer flip() { limit = position; position = 0; mark = -1; return this; } // 一般在把數(shù)據(jù)重寫入Buffer前調(diào)用 // out.write(buf); // Write remaining data // buf.rewind(); // Rewind buffer // buf.get(array); // Copy data into array public final Buffer rewind() { position = 0; mark = -1; return this; } // limit - position public final int remaining(); // position < limit public final boolean hasRemaining(); public abstract boolean isReadOnly();
存取方法:
public abstract byte get( ); public abstract byte get (int index); public abstract ByteBuffer put (byte b); public abstract ByteBuffer put (int index, byte b);
壓縮方法:
//將position到limit之間的數(shù)據(jù)遷移至0開始處,然后limit=capacity public abstract ByteBuffer compact( );
比較方法
// true:兩個(gè)對(duì)象類型相同,兩個(gè)對(duì)象都剩余同樣數(shù)量的元素,在每個(gè)緩沖區(qū)中應(yīng)被Get()函數(shù)返回的剩余數(shù)據(jù)元素序列必須一致 public boolean equals (Object ob) // 比較是針對(duì)每個(gè)緩沖區(qū)內(nèi)剩余數(shù)據(jù)進(jìn)行的,與它們?cè)趀quals()中的方式相同,直到不相等的元素被發(fā)現(xiàn)或者到達(dá)緩沖區(qū)的上界。如果一個(gè)緩沖區(qū)在不相等元素發(fā)現(xiàn)前已經(jīng)被耗盡,較短 的緩沖區(qū)被認(rèn)為是小于較長(zhǎng)的緩沖區(qū) public int compareTo (Object ob)直接緩沖區(qū)
字節(jié)緩沖區(qū)跟其他緩沖區(qū)類型最明顯的不同在于,它們可以成為通道所執(zhí)行的 I/O 的源 頭和/或目標(biāo),只有字節(jié)緩沖區(qū)有資格參與 I/O 操作。
直接緩沖區(qū)被用于與通道和固有I/O例程交互。它們通過使用固有代碼來告知操作系統(tǒng)直接釋放或填充內(nèi)存區(qū)域,對(duì)用于通道直接或原始存取的內(nèi)存區(qū)域中的字節(jié)元素的存儲(chǔ)盡了最大的努力。
直接字節(jié)緩沖區(qū)通常是 I/O 操作最好的選擇。在設(shè)計(jì)方面,它們支持 JVM 可用的最高效 I/O 機(jī)制。非直接字節(jié)緩沖區(qū)可以被傳遞給通道,但是這樣可能導(dǎo)致性能耗。通常非直接緩沖不可能成為一個(gè)本地I/O操作的目標(biāo)。如果您向一個(gè)通道中傳遞一個(gè)非直接ByteBuffer對(duì)象用于寫入,通道可能會(huì)在每次調(diào)用中隱含地進(jìn)行下面的操作:
創(chuàng)建一個(gè)臨時(shí)的直接 ByteBuffer 對(duì)象
將非直接緩沖區(qū)的內(nèi)容復(fù)制到臨時(shí)緩沖區(qū)中。
使用臨時(shí)緩沖區(qū)執(zhí)行低層次 I/O 操作。
臨時(shí)緩沖區(qū)對(duì)象離開作用域,并最終成為被回的無用數(shù)據(jù)。
直接緩沖區(qū)使用的內(nèi)存是通過調(diào)用本地操作系統(tǒng)方面的代碼分配的, 過了標(biāo)準(zhǔn)JVM。建立和銷毀直接緩沖區(qū)會(huì)明顯比具有的緩沖區(qū)更加費(fèi),這取決于主操作系統(tǒng)以及JVM實(shí)現(xiàn)。直接緩沖區(qū)的內(nèi)存區(qū)域不受無用存儲(chǔ)單元 集支配,因?yàn)樗鼈兾挥跇?biāo)準(zhǔn)JVM之外。
public static ByteBuffer allocateDirect (int capacity);
Channel基本接口
public interface Channel extends Closeable { public boolean isOpen(); public void close() throws IOException; }FileChannel 文件通道
文件通道總是阻塞的,不能被置于非阻模式.現(xiàn)代操作系統(tǒng)都有復(fù)雜的緩存和預(yù)取機(jī)制,使得本地磁盤 I/O 操作延遲很少。ileChannel實(shí)例只能通過在一個(gè) 打開的file對(duì)象(RandomAccessFile、FileInputStream或FileOutputStream)上調(diào)用getChannel()方法獲取.
FileChannel 對(duì)象是線程安全(thread-safe)的。多個(gè)進(jìn)程可以在同一個(gè)實(shí)例上并發(fā)調(diào)用方法而 不會(huì)引起任何問題,不過并非所有的操作都是多線程的(multithreaded)。影響通道位置或者影響文件大小的操作都是單線程的(single-threaded)。如果有一個(gè)線程已經(jīng)在執(zhí)行會(huì)影響通道位置或文件大小的操作,那么其他試進(jìn)行此類操作之一的線程必須等待。并發(fā)行為也會(huì)受到底層的操作系 統(tǒng)或文件系統(tǒng)影響。
同大多數(shù) I/O 相關(guān)的類一樣,F(xiàn)ileChannel 是一個(gè)反映 Java 虛擬機(jī)外部一個(gè)具體對(duì)象的抽象。 FileChannel 類保證同一個(gè) Java 虛擬機(jī)上的所有實(shí)例看到的某個(gè)文件的 圖均是一致的,但是 Java 虛擬機(jī)卻不能對(duì)超出它控制范圍的因素 供 保。通過一個(gè) FileChannel 實(shí)例看到的某個(gè)文件的 圖同通過一個(gè)外部的非 Java 進(jìn)程看到的該文件的 圖可能一致,也可能不一致。多個(gè)進(jìn)程發(fā)起的并發(fā)文件訪問的語義高度取決于底層的操作系統(tǒng)和(或)文件系統(tǒng)。一般而言,由運(yùn)行在不同 Java 虛擬機(jī)上的 FileChannel 對(duì)象發(fā)起的對(duì)某個(gè)文件的并發(fā)訪問和由非Java進(jìn)程發(fā)起的對(duì)該文件的并發(fā) 訪問是一致的。
新的 FileChannel 類供了一個(gè)名為map()的方法,該方法可以在一個(gè)打開的文件和一個(gè)特殊類型的ByteBuffer之間建立一個(gè)虛擬內(nèi)存映射(第一章中已經(jīng)歸納了什么是內(nèi)存映射文件以及它們 如何同虛擬內(nèi)存交互)。在FileChannel 上調(diào)用 map()方法會(huì)創(chuàng)建一個(gè)由磁盤文件支持的虛擬內(nèi)存映射(virtual memory mapping)并在那塊虛擬內(nèi)存空間外部封裝一個(gè) MappedByteBuffer 對(duì)象.
由 map()方法返回的MappedByteBuffer對(duì)象的行為在多數(shù)方面類似一個(gè)基于內(nèi)存的緩沖區(qū),只不過該對(duì)象的數(shù)據(jù)元素存儲(chǔ)在磁盤上的一個(gè)文件中。調(diào)用 get()方法會(huì)從磁盤文件中獲取數(shù)據(jù),此數(shù)據(jù)反映該文件的當(dāng)前內(nèi)容,即使在映射建立之后文件已經(jīng)被一個(gè)外部進(jìn)程做了修改。通過文件映射看到的數(shù)據(jù)同您用常規(guī)方法讀取文件看到的內(nèi)容是完全一樣的。相似地,對(duì)映射的緩沖區(qū)實(shí)現(xiàn)一個(gè)put()會(huì)更新磁盤上的那個(gè)文件(假設(shè)對(duì)該文件您有寫的權(quán)限),并且您做的修改對(duì)于該文件的其他閱讀者也是可見的。
通過內(nèi)存映射機(jī)制來訪問一個(gè)文件會(huì)比使用常規(guī)方法讀寫高效得多,甚至比使用通道的效率都高。因?yàn)椴恍枰雒鞔_的系統(tǒng)調(diào)用,那會(huì)很消耗時(shí)間。更重要的是,操作系統(tǒng)的虛擬內(nèi)存可以自動(dòng) 緩存內(nèi)存頁(yè)(memory page)。這些頁(yè)是用系統(tǒng)內(nèi)存來緩存的,所以不會(huì)消耗 Java 虛擬機(jī)內(nèi)存 (memory heap). 一旦一個(gè)內(nèi)存頁(yè)已經(jīng)生效(從磁盤上緩存進(jìn)來),它就能以完全的硬件速度再次被訪問而不需 要再次調(diào)用系統(tǒng)命令來獲取數(shù)據(jù)。那些包含索引以及其他需頻繁引用或更新的內(nèi)容的巨大而結(jié)構(gòu)化文件能因內(nèi)存映射機(jī)制受益非常多。如果同時(shí)結(jié)合文件鎖定來保護(hù)關(guān)鍵區(qū)域和控制事務(wù)原子性,那您將能了解到內(nèi)存映射緩沖區(qū)如何可以被很好地利用。 MemoryMappedBuffer 直接反映它所關(guān)聯(lián)的磁盤文件。如果映射有效時(shí)文件被在結(jié)構(gòu)上修改, 就會(huì)產(chǎn)生奇 的行為(當(dāng)然具體的行為是取決于操作系統(tǒng)和文件系統(tǒng)的)。MemoryMappedBuffer 有固定的大小,不過它所映射的文件卻是彈性的。具體來說,如果映射有效時(shí)文件大小變化了,那么緩沖區(qū)的部分或全部?jī)?nèi)容都可能無法訪問,并將返回未定義的數(shù)據(jù)或者拋出未檢查的異常。關(guān)于被內(nèi)存映射的文件如何受其他線程或外部進(jìn)程控制這一點(diǎn),請(qǐng)務(wù)必小心對(duì)待。 所有的 MappedByteBuffer對(duì)象都是直接的,這意味著它們占用的內(nèi)存空間位于 Java虛擬機(jī)內(nèi)存之外(并且可能不會(huì)算作Java虛擬機(jī)的內(nèi)存占用,不過這取決于操作系統(tǒng)的虛擬內(nèi)存模型)。 因?yàn)?MappedByteBuffers 也是 ByteBuffers,所以能夠被傳遞 SocketChannel 之類通道的read()或write()以有效傳輸數(shù)據(jù)給被映射的文件或從被映射的文件讀取數(shù)據(jù)。
public abstract class MappedByteBuffer extends ByteBuffer { // 加載文件到物理內(nèi)存 public final MappedByteBuffer load( ) //是否已加載 public final boolean isLoaded( ) //將緩存區(qū)的更改寫入磁盤 public final MappedByteBuffer force( ) }
當(dāng)我們?yōu)橐粋€(gè)文件建立虛擬內(nèi)存映射之后,文件數(shù)據(jù)通常不會(huì)因此被從磁盤讀取到內(nèi)存(這取 決于操作系統(tǒng))。該過程類似打開一個(gè)文件:文件先被定位,然后一個(gè)文件句柄會(huì)被創(chuàng)建,當(dāng)您準(zhǔn)備好之后就可以通過這個(gè)句來訪問文件數(shù)據(jù)。對(duì)于映射緩沖區(qū),虛擬內(nèi)存系統(tǒng)將根據(jù)您的需要來把文件中相應(yīng)區(qū)塊的數(shù)據(jù)讀進(jìn)來。這個(gè)頁(yè)驗(yàn)證或防錯(cuò)過程需要一定的時(shí)間,因?yàn)閷⑽募?shù)據(jù)讀取到 內(nèi)存需要一次或多次的磁盤訪問。某些場(chǎng)下,您可能想先把所有的頁(yè)都讀進(jìn)內(nèi)存以實(shí)現(xiàn)最小的緩沖區(qū)訪問延遲。如果文件的所有頁(yè)都是常駐內(nèi)存的,那么它的訪問速度就和訪問一個(gè)基于內(nèi)存的緩沖區(qū)一樣了。
load()方法會(huì)加載整個(gè)文件以使它常駐內(nèi)存。正如我們?cè)诘谝徽滤懻摰?,一個(gè)內(nèi)存映射緩沖區(qū)會(huì)建立與某個(gè)文件的虛擬內(nèi)存映射。此映射使得操作系統(tǒng)的底層虛擬內(nèi)存子系統(tǒng)可以根據(jù)需要將文件中相應(yīng)區(qū)塊的數(shù)據(jù)讀進(jìn)內(nèi)存。已經(jīng)在內(nèi)存中或通過驗(yàn)證的頁(yè)會(huì)占用實(shí)際內(nèi)存空間,并且在它們 被讀進(jìn) RAM 時(shí)會(huì)擠出最近較少使用的其他內(nèi)存頁(yè)。
public class ChannelAccept { public static final String GREETING = "Hello I must be going. "; public static void main (String [] argv) throws Exception { int port = 1234; // default if (argv.length > 0) { port = Integer.parseInt (argv [0]); } ByteBuffer buffer = ByteBuffer.wrap (GREETING.getBytes( )); ServerSocketChannel ssc = ServerSocketChannel.open( ); ssc.socket( ).bind (new InetSocketAddress (port)); ssc.configureBlocking (false); while (true) { System.out.println ("Waiting for connections"); SocketChannel sc = ssc.accept( ); if (sc == null) { // no connections, snooze a while Thread.sleep (2000); } else { System.out.println ("Incoming connection from: " + sc.socket().getRemoteSocketAddress( )); buffer.rewind( ); sc.write (buffer); sc.close( ); } }ServerSocketChannel TCP服務(wù)器端
public class ConnectAsync { public static void main (String [] argv) throws Exception { String host = "localhost"; int port = 80; if (argv.length == 2) { host = argv [0]; port = Integer.parseInt (argv [1]); } InetSocketAddress addr = new InetSocketAddress (host, port); SocketChannel sc = SocketChannel.open( ); sc.configureBlocking (false); System.out.println ("initiating connection"); sc.connect (addr); while ( ! sc.finishConnect( )) { doSomethingUseful( ); } System.out.println ("connection established"); // Do something with the connected socket // The SocketChannel is still nonblocking sc.close( ); } private static void doSomethingUseful( ) { System.out.println ("doing something useless"); } }DatagramChannel UDP
public class TimeClient { private static final int DEFAULT_TIME_PORT = 37; private static final long DIFF_1900 = 2208988800L; protected int port = DEFAULT_TIME_PORT; protected List remoteHosts; protected DatagramChannel channel; public TimeClient(String[] argv) throws Exception { if (argv.length == 0) { throw new Exception("Usage: [ -p port ] host ..."); } parseArgs(argv); this.channel = DatagramChannel.open(); } protected InetSocketAddress receivePacket(DatagramChannel channel, ByteBuffer buffer) throws Exception { buffer.clear(); // Receive an unsigned 32-bit, big-endian value return ((InetSocketAddress) channel.receive(buffer)); } // Send time requests to all the supplied hosts protected void sendRequests() throws Exception { ByteBuffer buffer = ByteBuffer.allocate(1); Iterator it = remoteHosts.iterator(); while (it.hasNext()) { InetSocketAddress sa = (InetSocketAddress) it.next(); System.out.println("Requesting time from " + sa.getHostName() + ":" + sa.getPort()); // Make it empty (see RFC868) buffer.clear().flip(); // Fire and forget channel.send(buffer, sa); 112 } } // Receive any replies that arrive public void getReplies() throws Exception { // Allocate a buffer to hold a long value ByteBuffer longBuffer = ByteBuffer.allocate(8); // Assure big-endian (network) byte order longBuffer.order(ByteOrder.BIG_ENDIAN); // Zero the whole buffer to be sure longBuffer.putLong(0, 0); // Position to first byte of the low-order 32 bits longBuffer.position(4); // Slice the buffer; gives view of the low-order 32 bits ByteBuffer buffer = longBuffer.slice(); int expect = remoteHosts.size(); int replies = 0; System.out.println(""); System.out.println("Waiting for replies..."); while (true) { InetSocketAddress sa; sa = receivePacket(channel, buffer); buffer.flip(); replies++; printTime(longBuffer.getLong(0), sa); if (replies == expect) { System.out.println("All packets answered"); break; } // Some replies haven"t shown up yet System.out.println("Received " + replies + " of " + expect + " replies"); } } // Print info about a received time reply protected void printTime(long remote1900, InetSocketAddress sa) { // local time as seconds since Jan 1, 1970 113 long local = System.currentTimeMillis() / 1000; // remote time as seconds since Jan 1, 1970 long remote = remote1900 - DIFF_1900; Date remoteDate = new Date(remote * 1000); Date localDate = new Date(local * 1000); long skew = remote - local; System.out.println("Reply from " + sa.getHostName() + ":" + sa.getPort()); System.out.println(" there: " + remoteDate); System.out.println(" here: " + localDate); System.out.print(" skew: "); if (skew == 0) { System.out.println("none"); } else if (skew > 0) { System.out.println(skew + " seconds ahead"); } else { System.out.println((-skew) + " seconds behind"); } } protected void parseArgs(String[] argv) { remoteHosts = new LinkedList(); for (int i = 0; i < argv.length; i++) { String arg = argv[i]; // Send client requests to the given port if (arg.equals("-p")) { i++; this.port = Integer.parseInt(argv[i]); continue; } // Create an address object for the hostname InetSocketAddress sa = new InetSocketAddress(arg, port); // Validate that it has an address if (sa.getAddress() == null) { System.out.println("Cannot resolve address: " + arg); continue; } 114 remoteHosts.add(sa); } } public static void main(String[] argv) throws Exception { TimeClient client = new TimeClient(argv); client.sendRequests(); client.getReplies(); } }Selector
選擇器供選擇執(zhí)行已經(jīng)就緒的任務(wù)的能力,這使得多元I/O成為可能。就緒選擇和多元執(zhí)行使得單線程能夠有效率地同時(shí)管理多個(gè)I/O通道(channels).將之前創(chuàng)建的一個(gè)或多個(gè)可選擇的通道注冊(cè)到選擇器對(duì)象中。一個(gè)表示通道和選擇器的鍵將會(huì)被返回。選擇鍵會(huì)記您關(guān)心的通道。它們也會(huì)對(duì)應(yīng)的通道是否已經(jīng)就緒。當(dāng)您調(diào)用一個(gè)選擇器對(duì)象的 select()方法時(shí),相關(guān)的鍵建會(huì)被更新, 用來檢查所有被注冊(cè)到該選擇器的通道。
在與 SelectableChannel聯(lián)合使用時(shí),選擇器供了這種服務(wù),但這里面有更多的事情需要去了解。就緒選擇的真正價(jià)值在于潛在的大量的通道可以同時(shí)進(jìn)行就緒狀態(tài)的檢查。調(diào)用者可以輕松地決定多個(gè)通道中的哪一個(gè)準(zhǔn)備好要運(yùn)行。有兩種方式可以選擇:被激發(fā)的線程可以處于休眠狀態(tài),直到一個(gè)或者多個(gè)注冊(cè)到選擇器的通道就緒,或者它也可以周期性地詢選擇器,看看從上次檢查之后,是否有通道處于就緒狀態(tài)。如果您考慮一下需要管理大量并發(fā)的連接的網(wǎng)絡(luò)服務(wù)器(webserver)的實(shí)現(xiàn),就可以很容易地想到如何善加利用這些能力。
真正的就緒選擇必須由操作系統(tǒng)來做。操作系統(tǒng)的一項(xiàng)最重要的功能就是處理 I/O 請(qǐng)求并通知 各個(gè)線程它們的數(shù)據(jù)已經(jīng)準(zhǔn)備好了。選擇器類供了這種抽象,使得 Java 代碼能夠以可移植的方式,請(qǐng)求底層的操作系統(tǒng)供就緒選擇服務(wù)
Selector:
選擇器類管理著一個(gè)被注冊(cè)的通道集合的信息和它們的就緒狀態(tài)。通道是和選擇器一起被注冊(cè)的,并且使用選擇器來更新通道的就緒狀態(tài)。當(dāng)這么做的時(shí)候,可以選擇將被激發(fā)的線程掛起,直到有就緒的的通道。
public abstract class Selector implements Closeable { protected Selector() { } //實(shí)例化Selector public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); } public abstract SelectorProvider provider(); //返回 注冊(cè)到它們之上的通道的集合,不可以直接修改的 public abstract Setkeys(); //返回 就緒的鍵.已注冊(cè)的鍵的集合的子集,這個(gè)集合的每個(gè)成員都是相關(guān)的通道被選擇器(在前一個(gè)選擇操作中)斷為已經(jīng)準(zhǔn)備好的,并且包含于鍵的interest集合中的操作/ public abstract Set selectedKeys(); //非阻塞 public abstract int selectNow() throws IOException; //設(shè)定時(shí)間內(nèi)阻塞 public abstract int select(long timeout) throws IOException; //完全阻塞 public abstract int select() throws IOException; //停止阻塞中的select方法 public abstract Selector wakeup(); //測(cè)試一個(gè)選擇器是否處于被打開的狀態(tài) public abstract boolean isOpen(); //釋放它可能占用的資源并將所有相關(guān)的選擇鍵設(shè)置為無效,一個(gè)在選擇操作中阻 的線程都將被醒,就像wakeup()方法被調(diào)用了一樣。與選擇器相關(guān)的通道將被注銷,而鍵將被取消 public abstract void close() throws IOException; }
select()方法:
1. 已取消的鍵的集合將會(huì)被檢查。如果它是非空的,每個(gè)已取消的鍵的集合中的鍵將從另外兩個(gè)集合中移除,并且相關(guān)的通道將被注銷。這個(gè)步驟結(jié)束后,已取消的鍵的集合將是空的。 2. 已注冊(cè)的鍵的集合中的鍵的interest集合將被檢查。在這個(gè)步驟中的檢查執(zhí)行過后,對(duì)interest集合的改動(dòng)不會(huì)影響剩余的檢查過程。 一旦就緒條件被定下來,底層操作系統(tǒng)將會(huì)進(jìn)行查詢,以確定每個(gè)通道所關(guān)心的操作的真實(shí)就緒狀態(tài)。依賴于特定的select()方法調(diào)用,如果沒有通道已經(jīng)準(zhǔn)備好,線程可能會(huì)在這時(shí)阻塞,通常會(huì)有一個(gè)超時(shí)值。 直到系統(tǒng)調(diào)用完成為止,這個(gè)過程可能會(huì)使得調(diào)用線程一段時(shí)間,然后當(dāng)前每個(gè)通道的就緒狀態(tài)將確定下來。對(duì)于那些還沒準(zhǔn)備好的通道將不會(huì)執(zhí)行任何的操作。對(duì)于那些操作系統(tǒng)指示至少已經(jīng)準(zhǔn)備好interest集合中的一種操作的通道,將執(zhí)行以下兩種操作中的一種: - 如果通道的鍵還沒有處于已選擇的鍵的集合中,那么鍵的 ready 集合將被清空,然后表示操 作系統(tǒng)發(fā)現(xiàn)的當(dāng)前通道已經(jīng)準(zhǔn)備好的操作的比特 碼將被設(shè)置。 - 否則,也就是鍵在已選擇的鍵的集合中。鍵的ready集合將被表示操作系統(tǒng)發(fā)現(xiàn)的當(dāng)前已經(jīng)準(zhǔn)備好的操作的比特碼更新。所有之前的已經(jīng)不再是就緒狀態(tài)的操作不會(huì)被清除。事實(shí)上,所有的比特位都不會(huì)被清理。由操作系統(tǒng)決定的 ready 集合是與之前的ready集合按位分離的,一旦鍵被放置于選擇器的已選擇的鍵的集合中,它的ready集合將是的。比特位只會(huì)被設(shè)置,不會(huì)被清理。 3. 步驟2可能會(huì)花費(fèi)很長(zhǎng)時(shí)間,特別是所激發(fā)的線程處于休眠狀態(tài)時(shí)。與該選擇器相關(guān)的鍵可能會(huì)同時(shí)被取消。當(dāng)步驟2結(jié)束時(shí),步驟1將重新執(zhí)行,以完成任意一個(gè)在選擇進(jìn)行的過程中,鍵已經(jīng)被取消的通道的注銷 4. select操作返回的值是ready集合在步驟2中被修改的鍵的數(shù)量,而不是已選擇的鍵的集合中的通道的總數(shù)。返回值不是已準(zhǔn)備好的通道的總數(shù),而是從上一個(gè) select()調(diào)用之后進(jìn)入就緒狀態(tài)的通道的數(shù)量。之前的調(diào)用中就緒的,并且在本次調(diào)用中仍然就緒的通道不會(huì)被計(jì)入,而那些在前一次調(diào)用中已經(jīng)就緒但已經(jīng)不再處于就緒狀態(tài)的通道也不會(huì)被計(jì)入。這些通道可能仍然在已選擇的 鍵的集合中,但不會(huì)被計(jì)入返回值中。返回值可能是 0。
選擇是累積的。一旦一個(gè)選擇器將一個(gè)鍵加到它的已選擇的鍵的集合中,它就不會(huì)移除這個(gè)鍵。并且,一旦一個(gè)鍵處于已選擇的鍵的集合中,這個(gè)鍵的ready集合將只會(huì)被設(shè)置,而不會(huì)被清理。
合理地使用選擇器的是理解選擇器維護(hù)的選擇鍵集合所演的角色。最重要的部分是當(dāng)鍵已經(jīng)不再在已選擇的鍵的集合中時(shí)將會(huì)發(fā)生什么。當(dāng)通道上的至少一個(gè)感興趣的操作就緒時(shí),鍵的ready集合就會(huì)被清空,并且當(dāng)前已經(jīng)就緒的操作將會(huì)被加到ready集合中。該鍵之后將被 加到已選擇的鍵的集合中。
清理一個(gè) SelectKey的ready集合的方式是將這個(gè)鍵從已選擇的鍵的集合中移除(removed掉)。選擇鍵的就緒狀態(tài)只有在選擇器對(duì)象在選擇操作過程中才會(huì)修改。處理思想是只有在已選擇的鍵的集合中的鍵才被認(rèn)為是包含了合法的就緒信息的。這些信息將在鍵中長(zhǎng)久地存在,直到鍵從已選擇的鍵的集合中移除,以通知選擇器您已經(jīng)看到并對(duì)它進(jìn)行了處理。如果下一次通道的一些感興趣的操作發(fā)生時(shí),鍵將被重新設(shè)置以反映當(dāng)時(shí)通道的狀態(tài)并再次被 加到已選擇的鍵的集合中。
SelectableChannel:
FileChannel對(duì)象不是可選擇的,SocketChannel,SocketServerChannel,DatagramChannel是可選擇的.
public abstract class SelectableChannel extends AbstractChannel implements Channel{ // 注冊(cè)通道到Selector上,第二個(gè)參數(shù)表示所關(guān)心的通道操作 public abstract SelectionKey register (Selector sel, int ops) throws ClosedChannelException; // 同上,第三個(gè)參數(shù)attach到SelectionKey public abstract SelectionKey register (Selector sel, int ops,Object att) throws ClosedChannelException; public abstract boolean isRegistered( ); // 返回與該通道和指定的選擇器相關(guān)的鍵 public abstract SelectionKey keyFor (Selector sel); public abstract int validOps( ); //通道在被注冊(cè)到一個(gè)選擇器上之前,必須先設(shè)置為false,否則會(huì)拋出IllegalBlockingModeException public abstract void configureBlocking (boolean block) throws IOException; public abstract boolean isBlocking( ); public abstract Object blockingLock( ); }
SelectionKey:
選擇鍵封裝了特定的通道與特定的選擇器的注冊(cè)關(guān)系
public abstract class SelectionKey{ public static final int OP_READ public static final int OP_WRITE public static final int OP_CONNECT public static final int OP_ACCEPT //返回相關(guān)SelectableChannel public abstract SelectableChannel channel( ); //返回相關(guān) Selector public abstract Selector selector( ); //不會(huì)立即注銷。直到下一次操作發(fā)生為止,它們?nèi)匀粫?huì)處于被注冊(cè)的狀態(tài). public abstract void cancel( ); //是否仍然有效 public abstract boolean isValid( ); //返回 通道/ 選擇器組合體所關(guān)心的操作(instrest 集合) public abstract int interestOps( ); //修改 instrest 集合 public abstract void interestOps (int ops); // 通道準(zhǔn)備好要執(zhí)行的操作,ready集合是interest集合的子集,并且表示了interest集合中從上次調(diào)用select()以來已經(jīng)就緒的那些操作 //通過相關(guān)的選擇鍵的readyOps()方法返回的就緒狀態(tài)指示只是一個(gè) 示,不是保證。底層的通道在任何時(shí)候都會(huì)不斷改變。其他線程可能在通道上執(zhí)行操作并影響它的就緒狀態(tài)。 public abstract int readyOps( ); //是否可讀 public final boolean isReadable( ) //是否可寫 public final boolean isWritable( ) //是否可連接 public final boolean isConnectable( ) //是否可接受連接 public final boolean isAcceptable( ) //附上對(duì)象 public final Object attach (Object ob) //返回 附著的對(duì)象 public final Object attachment( ) }
1.建立選擇器
Selector selector = Selector.open( ); channel1.register (selector, SelectionKey.OP_READ); channel2.register (selector, SelectionKey.OP_WRITE); channel3.register (selector, SelectionKey.OP_READ |SelectionKey.OP_WRITE); // Wait up to 10 seconds for a channel to become ready readyCount = selector.select (10000);
上述代碼,建了一個(gè)新的選擇器,然后將這三個(gè)(已經(jīng)存在的)socket 通道注冊(cè)到選擇器上,而且感興趣的操作各不相同
2.完整的交互例子
import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class SelectSockets { public static int PORT_NUMBER = 1234; public static void main(String[] argv) throws Exception { new SelectSockets().go(argv); } public void go(String[] argv) throws Exception { int port = PORT_NUMBER; if (argv.length > 0) { // Override default listen port port = Integer.parseInt(argv[0]); } ServerSocketChannel serverChannel = ServerSocketChannel.open(); ServerSocket serverSocket = serverChannel.socket(); Selector selector = Selector.open(); serverSocket.bind(new InetSocketAddress(port)); serverChannel.configureBlocking(false); serverChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { int n = selector.select(); if (n == 0) { continue; } Iterator it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); // Is a new connection coming in? if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel = server.accept(); registerChannel(selector, channel, SelectionKey.OP_READ); sayHello(channel); } } } } protected void registerChannel(Selector selector, SelectableChannel channel, int ops) throws Exception { if (channel == null) { return; } channel.configureBlocking(false); channel.register(selector, ops); } private ByteBuffer buffer = ByteBuffer.allocateDirect(1024); protected void readDataFromSocket(SelectionKey key) throws Exception { SocketChannel socketChannel = (SocketChannel) key.channel(); int count; buffer.clear(); // Empty buffer while ((count = socketChannel.read(buffer)) > 0) { buffer.flip(); // Make buffer readable while (buffer.hasRemaining()) { socketChannel.write(buffer); } buffer.clear(); // Empty buffer } if (count < 0) { socketChannel.close(); } } private void sayHello(SocketChannel channel) throws Exception { buffer.clear(); buffer.put("Hi there! ".getBytes()); buffer.flip(); channel.write(buffer); } }
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/70246.html
摘要:從使用到原理學(xué)習(xí)線程池關(guān)于線程池的使用,及原理分析分析角度新穎面向切面編程的基本用法基于注解的實(shí)現(xiàn)在軟件開發(fā)中,分散于應(yīng)用中多出的功能被稱為橫切關(guān)注點(diǎn)如事務(wù)安全緩存等。 Java 程序媛手把手教你設(shè)計(jì)模式中的撩妹神技 -- 上篇 遇一人白首,擇一城終老,是多么美好的人生境界,她和他歷經(jīng)風(fēng)雨慢慢變老,回首走過的點(diǎn)點(diǎn)滴滴,依然清楚的記得當(dāng)初愛情萌芽的模樣…… Java 進(jìn)階面試問題列表 -...
摘要:大多數(shù)待遇豐厚的開發(fā)職位都要求開發(fā)者精通多線程技術(shù)并且有豐富的程序開發(fā)調(diào)試優(yōu)化經(jīng)驗(yàn),所以線程相關(guān)的問題在面試中經(jīng)常會(huì)被提到。將對(duì)象編碼為字節(jié)流稱之為序列化,反之將字節(jié)流重建成對(duì)象稱之為反序列化。 JVM 內(nèi)存溢出實(shí)例 - 實(shí)戰(zhàn) JVM(二) 介紹 JVM 內(nèi)存溢出產(chǎn)生情況分析 Java - 注解詳解 詳細(xì)介紹 Java 注解的使用,有利于學(xué)習(xí)編譯時(shí)注解 Java 程序員快速上手 Kot...
摘要:它使用了事件通知以確定在一組非阻塞套接字中有哪些已經(jīng)就緒能夠進(jìn)行相關(guān)的操作。目前,可以把看作是傳入入站或者傳出出站數(shù)據(jù)的載體。出站事件是未來將會(huì)觸發(fā)的某個(gè)動(dòng)作的操作結(jié)果,這些動(dòng)作包括打開或者關(guān)閉到遠(yuǎn)程節(jié)點(diǎn)的連接將數(shù)據(jù)寫到或者沖刷到套接字。 netty的概念 定義 Netty 是一款異步的事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)用程序框架,支持快速地開發(fā)可維護(hù)的高性能的面向協(xié)議的服務(wù)器和客戶端。我們可以很簡(jiǎn)單的...
閱讀 1272·2021-09-23 11:51
閱讀 1391·2021-09-04 16:45
閱讀 633·2019-08-30 15:54
閱讀 2087·2019-08-30 15:52
閱讀 1605·2019-08-30 11:17
閱讀 3107·2019-08-29 13:59
閱讀 2023·2019-08-28 18:09
閱讀 389·2019-08-26 12:15