Java NIO Tutorial 地址:http://tutorials.jenkov.com/java-nio/index.html
Java NIO系列教程譯文地址:http://ifeve.com/java-nio-all/
以下是我拜讀過程中摘抄的部分內容,並且加了一些內容、筆記,姑且叫《快學Java NIO》,方便以后再翻閱學習
附上一個Java NIO實現的demo,多人網絡聊天室
Java NIO 由以下幾個核心部分組成:
- Channels
- Buffers
- Selectors
基本上,所有的 IO 在NIO 中都從一個Channel 開始。Channel 有點像流。 數據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。
Channel
FileChannel 從文件中讀寫數據。
DatagramChannel 能通過UDP讀寫網絡中的數據。
SocketChannel 能通過TCP讀寫網絡中的數據。
ServerSocketChannel可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。
下面是一個FileChannel的示例
public class FileChannelTest { public static void main(String[] args) throws IOException { RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel();
//涉及到的buffer的方法稍后解釋 ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); while (bytesRead != -1) { //make buffer ready for read buf.flip(); while (buf.hasRemaining()) { System.out.print((char) buf.get());// read 1 byte at a time }
buf.clear();//buf.compact();也可以 bytesRead = inChannel.read(buf); } aFile.close(); } }
Buffer
為了理解Buffer的工作原理,需要熟悉它的三個屬性:
- capacity
- position
- limit
在寫模式下,Buffer的limit表示你最多能往Buffer里寫多少數據。 寫模式下,limit等於Buffer的capacity。
當切換Buffer到讀模式時, limit表示你最多能讀到多少數據。因此,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)
clear方法就是讓position設回0,limit與capacity相等。
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
flip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,並將limit設置成之前position的值。
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
compact()方法將所有未讀的數據拷貝到Buffer起始處。然后將position設到最后一個未讀元素正后面。limit屬性依然像clear()方法一樣,設置成capacity。現在Buffer准備好寫數據了,但是不會覆蓋未讀的數據。
public ByteBuffer compact() { System.arraycopy(hb, ix(position()), hb, ix(0), remaining()); position(remaining()); limit(capacity()); discardMark(); return this; }
Scatter/Gather
scatter/gather用於描述從Channel中讀取或者寫入到Channel的操作
分散(scatter)從Channel中讀取是指在讀操作時將讀取的數據寫入多個buffer中。因此,Channel將從Channel中讀取的數據“分散(scatter)”到多個Buffer中。
聚集(gather)寫入Channel是指在寫操作時將多個buffer的數據寫入同一個Channel,因此,Channel 將多個Buffer中的數據“聚集(gather)”后發送到Channel。
應用場景:例如傳輸一個由消息頭和消息體組成的消息,你可能會將消息體和消息頭分散到不同的buffer中,這樣你可以方便的處理消息頭和消息體
Scattering Reads在移動下一個buffer前,必須填滿當前的buffer,這也意味着它不適用於動態消息(譯者注:消息大小不固定)。
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; channel.read(bufferArray);
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); //write data into buffers ByteBuffer[] bufferArray = { header, body }; channel.write(bufferArray);
FileChannel的transferFrom()方法可以將數據從源通道傳輸到FileChannel中,下面是一個簡單的例子:
RandomAccessFile fromFile = new RandomAccessFile("data/fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("data/toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); //toChannel.transferFrom(fromChannel, position, count);也可以 fromChannel.transferTo(position, count, toChannel);
Selector
Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否為諸如讀寫事件做好准備的組件。這樣,一個單獨的線程可以管理多個channel,從而管理多個網絡連接,Selector能夠處理多個通道。
Selector selector = Selector.open();
//FileChannel不能切換到非阻塞模式,所以這邊不能使FileChannel channel.configureBlocking(false);//與Selector一起使用時,Channel必須處於非阻塞模式下 SelectionKey key = channel.register(selector, SelectionKey.OP_READ); //除了注冊讀,還可以注冊connect,accept,read,write事件 while(true) { int readyChannels = selector.select(); //阻塞到至少有一個通道就緒,還有select(long timeout)超時就不阻塞,selectNow()不阻塞,沒有就返回0,當然打斷阻塞還有wakeUp()方法,可以用另外一個線程調用這個方法,操作同一個selector對象即可 if(readyChannels == 0) continue; Set selectedKeys = selector.selectedKeys(); //可以通過這個方法,知道可用通道的集合 Iterator keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel.
//SelectionKey.channel()方法返回的通道需要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等 } 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 }
//Selector不會自己從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。 keyIterator.remove(); } }
現在能看到的情況是,一個請求過來,到Selector這邊,selector從注冊的通道中選擇就緒的通道,然后找到具體的通道處理這個請求。
用一個selector線程來安排所有的channel!
當然為了並發,可以用多個selector,然后不同的channel來注冊。這樣就有了反向代理的感覺,selector就是反向代理服務器上的線程!
(以上是我個人對selector的理解,若理解有誤,請指正)
Java NIO與IO
我應該何時使用IO,何時使用NIO呢?在本文中,我會盡量清晰地解析Java NIO和IO的差異、它們的使用場景,以及它們如何影響您的代碼設計。
Java NIO與IO之間主要差別
IO NIO
面向流 面向緩沖
阻塞IO 非阻塞IO
無 選擇器
Java NIO的緩沖導向方法是數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動,這就增加了處理過程中的靈活性。NIO設計中多了buffer,傳統IO如果要這個效果,需要自行定義操作buffer。
Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取。
Java NIO的選擇器允許一個單獨的線程來監視多個輸入通道,你可以注冊多個通道使用一個選擇器。
在IO設計中,我們從InputStream或 Reader逐字節讀取數據。 readline()阻塞直到整行讀完
NIO可讓您只使用一個(或幾個)單線程管理多個通道(網絡連接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。
如果需要管理同時打開的成千上萬個連接,這些連接每次只是發送少量的數據,例如聊天服務器,實現NIO的服務器可能是一個優勢。
如果你需要維持許多打開的連接到其他計算機上,如P2P網絡中,使用一個單獨的線程來管理你所有出站連接,可能是一個優勢。
Java NIO: 單線程管理多個連接,如下圖
如果你有少量的連接使用非常高的帶寬,一次發送大量的數據,也許典型的IO服務器實現可能非常契合。
Java IO: 一個典型的IO服務器設計- 一個連接通過一個線程處理,如下圖
至此,基本上Java NIO的大體輪廓已經明白了,鑒於篇幅不要太長,各個具體Channel的介紹移步:快學Java NIO續篇
- FileChannel
- SocketChannel
- ServerSocketChannel
- Java NIO DatagramChannel
- Pipe