1. 程式人生 > >java學習之nio

java學習之nio

 

Java NIO 完全學習筆記(轉)

本篇部落格依照 Java NIO Tutorial 翻譯,算是學習 Java NIO 的一個讀書筆記。建議大家可以去閱讀原文,相信你肯定會受益良多。

1. Java NIO Tutorial

Java NIO,被稱為新 IO(New IO),是 Java 1.4 引入的,用來替代 IO API的。

Java NIO:Channels and Buffers

標準的 Java IO API ,你操作的物件是位元組流(byte stream)或者字元流(character stream),而 NIO,你操作的物件是 channels 和 buffers。資料總是從一個 channel 讀到一個 buffer 上,或者從一個 buffer 寫到 channel 上。

Java NIO:Non-blocking IO

Non-blocking 是非阻塞的意思。Java NIO 讓你可以做非阻塞的 IO 操作。比如一個執行緒裡,可以從一個 channel 讀取資料到一個 buffer 上,在 channel 讀取資料到 buffer 的時候,執行緒可以做其他的事情。當資料讀取到 buffer 上後,執行緒可以繼續處理它。

Java NIO: Selectors

selector 翻譯為選擇器,selector 可以監控(monitor)多個 channel 的事件。

2. Java NIO OverView

Java NIO 有三個核心元件(core components):

  • Channels
  • Buffers
  • Selectors

除了這三個核心元件外,還有一些輔助的類,但是咱們現在最關心的是這三個。

Channels and Buffers

一般來說,NIO 都是從一個 Channel 開始的,Channel 有點像流(Stream),通過 channel,資料可以讀取到 Buffer,同樣的,資料是從 Buffer 寫入到 Channel 的。

NIO 中包含了幾個常見的 Channel ,這幾個 channles 包含了咱們開發中使用率較高的 檔案 IO,UDP+TCP 網路 IO。

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

NIO 中實現了幾個常用的 Buffer

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
  • MappedByteBuffer

這幾個 Buffer 分別對應了 Java 的幾個基礎型別,唯獨沒 boolean 型別的 Buffer。MappedByteBuffer 是一個特殊的,一般被用來做記憶體對映(memory mapped files)。

Selectors

選擇器讓一個執行緒可以處理多個 Channels。如果你的應用有多個連線開啟,但每個連線的流量都比較低(low traffic),為每個連線單開一個執行緒顯得比較浪費,而選擇器讓你可以在一個執行緒中操作這些低流量的連線,減少的執行緒的開銷。

3. Java NIO Channel

Java NIO Channel 和流有些相似,但也有不少不同:

  • 一個 Channel 可以讀和寫,而一個流一般只能讀或者寫
  • Channel 可以非同步(asynchronously)的讀和寫
  • Channel 總是需要一個 Buffer,不管是讀到 Buffer 還是從 Buffer 寫到 Channel

Channel Implementations

Java NIO 實現了幾個常見的 channel:

  • FileChannel 讀取資料或者寫入資料到檔案中
  • DatagramChannel 讀寫資料通過 UDP 協議
  • SocketChannel 讀寫資料通過 TCP 協議
  • ServerSocketChannel 提供 TCP 連線的監聽,每個進入的連線都會建立一個 SocketChannel。

Basic Channel Example

下面是一個簡單的例子,大概的步驟是開啟一個檔案的連線,然後獲取到一個 Channel,開闢一個 Buffer,從 channel 讀取資料到 buffer,然後再從 Buffer 中獲取到讀取的資料。

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
  System.out.println("Read " + bytesRead);
  buf.flip();
  while(buf.hasRemaining()){
      System.out.print((char) buf.get());
  }
  buf.clear();
  bytesRead = inChannel.read(buf);
}
aFile.close();

4. Java NIO Buffer

在和 Channel 互動的時候,需要一個 Buffer,從 Channel 中讀取資料放入到 Buffer 中,或者從 Buffer 中寫入資料到 Channel 中。我們並不直接操作 Channel,而是操作 Buffer。

Basic Buffer Usage

使用 Buffer 一般需要下面四個步驟:

  • 寫資料到 Buffer
  • 呼叫 buffer.flip()
  • 從 Buffer 讀出資料
  • 呼叫 buffer.clear() 或者 buffer.compact()

當你往 Buffer 中寫資料,Buffer 會記錄你已經寫了多少資料,一旦你需要從 Buffer 中讀資料,你需要呼叫 flip() 方法,讓 Buffer 由寫模式程式設計讀模式。讀模式下,你可以讀出你剛寫入的所有資料。

當你讀完所有資料後,你需要清空 buffer,讓 Buffer 變成可以寫入的狀態。有兩個辦法讓 Buffer 變成寫模式,一個是呼叫 clear() 方法,一個是 compact() 方法。clear() 方法會清空 Buffer 的所有資料,而 compact() 方法之清除你已經讀取的資料,那些在 Buffer 中而沒有被讀取的資料會被移動到 Buffer 的開頭部分,下次寫的時候就從移動後最後的位置開始寫入。

Buffer Capacity,Position and Limit

一個 Buffer 本質上是一個記憶體塊,你可以往裡寫入資料,並且可以從裡往外讀取資料。這樣一個記憶體塊被包裹在一個 Buffer 物件中,並且 Buffer 提供了一些方便操作方法。

Buffer 有三個重要的屬性,理解這三個屬性讓你能更加明白 Buffer 的原理。

  • capacity
  • position
  • limit

在讀模式和寫模式下,position 和 limit 有著不同的意思,但 capacity 在讀模式和寫模式下,意思是相同的。

Capacity

capacity 是容量的意思。當你建立一個 Buffer 物件的時候,這個 Buffer 的容量就是固定的,你只能寫入小於等於 capacity 大小的資料,如果這個 Buffer 已經滿了,你要麼需要讀出資料,要麼需要清空資料,否則你不能寫人更多的資料。

Position

position 是位置的意思,在你寫入資料到 Buffer 的時候,這個資料被放入一個確切的位置,position 從 0 開始,寫入一個數據,position 往後移動一個單元,也就是 +1 ,直到等於 capacity -1 。

當你讀取資料的時候,也是從一個確切的位置讀取的。當你呼叫 flip() 方法,Buffer 由寫模式變成了讀模式,position 變成了 0,當你讀一個數據的時候,position 同樣往後移動一個單位。

Limit

limit 是極限的意思,在寫模式下,limit 是等同於 capacity 。當由寫模式變成讀模式的時候,limit 被設定為寫模式下最後寫入的 position。這樣,position 由 0 開始,可以讀到 limit 的位置。因為在讀模式下,超過 limit 位置的資料其實是不合法的資料,不應該被讀出。

Buffer Types

Java NIO 實現了下面幾個 Buffer,上文已經做了些介紹了。

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

Allocating a Buffer

每個 Buffer 類都有 allocate() 的靜態方法,用這個方法可以建立一個 Buffer 例項,比如:

ByteBuffer buf = ByteBuffer.allocate(48);
CharBuffer buf = CharBuffer.allocate(1024);

Writing Data to a Buffer

有兩種寫入資料到 Buffer 的方法:

  1. 通過 Channel 寫入資料
  2. 通過 Buffer 的 put() 方法寫入資料

程式碼如下:

int bytesRead = inChannel.read(buf); //read into buffer.
buf.put(127);

當然,put() 方法有很多過載的方法可以讓你通過各種姿勢寫入資料。具體的用法看 JavaDoc,很簡單的。

flip()

flip() 方法讓一個 Buffer 從寫模式變成讀模式,這個方法呼叫後,position 回到 0,limit 變成寫模式的最後一個位置。其實就是在讀寫模式切換的時候,對 position 和 limit 屬性的修改。

Reading Data from a Buffer

和寫資料相似,讀資料也有兩種方式:

  1. 通過 channel
  2. 通過 Buffer 的 get() 方法。

下面是程式碼:

int bytesWritten = inChannel.write(buf);
byte aByte = buf.get();

同 put() 方法,也有很多 get() 方法可以使用。

rewind()

Buffer.rewind() 方法讓 position 回到 0,這樣你可以再次讀取一次資料。但這個方法並不會影響 limit 屬性。

clear() and compact()

每次當你完成讀資料後,需要讓 Buffer 變回寫模式,你可以通過呼叫 clear() 或者 compact() 方法。

clear() 方法讓 position 等於 0,limit 等於 capacity 。相當於 Buffer 被清空了,但事實上資料本身沒有被清空,只是這個 Buffer 的所有資料單元都是可寫入的,可覆蓋的。

如果你呼叫了 clear() 方法,Buffer 中那些沒有被你讀取的資料就等於被清除了,你再也沒有機會再讀取他們了。

如果你還有部分資料沒有讀取不想丟掉,但是又需要讓 Buffer 再次進入寫模式,那麼你應該呼叫 compact() 方法。compact() 方法會先拷貝那些還沒有被讀取的資料到 Buffer 的開頭部分,position 設定在這部分資料的結束的位置,limit 還是等於 capacity 。當進入寫入模式後,寫入的資料就從 position 的位置開始往後寫,之前沒有被讀取的資料被儲存了下來。

mark() and reset()

呼叫 Buffer.mark() 的時候,在這個 Buffer 上做一個標記,在稍後呼叫 Buffer.reset(),position 會回到剛才標記的位置。

equals() and compareTo()

這兩個方法用來比較兩個 Buffer 是否相同。

5. Java NIO Scatter / Gather

scatter 有分散,散開的意思,gather 有收集,聚合的意思。scatter 的意思是從一個 channel 讀取資料,填充到多個 Buffer 中,gather 的意思是把多個 Buffer 的資料寫入到一個 Channel 中。

Scattering Reads

scattering read 表示從一個 channel 中讀取資料填充到多個 Buffer 中,比如:

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

這裡有個限制,只有當第一個 Buffer 被填充滿的情況下才會被填充到下一個 Buffer 中,像上面的例子,除非你能確保 header 部分肯定是 128 個位元組,不然就不適合這樣的寫法。

Gathering Writes

gathering write 表示可以把多個 Buffer 的資料寫入到一個 channel 中。

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);

當從 Buffer 中讀資料往 channel 裡寫的時候,Buffer 可讀的範圍受 limit 限制,所以,如果上面程式碼中 header 只被寫入 58 個位元組的時候,從 header 往外讀也只會是 58 位元組,不會是 128位元組。

6. Java NIO Channel to Channel Transfers

Java NIO 支援兩個 channel 直接轉移資料。

transferFrom()

RandomAccessFile fromFile = new  RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);

position 和 count 兩個引數用來告訴開始傳輸的起始位置和多少個位元組需要傳輸,如果源 channel 的大小小於 count,那麼取那個數值小的。

一些 SocketChannel 會只傳輸當前 channel 對應的 Buffer 已經準備好的資料,即使 SocketChannel 稍後還有更多的資料。因此,可能不能完整的傳輸一個 SocketChannel 的資料到一個 FileChannel。

transferTo()

下面是一個使用 transferTo() 的例子。和上文的例子相似,如果從一個 SocketChannel 傳輸資料到一個 FileChannel,傳輸會在讀取完當前 SocketChannel 可用的資料後就結束,而很大可能不是全部資料。

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);

7. Java NIO Selector

Selector 讓一個執行緒可以同時管理多個 Channel。

Why use a Selector?

最大的好處是你可以一個執行緒管理多個 channel,而不是多個執行緒。一般咱們認為在多個執行緒之間切換是相對昂貴的,每個執行緒都佔用著一定的系統資源,比如記憶體,所以越少的執行緒是越好的。

當然,現在的作業系統和 CPU 更好的支援了多執行緒工作,如果你的 CPU 是多核的,不使用多工的話,等於浪費 CPU。

不管怎麼說,特別對於 Android 開發來說,執行緒肯定也是寶貴的,能少就少。

Creating a Selector

建立 Selector 很簡單,呼叫 open() 方法就可以了。
Selector selector = Selector.open();

Registering Channels with the Selector

使用選擇器,必須對 Channel 進行註冊。註冊例子:

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

Channel 必須是非阻塞的,FileChannel 不能使用選擇器,因為 FileChannel 沒有非阻塞模式。

在呼叫 register() 方法的時候,第二個引數表面了你對那些事件感興趣,只有你感興趣的事件,你才會收到對應事件的回撥,一共有四個事件:

  1. Connect
  2. Accept
  3. Read
  4. Write

當一個 Channel 發起一個事件的時候,意味著這個事件已經準備好了。比如,只有在一個 Channel 成功連線到另一個服務的時候,才意味著連線完成(connect ready)。一個 ServerSocketChannel 只有在與一個客戶端的建立連線後,才會觸發 accept ready。當一個 Channel 準備好可讀的時候,才是 read ready,準備好可寫的時候,才是 write ready。

下面有四個常數,被用在 register() 的第二個引數。

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

SelectionKey's

register() 方法返回一個 SelectionKey 物件,這個物件包含了一些咱們需要關心的屬性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

Interest Set

Interest Set 就是在註冊監聽的時候,咱們設定的興趣的集合。

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

通過以上的程式碼,咱們可以獲取哪些事件是已經設定為感興趣的。

Ready Set

Ready set 是準備集合,可以通過它來獲取當前有哪些事件是準備完成的。獲取狀態集合的程式碼:

int readySet = selectionKey.readyOps();

也可以通過下面的 API ,更方便的得到當前的狀態是否可用。

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

Channel + Selector

通過 SelectionKey 可以獲得 Channel 物件和 Selector 物件:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();

Attaching Objects

附加的物件,你可以往 SelectionKey 新增物件,在稍後的時候,你可以從 SelectionKey 取出新增的物件。

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

新增物件也可以在註冊的時候新增:

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

Selecting Channels via a Selector

當你把多個 Channel 註冊到 Selector,你可以通過呼叫 select() 方法獲取到 Channel,select() 方法返回的 Channel 是那些準備好的,並且你是設定為感興趣的事件。select() 方法:

  • int select() 這個方法呼叫後會阻塞住,直到有一個 channel 的某個你感興趣的事件已經準備好了,才返回
  • int select(long timeout) 功能和 select() 方法一樣,多了一個超時時間
  • int selectNow() 直接返回

select() 方法返回的是滿足條件的 channel 的個數,這個個數是距離上一次呼叫 select() 方法直接變成 ready 的channel 的個數。

selectedKeys()

通過 select() 方法獲取到個數後,你還不能操作 channel,因為你現在沒有 channel 物件的引用,現在你需要通過 Selector.selectedKeys() 方法獲取到一個 SelectionKey 集合:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

獲取到這個集合後,你就可以遍歷這個集合,程式碼如下:

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}

注意 remove() 方法,這個不是把 SelectionKey 從 Selector 中移除掉,只是從這個集合中移除掉,當這個 channel 下回 ready 的時候,一個 SelectionKey 會再次被新增到這個集合中,所以,remove() 是很有必要的。

wakeUp()

如上文所說,select() 方法是阻塞的,直到有 channel 的狀態變成 ready 才會返回。如果一直沒有 channel 的狀態變成 ready ,那麼這個方法會一直阻塞在那,這個時候,我們可以在其他執行緒通過呼叫 selector.wakeUp() 方法,讓 select() 立刻返回。

close()

在你使用完 Selector 的時候,務必要記得呼叫 close() 方法。close() 方法會關閉 Selector 和 作廢 SelectionKey 例項。

8. Java NIO FileChannel

FileChannel 提供了除了標準的 Java 檔案 IO 外的另外一種讀寫檔案的方式。

和其他 Channel 不同的是,FileChannel 不能被設定為非阻塞模式(non-blocking mode)。

Opening a FileChannel

FileChannel 不能直接開啟一個檔案,需要 InputStream,OutputStream 或者 RandomAccessFile ,然後獲取 Channel。

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

Reading Data from a FileChannel

下面是從 FileChannel 讀資料的程式碼:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

bytesRead 的取值為 read() 方法從 channel 寫入到 Buffer 的位元組數量,-1 表示檔案結束。

Writing Data to a FileChannel

下面是往 FileChannel 寫資料的程式碼:

String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
    channel.write(buf);
}

注意:channel.write() 並不能保證每次都把 buffer 裡的資料都寫入到 channel 中,所有,你需要在一個迴圈內,多次的呼叫 write() 方法,直到 buffer.hasRemaining() 返回 false,表示 buffer 內已經沒有再需要被取代的資料了,說明 buffer 的資料都被寫入到 channel 了,才可以退出迴圈。

Closing a FileChannel

關閉一個 FileChannel,這個沒有什麼好說明的,用完就關,這是基本法。

FileChannel Position

在 Java IO 中,只有 RandomAccessFile 可以做到 position 前後移動,而其他的都做不到,或者有很多限制(比如帶 Buffer 的流)。而在 FileChannel 就支援這樣的操作。

long pos channel.position();
channel.position(pos +123);

如果你把 position 移動到大於檔案大小的位置,在讀的時候,會獲得 -1 的結果,表示檔案已經結束。

如果你把 position 移動到大於檔案大小的位置,並且往裡寫的話,新寫入的內容會被寫入到你指定的位置。這樣的結果是,會出現檔案洞。This may result in a "file hole", where the physical file on the disk has gaps in the written data.

FileChannel Truncate

檔案截斷,如下的程式碼能把檔案截斷為 1024 個位元組。

channel.truncate(1024);

FileChannel Force

FileChannel.force() 讓 FileChannel 把所有還沒有寫入到磁碟的資料寫入。一般作業系統會做一些緩衝,所以資料沒有真正被寫入到磁碟中。呼叫 force() 方法可以讓資料和檔案資訊寫入到磁碟中。

9. Java NIO SocketChannel

SocketChannel 用於建立 TCP 連線,SocketChannel 有兩種建立的方式:

  1. 直接建立一個 SocketChannel,連線到任何的網路服務
  2. ServerSocketChannel 在收到連線後,也會建立一個 SocketChannel。

開啟 SocketChannel

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

讀和寫

SocketChannel 的讀和寫都與 FileChannel 相似,基本都是一樣的。

Non-blocking Mode

SocketChannel 和 FileChannel 不一樣的是,它具有非阻塞模式,在非阻塞模式下,你可以以非同步的方式呼叫 connect(),read() 和 write()。

connect()

在非阻塞模式下,connect() 會立刻返回,不會等連線建立。你需要通過 finishConnect() 返回來判斷連線是否建立。

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
while(! socketChannel.finishConnect() ){
    //wait, or do something else...    
}

write() and read()

在非阻塞模式中,write() 和 read() 並沒有真正的資料讀寫,所以你需要在稍後的操作中迴圈的讀寫 buffer 中的資料。

Non-blocking Mode with Selectors

因為 SocketChannel 有非阻塞模式,所以它是可以很好的使用在 Selector 中的。我們可以在 Selector 中註冊多個 SocketChannel,然後通過一個這個 Selector 管理這些 SocketChannel。

10. Java NIO ServerSocketChannel

ServerSocketChannel 被使用在 TCP 連線的服務端,通過制定的埠,監聽任何的連線。

Opening a ServerSocketChannel

下面的程式碼是建立一個 ServerSocketChannel 例項。

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

在建立完一個 ServerSocketChannel 例項後,咱們需要為這個 socket server 繫結一個埠,程式碼如下:

serverSocketChannel.socket().bind(new InetSocketAddress(9999));

Listening for Incoming Connections

ServerSocketChannel.accept() 方法用來獲得一個接入的連線(incoming connection),預設的,這個方法是阻塞的,程式碼如下:

while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();
    //do something with socketChannel...
}

Non-blocking Mode

ServerSocketChannel 可以被設定為非阻塞模式,在非阻塞模式下, accept() 會立馬返回,因此它很有可能會返回一個空的 SocketChannel 物件。同樣的,ServerSocketChannel 是可以配合 Selector 一起友好的工作的。設定非阻塞模式的程式碼如下:

serverSocketChannel.configureBlocking(false);

11. Java NIO: Non-blocking Server

12. Java NIO DatagramChannel

DatagramChannel 被用來接收和傳送 UDP 資料包,因為 UDP 是無連線(connection-less)的網路協議,所以,它的讀寫和其他的 Channel 有些區別。

Opening a DatagramChannel

下面是開啟一個 DatagramChannel 的程式碼:

DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));

Receiving Data

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);

接收資料和其他的類似,receive() 會把接收到的資料包裡的內容拷貝到 Buffer 中,但是如果資料包的內容大於 Buffer 的大小,那麼多出來的資料會被拋棄掉(discarded silently),這一點需要大家注意。

Sending Data

String newData = "New String to write to file..."
                + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));

UDP 是一個無連線的網路協議,所以你傳送的資料並不能保證接收方能否正確接收,你也不會知道這些資料傳送的任何狀態。

Connecting to a Specific Address

因為 UDP 協議的特性,所以你建立一個 DatagramChannel 的時候,並不需要關心服務端是否開啟並且已經準備好。因此,你完全可以任意指定一個接收地址,哪怕它真的不存在。

channel.connect(new InetSocketAddress("jenkov.com", 80));

在初始化 DatagramChannel 後,你除了使用 receive() 和 send() 方法外,你同樣也可以使用 write() 和 read() 方法,和其他的 Channel 一樣。只是 DatagramChannel 並不保證資料是否正確分發。

13. Java NIO Pipe

管道提供了兩個執行緒之間資料的連線和傳輸。一個管道包含一個 source channel 和一個 sink channel,資料從 sink channel 寫入,從 source channel 被另外一個執行緒讀出。

Creating a Pipe

建立一個管道很簡單:

Pipe pipe = Pipe.open();

Writing to a Pipe

在往管道里寫資料前,需要先獲取一個 sink channel :

Pipe.SinkChannel sinkChannel = pipe.sink();

在獲取到 sink channel 後,就可以往裡寫資料了:

String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
    sinkChannel.write(buf);
}

Reading from a Pipe

從管道中讀資料,需要先獲取到一個 source channel:

Pipe.SourceChannel sourceChannel = pipe.source();

獲取到 source channel 後,讀取資料就跟普通 channel 相似了:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

14. Java NIO vs. IO

Main Differences Betwen Java NIO and IO

下面表格展示了 NIO 和 IO 的大致區別。

QQ圖片20160408162815.png

Stream Oriented vs. Buffer Oriented

面向流和麵向緩衝是 NIO 和 IO 的最大區別。

面向流意思是你一次只能從流中讀一個或者多個位元組的內容,它們是沒有緩衝的,因此,你也不能在一個流中任意的向前或者先後移動,如果你需要向前或者向後讀取,你只能自己先做一個緩衝。

而 Java NIO 是先將資料讀取後寫入到 Buffer 中,你可以自由的在這個 Buffer 中讀取。當然,Buffer 是有大小的,意味著你也不是任意的往前和任意的往後移動的。但至少,對於 Java NIO,你直接操作的是 Buffer。

Blocking vs. Non-blocking IO

Java IO 是阻塞的,意味著在呼叫 read() 或者 write() 的時候,執行緒會阻塞在這裡的,直到資料被寫入或者讀出,在這個時候,執行緒不能做其他的事情。

Java NIO 的非阻塞模式,可以讓一個執行緒想 channel 發起一個讀取資料的請求,然後並不關心這個時候有多少資料被讀取,執行緒可以繼續執行其他的程式碼。非阻塞的寫也是相似的,執行緒請求往一個 channel 裡寫入資料的時候,並不會等資料真正被寫入。

因為讀和寫都可以是非阻塞的,所以一個執行緒可以同時處理多個 channel 的讀寫任務。

Selectors

Selectors 讓一個執行緒可以同時處理多個 channel 的資料。你們把多個 channel 註冊到一個 Selector 上,然後在一個執行緒裡,通過 Selector 的 select() 方法分別去處理各個 channel 的讀或者寫。

How NIO and IO Influences Application Design

上文已經說了很多 NIO 和 IO 的區別了,已經 NIO 的使用方式,具體到應用中,就需要考慮怎麼去選擇和設計了。

一般來說,NIO 比較適合用在那些流量少,個數又多的場景中,比如一個聊天服務端,每個客戶端並不是總是時時向服務端傳送資料的,如果服務端使用 IO 的方式的話,需要給每個客戶端的連線建立一個執行緒,一直等待客戶端向服務端傳送資料。而如果使用 NIO,那麼服務端就可以使用一個執行緒或者很少的執行緒來完成相同的工作。

15. Java NIO Path

Path 在 Java 7 中被加入,Path 的完整位置為 java.nio.file.Path。但這一路徑沒有出現在 Android 中。一個 Path 例項表示了一個檔案系統的路徑,這個路徑可以是絕對路徑,可以是相對路徑,可以是一個檔案,也可以是一個目錄。Path 和 java.io.File 有點相似,但又有些不一樣(廢話~~)

Creating a Path Instance

下面的程式碼對應了 windows 和 Unix 系統下獲取絕對路徑 Path 的寫法:

Path path = Paths.get("c:\\data\\myfile.txt");
Path path = Paths.get("/home/jakobjenkov/myfile.txt");

首先 Path 是一個介面,獲取一個 Path 例項是通過 Paths.get() 方法,Paths.get() 就是 Path 的工廠方法。

一個相對路徑的 Path 需要依賴另外一個 Path,被稱為 Base Path,這個相對路徑的 Path 的絕對路徑就是 Base path 加上相對路徑。

Path projects = Paths.get("d:\\data", "projects");
Path file     = Paths.get("d:\\data", "projects\\a-project\\myfile.txt");

Path 支援使用一個點 . 表示當前路徑和使用兩個點 .. 表示上級路徑。當一個路徑中存在 . 或者 .. 後,可以使用 Path.normalize() 刪除掉那些路徑,並且得到一個最終的絕對路徑。

16. Java NIO Files

Java NIO Files 和 Paths 一起配合使用,提供了豐富的檔案操作的 API,位於 java.nio.file 包下,但是目前同樣沒有出現在 Android 中。具體的 API 很簡單,看看 JavaDoc 就基本 OK 了。

17. Java NIO AsynchronousFileChannel

AsynchronousFileChannel 在 Java 7 中被加入到 Java NIO 中。AsynchronousFileChannel 讓對一個檔案的非同步讀寫可以在非同步進行。

Creating an AsynchronousFileChannel

下面的程式碼展示瞭如何建立一個 AsynchronousFileChannel 例項:

Path path = Paths.get("data/test.xml");
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

Reading Data

從 AsynchronousFileChannel 讀取資料有兩種方式:

1. Reading Data Via a Future

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
Future<Integer> operation = fileChannel.read(buffer, position);
while(!operation.isDone());
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
buffer.clear();

在上面的程式碼中, read() 方法返回了一個 Future 物件,當 Future.isDone() 返回 true 的時候,表示檔案已經被讀取到 Buffer 中了。當然上面只是一個例子,while true 的方式是不好的。

2. Reading Data Via a CompletionHandler

第二種方式是在 read() 方法的第三個引數傳遞一個 CompletionHandler 例項。

fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        System.out.println("result = " + result);
        attachment.flip();
        byte[] data = new byte[attachment.limit()];
        attachment.get(data);
        System.out.println(new String(data));
        attachment.clear();
    }
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
    }
});

在上面的程式碼中,一旦讀的操作完成的時候,completed() 方法會被呼叫,剩餘的程式碼和上一種方式就大概相同了。

Writing Data

寫資料和讀資料一樣,也有兩種方式,一種是通過 Future,一種也是實現 CompletionHandler。

18. 總結

歷時 2 天,基本上完整得看完了 Jenkov 的文章,也順便記錄下了上面的內容,感覺收穫是很大的,特別是感覺順著文章一路往下學習,整個知識更加清晰明瞭了。相信如果你也看完一遍,你也可以有很清晰的理解。

最後結合 Android,個人認為 NIO 在 Android 上使用可能並不是很多,即使有也是在網路請求那部分,而這一部分基本上市面上主流的開源框架都整合好了,如果需要深入理解的話,可以去看看他們怎麼實現,反正咱們在專案中基本上是不大可能需要直接面對 NIO 了。

對於檔案操作,因為 FileChannel 是阻塞的,所以他的用處可能出現在對檔案的讀寫上了,特別是小數閱讀類的應用,FileChannel 的面向 Buffer 的機制是很有必要的。當然 RandomAccessFile 也可能能夠勝任。

而文章後面的 Path,Files 和 AsynchronousFileChannel 並沒有出現在 Android 中,目前還不知道怎麼個支援方式。
我的標籤

閱讀排行榜

評論排行榜

推薦排行榜