1. 程式人生 > >java網路程式設計—NIO與Netty(一)

java網路程式設計—NIO與Netty(一)

java BIO

流是一個連續的寫入\讀取 的資料流。將網路、硬碟、記憶體中的資料寫入程式中稱為InputStream,方向相反輸出則稱為OutputStream
顯然,流具有方向性

InputStream
InputStream

對於java IO包的整體設計是decorator設計模式,InputStream、OutputStream是父類,比如BufferedOutputStream/BufferedInputStream在父類原有功能的基礎上增加了快取(byte[])的設計,以減少底層IO的讀寫頻率

java.io.*
java.io.*

java BIO與NIO

IOvsNIO
BIO
常見的客戶端BIO+連線池模型,可以建立n個連線,然後當某一個連線被I/O佔用的時候,可以使用其他連線來提高效能。

面向流與面向緩衝

java IO是面向流的(InputStream、OutputStream),在程式碼進行 read() 呼叫時,程式碼會在無資料可讀時阻塞直至有可供讀取的資料。write()同樣會在資料寫滿SendQ時阻塞。IO需要按順序讀寫,如果要打破順序讀寫,則需要使用額外的緩衝區。

java NIO是面向緩衝的,會先把流中的資料讀寫到緩衝區中,我們從緩衝區獲取資料,相比IO增加了靈活性。緩衝的實現通常都是陣列,它不僅用來儲存資料,同時還監控讀寫情況。

        ByteBuffer buffer = ByteBuffer.allocate(1024); //byte[]實現底層資料結構

這裡寫圖片描述

阻塞與非阻塞

java IO是阻塞式讀寫資料,每個IO一般都會有兩個流 InputStream、OutputStream。 write()方法呼叫後如果資料寫滿後會阻塞,read()當流中資料讀取完會阻塞執行緒。

javaNIO是非阻塞的,通過使用channel概念, NIO的channel與IO的流不同,channel是雙向的既可以寫也可以讀,(好比一個channel代替兩個流 InputStream、OutputStream)這個執行緒可以在多個channel間讀寫資料,如果某個channel中沒有資料或者資料寫完,這個執行緒並不會阻塞,依舊在其他channel上做讀寫操作。

總結:
IO(BIO)面向 流,流單向傳輸Output\Input;阻塞式讀寫;
NIO面向快取,channel雙向傳輸;非阻塞式讀寫;

NIO解決什麼問題?

可以看到bio因為是阻塞IO,通常使用一個執行緒對應一個socket來提高cpu使用效率。這樣bio嚴重依來執行緒,而執行緒也是有相對的開銷的,特別是在十萬級以上的連線場景下,bio無法克服瓶頸問題,因此NIO出現就是解決這個問題的。

Java NIO:Channel

A channel represents an open connection to an entity such as a
hardware device, a file, a network socket, or a program component
that is capable of performing one or more distinct I/O operations,
for example reading or writing.
簡單來說,channel代表了一個指向資訊的連線。

這些通道涵蓋了UDP 和 TCP 網路IO,以及檔案IO.
既可以從通道中讀取資料,又可以寫資料到通道。但流的讀寫通常是單向的。
通道可以非同步地讀寫。
通道中的資料總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入

  • FileChannel 從檔案中讀寫資料
  • DatagramChannel 能通過UDP讀寫網路中的資料
  • SocketChannel 能通過TCP讀寫網路中的資料
  • ServerSocketChannel l可以監聽新進來的TCP連線,像Web伺服器那樣。對每一個新進來的連線都會建立一個SocketChannel。

Java NIO:Buffer

Buffer是與channel進行互動的,channel中資料放入Buffer,我們通過使用Buffer來讀寫資料。Buffer本質上就是一塊存放資料的記憶體。
buffer涵蓋了java中所有的基本型別:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer

Buffer的關鍵屬性(指標)

position:在讀取時,表示當前讀取的起始位置;在寫入時,表示目前寫入的位置(下個寫入位置則是當前position++);
這裡寫圖片描述
limit:讀取模式,最大的讀取位置(往往也是之前寫入的最後位置)
capacity:Buffer的最大容量;
具體請看下邊介紹:

Buffer的初始化

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

此時position為0;limit=capacity

Buffer的讀寫

兩種方式寫入Buffer:

  • 從Channel讀取資料到Buffer。
//此時每寫一個byte,position++
int bytesRead = inChannel.read(buf); 
  • 使用put()方法向buffer寫入資料。
        ByteBuffer buf = ByteBuffer.allocate(1024);
        buf.put(0, (byte) 512);

寫入Buffer切換為讀取Buffer: flip()

當寫入時,position會一直累加,對應於position一直向末尾走。當呼叫flip()方法後,position會置為0,limit被置為之前position的(原position位置)值。為了讀取從0開始讀取,並且不超過limit(之前寫入的最大位置)。

這裡寫圖片描述
Buffer中讀取資料:

  • 將Buffer資料寫入Channel
  • 使用get()方法從Buffer中讀取資料。
int bytesWritten = inChannel.write(buf);//從buf讀取
//或者
byte aByte = buf.get();

rewind()
將position置為0,然後你就可以從頭讀取資料,此時limit不變,還是上次寫入的最後位置
clear()
將position置為0,limit置為capacity。也就是“清空快取”(其實資料並沒有丟失,只是通過指標,保證下次寫入資料完全忽略並覆蓋之前資料
如果你想接著在之前寫過的資料後邊接著寫怎麼辦?用compact()
compact()
compact()方法將所有未讀的資料拷貝到Buffer起始處。然後將position設到最後一個未讀元素正後面。limit屬性依然像clear()方法一樣,設定成capacity。現在Buffer準備好寫資料了(比如後邊更一個put操作),但是不會覆蓋未讀的資料。

mark()\reset()
mark方法用於標記當前position位置,當你又寫了資料(position位置改變)當你想回到之前position時,呼叫reset(),此時position回到mark位置(也就是之前position位置)

示例:

/**
 * @author zhangsh
 */
public class NIOTest {

    public static void main(String[] args) throws IOException {
        // RandomAccessFile與其他IOStream最大卻別別
        // 就是可以通過內部指標可以指定檔案讀寫的具體位置
        RandomAccessFile randomAccessFile = new RandomAccessFile("A.txt", "rw");
        randomAccessFile.seek(0);// 從5byte位置開始讀
        FileChannel fileChannel = randomAccessFile.getChannel();
        // 初始化1M連續記憶體空間
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("init buffer:    " + buf);
        // 從inChannel中讀取資料,放入buf中
        int bytesRead = fileChannel.read(buf);

        System.out.println("buffer after has been  written:  " + buf);
        // flip()呼叫,然後開始讀
        buf.flip();

        System.out.println("buffer after flip:   " + buf);
        while (buf.hasRemaining()) {
            System.out.println((char) buf.get());
            System.out.println((char) buf.get());

            System.out.println("buffer after get:    " + buf);
            // buf.compact();
            // System.out.println("buffer after compact: " + buf);
        }
        buf.clear();
        randomAccessFile.close();
    }
}ystem.out.println(new String(buf.array()));
        }
        buf.clear();
        randomAccessFile.close();
        /**
         * output:
         * 
         * 678910 
         * abcdef
         */
    }

Java NIO:Scatter 與 Gather

NIO支援Scatter與Gather。

Scatter

是指允許將一個channel中的資料分散的依次寫入多個buffer中儲存。

示例:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

將一個channel中的資料分散寫入到多個buffer中,比如一個channel中的資料有訊息頭、訊息頭,要求先要按照陣列入參({ header, body })的順序,依次寫滿一個後,channel再向第二個buffer中寫入。

不支援動態調整寫入資料的大小,必須寫滿前一個後再寫下一個。
這裡寫圖片描述

Gather

是指允許將多個buffer中的資料按某種順序寫入一個channel中
示例:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

陣列入參({ header, body })中的buffer會按照順序寫入到這個channel中,其中每個buffer寫入的資料只是position 到 limit之間的資料。意味著可以動態調整寫入channel的資料位置。
這裡寫圖片描述
完整示例:

/**
 * @author zhangsh A.txt: 12345678900987654321
 */
public class NIOScatterAndGather {

    public static void main(String[] args) throws IOException {
        // RandomAccessFile與其他IOStream最大卻別別
        // 就是可以通過內部指標可以指定檔案讀寫的具體位置
        RandomAccessFile randomAccessFile = new RandomAccessFile("A.txt", "rw");
        randomAccessFile.seek(0);// 從5byte位置開始讀
        FileChannel fileChannel = randomAccessFile.getChannel();
        // 初始化1M連續記憶體空間
        ByteBuffer buf1 = ByteBuffer.allocate(10);
        ByteBuffer buf2 = ByteBuffer.allocate(10);

        ByteBuffer[] bufferArray = { buf1, buf2 };
        fileChannel.read(bufferArray);// scatter:會依次寫入byteBuffer中,前一個byteBuffer寫滿了,繼續寫第二個,以此類推

        // flip()呼叫,然後開始讀
        buf1.flip();
        buf2.flip();

        if (buf1.hasRemaining()) {
            System.out.println("buf1 " + buf1);
            System.out.println(new String(buf1.array()));// 1234567890
        }
        if (buf2.hasRemaining()) {
            System.out.println("buf2 " + buf2);
            System.out.println(new String(buf2.array()));// 0987654321
        }
        ByteBuffer bufEmpty = ByteBuffer.wrap(new String("  ").getBytes());
        fileChannel.write(new ByteBuffer[] { bufEmpty, buf1, buf2 });// gather:會按照陣列入參的順序讀取buffer中position到limit之間的元素
        buf1.clear();
        buf2.clear();

        randomAccessFile.close();
    }
    // console output:
    // 12345678900987654321 12345678900987654321
}