Java NIO學習筆記二------組件詳解篇

Channel

Channel是什麼

Channel一般翻譯爲通道。Java NIO的通道類似流,但又有些不同: 
既可以從通道中讀取數據,又可以寫數據到通道。但流的讀寫通常是單向的。 
通道可以異步地讀寫。 
通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。

Channel的實現

FileChannel, 從文件中讀寫數據。 
DatagramChannel,通過UDP讀寫網絡中的數據。 
SocketChannel,通過TCP讀寫網絡中的數據。 
ServerSocketChannel,可以監聽新進來的TCP連接,對每一個新進來的連接都會創建一個SocketChannel。

Buffer

Buffer是什麼

Java NIO中的Buffer用於和NIO通道進行交互。數據從通道讀入到緩衝區,從緩衝區寫入到通道中。 
緩衝區本質上是一塊可以寫入數據,然後可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。

Buffer的基本用法

使用Buffer讀寫數據一般遵循以下四個步驟: 
1. 寫入數據到Buffer。當向buffer寫入數據時,buffer會記錄下寫了多少數據。 
2. 調用flip()方法。一旦要讀取數據,需要通過flip()方法將Buffer從寫模式切換到讀模式。 
3. 從Buffer中讀取數據。在讀模式下,可以讀取之前寫入到buffer的所有數據。 
4. 調用clear()方法或者compact()方法。一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:調用clear()或compact()方法。clear()方法會清空整個緩衝區。compact()方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。

Buffer的三個重要屬性

capacity

作爲一個內存塊,Buffer有一個固定的大小值,即capacity。你只能往裏寫capacity個byte、char等類型。一旦Buffer滿了,需要將其清空才能繼續往裏寫數據。

position

position表示當前的位置。 
當Buffer處於寫模式時,初始的position值爲0。當一個byte、char等數據寫到Buffer後,position會向前移動到下一個可插入數據的Buffer單元。position最大可爲capacity – 1。 
當Buffer從寫模式切換到讀模式時,position會被重置爲0。當從Buffer的position處讀取數據時,position向前移動到下一個可讀的位置。

limit

在寫模式下,Buffer的limit表示你最多能往Buffer裏寫多少數據。此時,limit等於Buffer的capacity。 
當Buffer從寫模式切換到讀模式時, limit表示你最多能讀到多少數據。此時,limit會被設置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)。

Buffer的類型

Java NIO 有以下Buffer的實現: 
ByteBuffer 
MappedByteBuffer 
CharBuffer 
DoubleBuffer 
FloatBuffer 
IntBuffer 
LongBuffer 
ShortBuffer 
這些Buffer類型對應了不同的基本數據類型。

Buffer的分配

要想獲得一個Buffer對象首先要進行分配。 每一個Buffer類都有一個allocate方法。例如:

ByteBuffer byteBuf = ByteBuffer.allocate(48);
CharBuffer charBuf = CharBuffer.allocate(1024);

 

寫Buffer

方法一,從Channel寫到Buffer,例如:

int bytesRead = inChannel.read(buf);

方法二,通過put方法寫Buffer,例如:

buf.put(100);

寫模式切換到讀模式

flip()方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,並將limit設置成之前position的值。

 

讀Buffer

方法一,從Buffer讀到Channel,例如:

int bytesWritten = inChannel.write(buf);

方法二,通過get方法寫Buffer,例如:

byte someByte = buf.get();

重讀Buffer

rewind()將position設回0,所以可以重讀Buffer中的所有數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素。

清空Buffer

一旦讀完Buffer中的數據,需要讓Buffer準備好再次被寫入。可以通過clear()或compact()方法來完成。 
如果調用的是clear()方法,position將被設回0,limit被設置成 capacity的值。換句話說,Buffer 被清空了,不過Buffer中的數據並未清除,只是這些標記告訴我們可以從哪裏開始往Buffer裏寫數據。 
如果Buffer中仍有未讀的數據,且後續還需要這些數據,但是此時想要先寫些數據,那麼使用compact()方法。 
compact()方法將所有未讀的數據拷貝到Buffer起始處。然後將position設到最後一個未讀元素正後面。limit屬性依然像clear()方法一樣,設置成capacity。現在Buffer準備好寫數據了,但是不會覆蓋未讀的數據。

標記特定的位置

通過調用mark()方法,可以標記Buffer中的一個特定position。之後可以通過調用Buffer.reset()方法恢復到這個position。

兩個buffer的比較

兩個方法都是隻比較Buffer中的剩餘元素(從 position到limit之間的元素)。 
equals() 
當下列條件都滿足時,表示兩個Buffer相等: 
- 有相同的類型(byte、char、int等)。 
- Buffer中剩餘的byte、char等的個數相等。 
- Buffer中所有剩餘的byte、char等都相同。 
compareTo() 
當下列任一條件滿足時,表示一個Buffer“小於”另一個Buffer: 
- 第一個不相等的元素小於另一個Buffer中對應的元素 。 
- 所有元素都相等,但第一個Buffer的元素個數比另一個少。

聚集和分散

Scattering Reads

Scattering Reads是指數據從一個channel讀取到多個buffer中。

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

read()方法按照buffer在數組中的順序將從channel中讀取的數據寫入到buffer,當一個buffer被寫滿後,channel緊接着向另一個buffer中寫。

Gathering Writes

Gathering Writes是指數據從多個buffer寫入到同一個channel。

channel.write(bufferArray);

write()方法會按照buffer在數組中的順序,將數據寫入到channel,注意只有position和limit之間的數據纔會被寫入。

Selector

Selector是什麼

Selector(選擇器)是Java NIO的一個組件,能夠檢測一到多個NIO通道,並能夠知曉通道是否爲諸如讀寫事件做好準備。這樣,一個單獨的線程可以管理多個channel,從而管理多個網絡連接。僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。對於操作系統來說,線程之間上下文切換的開銷很大,而且每個線程都要佔用系統的一些資源。因此,使用的線程越少越好。

Selector的創建

通過調用Selector.open()方法創建一個Selector,例如:

Selector selector = Selector.open();

註冊通道

通過SelectableChannel.register()方法來註冊,例如:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

register()方法會返回一個SelectionKey 對象,這個對象代表了註冊到該Selector的通道。 
與Selector一起使用時,Channel必須處於非阻塞模式下。這意味着不能將FileChannel與Selector一起使用,因爲FileChannel不能切換到非阻塞模式。而套接字通道都可以。 
register()方法的第二個參數是一個“interest集合”,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽四種不同類型的事件,這四種事件用SelectionKey的四個常量來表示: 
1. Connect事件-SelectionKey.OP_CONNECT 
2. Accept事件-SelectionKey.OP_ACCEPT 
3. Read事件-SelectionKey.OP_READ 
4. Write事件-SelectionKey.OP_WRITE 
如果對不止一種事件感興趣,那麼可以用“位或”操作符將常量連接起來,

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

 

SelectionKey

register()方法返回的是一個SelectionKey對象,它包含了以下屬性:

interest集合

interest集合是你所選擇的感興趣的事件集合。用“位與”操作interest 集合和給定的SelectionKey常量,可以確定某個給定的事件是否在interest 集合中,例如:

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;

ready集合

ready集合是通道已經準備就緒的操作的集合。從SelectionKey訪問ready集合:

int readySet = selectionKey.readyOps();

可以用“位與”,來檢測channel中什麼事件或操作已經就緒,也可以使用以下四個方法,它們都會返回一個布爾類型:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel

從SelectionKey訪問Channel:

Channel channel = selectionKey.channel();

 

Selector

從SelectionKey訪問Selector:

Selector selector = selectionKey.selector();

附加的對象

可以將一個對象或者更多信息附加到SelectionKey上,這樣就能方便的識別某個給定的通道。例如:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

還可以在向Selector註冊Channel的時候附加對象,例如:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

 

選擇通道

向Selector註冊了通道以後,可以用select()方法返回你所感興趣的事件(如連接、接受、讀或寫)已經準備就緒的那些通道。select()方法有幾個重載: 
int select() 
阻塞到至少有一個通道在你註冊的事件上就緒了。 
int select(long timeout) 
和select()一樣,除了最長會阻塞timeout毫秒。 
int selectNow() 
不會阻塞,不管什麼通道就緒都立刻返回。如果自從前一次選擇操作後,沒有通道變成可選擇的,則返回零。 
select()方法返回的int值表示自上次調用select()方法後有多少通道已經就緒。 
調用了select()方法,並且返回值表明有一個或更多個通道就緒了,然後可以調用selectedKeys()方法,訪問就緒通道,例如:

Set selectedKeys = selector.selectedKeys();

叫醒Selector

某個線程調用select()方法後阻塞了,只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法,阻塞在select()方法上的線程會立馬返回。 
如果有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會立即wake up。

關閉Selector

用完Selector後調用其close()方法會關閉該Selector,註冊到該Selector上的所有SelectionKey實例會無效,但通道本身並不會關閉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章