非阻塞I/O(未完待續)

絕大部分知識與實例來自O’REILLY的《Java網絡編程》(Java Network Programming,Fourth Edition,by Elliotte Rusty Harold(O’REILLY))。

非阻塞I/O簡介

非阻塞I/O(NIO)是處理高併發的一種手段。在高併發的情況下,創建和回收線程以及在線程間切換的開銷變得不容忽視,此時就可以使用非阻塞I/O技術。這種技術的核心思想是每次選取一個準備好的連接,儘快地填充這個連接所能管理的儘可能多的數據,然後轉向下一個準備好的連接。

利用非阻塞I/O實現的客戶端

一般情況下,客戶端不會需要處理很高數量的併發連接。事實上,非阻塞I/O主要是爲服務器設計的,但它也可以用在客戶端上。由於客戶端的設計相比服務器容易,因此下面先用客戶端來進行簡單演示。
首先介紹通道(channel)和緩衝區。非阻塞I/O中使用SocketChannel類創建連接。要獲取SocketChannel對象,需要將一個SocketAddress對象(通常會使用它的子類InetSocketAddress)傳入它的靜態工廠方法open()中。下面爲一個示例:

SocketAddress address = new InetSocketAddress("127.0.0.1", 19);
SocketChannel client = SocketChannel.open(address);

open()方法是阻塞的,因此這之後的代碼在連接建立之前不會執行。如果連接無法建立,會拋出一個IOException異常。
連接建立之後就需要獲取輸入和輸出。不同於傳統的getInputStream()與getOutputStream(),利用通道,你可以直接寫入通道本身。不是寫入字節數組,而是要寫入一個ByteBuffer對象。ByteBuffer對象通過ByteBuffer.allocate(int capacity)獲取,capacity爲緩衝區大小,單位爲字節:

ByteBuffer buffer = ByteBuffer.allocate(74);

獲得ByteBuffer對象後,將其傳遞給SocketChannel對象的read()方法,SocketChannel對象會用從Socket讀取的數據填充這個緩衝區。read()方法返回成功讀取並儲存在緩衝區中的字節數。默認情況下,它會至少讀取一個字節,或者返回-1指示數據結束,沒有字節可用時阻塞。這與InputStream的行爲大致相同。但如果設置成非阻塞模式,沒有字節可用時它會立即返回0,不會阻塞。
現在假定緩衝區內已經有了一些數據,之後就需要將它們提取出來。可以使用傳統的方式,先將數據寫入一個字節數組,之後再寫入一個輸出流中。這裏介紹一種完全基於通道的方法:利用Channels工具類將輸出流封裝到一個通道中:

WritableByteChannel out = Channels.newChannel(System.out);

上面的代碼將System.out封裝入一個通道中。這之後就可以進行輸出了。ByteBuffer對象在每次輸出之前,需要調用一下它的flip()方法,使得通道從開頭開始讀。在讀寫完畢後,還需要調用它的clear()方法,重置緩衝區的狀態。下面是進行一次數據輸出的代碼:

buffer.flip();
out.write(buffer);
buffer.clear();

實例1:利用非阻塞I/O實現的CharGenerator(字符生成器)客戶端

服務器代碼:

public static void createCharGeneratorServer(){
    try(ServerSocket server = new ServerSocket(19)){
        while(true){
            try(Socket connection = server.accept()){
                OutputStream out = connection.getOutputStream();

                int firstPrintableCharacter = 33;
                int numberOfPrintableCharacter = 94;
                int numberOfCharactersPerLine = 72;

                int start = firstPrintableCharacter;
                while(true){
                    for(int i = start ;
                            i < start + numberOfCharactersPerLine ; i++){
                        out.write
                        (firstPrintableCharacter + (i - firstPrintableCharacter) % numberOfPrintableCharacter);
                    }
                    out.write('\r');
                    out.write('\n');
                    start = firstPrintableCharacter + (start + 1 - firstPrintableCharacter) % numberOfPrintableCharacter;
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

客戶端代碼:

try {
    SocketAddress address = new InetSocketAddress("127.0.0.1", 19);
    SocketChannel client = SocketChannel.open(address);

    ByteBuffer buffer = ByteBuffer.allocate(74);
        WritableByteChannel out = Channels.newChannel(System.out);

    while(client.read(buffer) != -1){
        buffer.flip();
        out.write(buffer);
        buffer.clear();
    }
} catch (IOException e) {
    e.printStackTrace();
}

輸出(無限循環):
]^_`abcdefghijklmnopqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<=>?@ABCDEF
^_`abcdefghijklmnopqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFG
_`abcdefghijklmnopqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGH
`abcdefghijklmnopqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHI
abcdefghijklmnopqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJ
bcdefghijklmnopqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJK

啓用非阻塞模式

上面的程序和使用輸入/輸出流的傳統方式並沒有太大差別。不過,可以調用ServerSocket的configureBlocking(false)方法將其設置爲非阻塞模式。這個模式下,如果沒有可用的數據,read()方法會立即返回,這讓客戶端可以去做其他事情。不過,由於read()方法在讀不到數據時會返回0,讀取數據的循環需要做一些改動:

while(true){
    //這裏可以寫每次循環都要做的事,無論有沒有讀到數據
    int n = client.read(buffer);
    if(n > 0){
        buffer.flip();
        out.write(buffer);
        buffer.clear();
    }else if (n == -1) {
        //除非服務器故障,否則不會發生
        break;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章