1. 程式人生 > 實用技巧 >JDK NIO基礎概念與原理

JDK NIO基礎概念與原理

轉載自:https://www.cnblogs.com/wade-luffy/p/6164668.html

原文如下:

我們首先需要澄清一個概念:NIO到底是什麼的簡稱?有人稱之為New I/O,因為它相對於之前的I/O類庫是新增的,所以被稱為New I/O,這是它的官方叫法。但是,由於之前老的I/O類庫是阻塞I/O,New I/O類庫的目標就是要讓Java支援非阻塞I/O,所以,更多的人喜歡稱之為非阻塞I/O(Non-block I/O),由於非阻塞I/O更能夠體現NIO的特點。

與Socket類和ServerSocket類相對應,NIO也提供了SocketChannel和ServerSocketChannel兩種不同的套接字通道實現。這兩種新增的通道支援阻塞和非阻塞兩種模式。阻塞模式使用非常簡單,但是效能和可靠性都不好,非阻塞模式則正好相反。開發人員一般可以根據自己的需要來選擇合適的模式,一般來說,低負載、低併發的應用程式可以選擇同步阻塞I/O以降低程式設計複雜度,但是對於高負載、高併發的網路應用,需要使用NIO的非阻塞模式進行開發。


NIO類庫簡介

新的輸入/輸出(NIO)庫是在JDK 1.4中引入的。NIO彌補了原來同步阻塞I/O的不足,它在標準Java程式碼中提供了高速的、面向塊的I/O。通過定義包含資料的類,以及通過以塊的形式處理這些資料,NIO不用使用本機程式碼就可以利用低階優化,這是原來的I/O包所無法做到的。

1.緩衝區Buffer

我們首先介紹緩衝區(Buffer)的概念,Buffer是一個物件,它包含一些要寫入或者要讀出的資料。在NIO類庫中加入Buffer物件,體現了新庫與原I/O的一個重要區別。在面向流的I/O中,可以將資料直接寫入或者將資料直接讀到Stream物件中。

在NIO庫中,所有資料都是用緩衝區處理的。在讀取資料時,它是直接讀到緩衝區中的;在寫入資料時,寫入到緩衝區中。任何時候訪問NIO中的資料,都是通過緩衝區進行操作。

緩衝區實質上是一個數組。通常它是一個位元組陣列(ByteBuffer),也可以使用其他種類的陣列。但是一個緩衝區不僅僅是一個數組,緩衝區提供了對資料的結構化訪問以及維護讀寫位置(limit)等資訊。

最常用的緩衝區是ByteBuffer,一個ByteBuffer提供了一組功能用於操作byte陣列。除了ByteBuffer,還有其他的一些緩衝區,事實上,每一種Java基本型別(除了Boolean型別)都對應有一種緩衝區,具體如下:

ByteBuffer:位元組緩衝區

CharBuffer:字元緩衝區

ShortBuffer:短整型緩衝區

IntBuffer:整形緩衝區

LongBuffer:長整形緩衝區

FloatBuffer:浮點型緩衝區

DoubleBuffer:雙精度浮點型緩衝區

每一個Buffer類都是Buffer介面的一個子例項。除了ByteBuffer,每一個 Buffer類都有完全一樣的操作,只是它們所處理的資料型別不一樣。因為大多數標準I/O操作都使用ByteBuffer,所以它除了具有一般緩衝區的操作之外還提供一些特有的操作,方便網路讀寫。

2.通道Channel

Channel是一個通道,可以通過它讀取和寫入資料,它就像自來水管一樣,網路資料通過Channel讀取和寫入。通道與流的不同之處在於通道是雙向的,流只是在一個方向上移動(一個流必須是InputStream或者OutputStream的子類),而且通道可以用於讀、寫或者同時用於讀寫。

因為Channel是全雙工的,所以它可以比流更好地對映底層作業系統的API。特別是在UNIX網路程式設計模型中,底層作業系統的通道都是全雙工的,同時支援讀寫操作。

自頂向下看,前三層主要是Channel介面,用於定義它的功能,後面是一些具體的功能類(抽象類),從類圖可以看出,實際上Channel可以分為兩大類:分別是用於網路讀寫的SelectableChannel和用於檔案操作的FileChannel。

3.多路複用器Selector

多路複用器Selector,它是Java NIO程式設計的基礎,熟練地掌握Selector對於掌握NIO程式設計至關重要。多路複用器提供選擇已經就緒的任務的能力。簡單來講,Selector會不斷地輪詢註冊在其上的Channel,如果某個Channel上面有新的TCP連線接入、讀和寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然後通過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作。

一個多路複用器Selector可以同時輪詢多個Channel,由於JDK使用了epoll()代替傳統的select實現,所以它並沒有最大連線控制代碼1024/2048的限制。這也就意味著只需要一個執行緒負責Selector的輪詢,就可以接入成千上萬的客戶端,這確實是個非常巨大的進步。

回到頂部

NIO服務端序列圖

下面,我們對NIO服務端的主要建立過程進行講解和說明,作為NIO的基礎入門,我們將忽略掉一些在生產環境中部署所需要的一些特性和功能。

步驟一:開啟ServerSocketChannel,用於監聽客戶端的連線,它是所有客戶端連線的父管道,程式碼示例如下。

ServerSocketChannel acceptorSvr = ServerSocketChannel.open();

步驟二:繫結監聽埠,設定連線為非阻塞模式,示例程式碼如下。

acceptorSvr.socket().bind(new InetSocketAddress(InetAddress.getByName(“IP”), port));

acceptorSvr.configureBlocking(false);

步驟三:建立Reactor執行緒,建立多路複用器並啟動執行緒,程式碼如下。

Selector selector = Selector.open();

new Thread(new ReactorTask()).start();

步驟四:將ServerSocketChannel註冊到Reactor執行緒的多路複用器Selector上,監聽ACCEPT事件,程式碼如下。

SelectionKey key = acceptorSvr.register( selector, SelectionKey.OP_ACCEPT, ioHandler);

步驟五:多路複用器線上程run方法的無限迴圈體內輪詢準備就緒的Key,程式碼如下。

int num = selector.select();

Set selectedKeys = selector.selectedKeys();

Iterator it = selectedKeys.iterator();

while (it.hasNext()) {

SelectionKey key = (SelectionKey)it.next();

// ... deal with I/O event ...

}

步驟六:多路複用器監聽到有新的客戶端接入,處理新的接入請求,完成TCP三次握手,建立物理鏈路,程式碼示例如下。

SocketChannel channel = svrChannel.accept();

步驟七:設定客戶端鏈路為非阻塞模式,示例程式碼如下。

channel.configureBlocking(false);

channel.socket().setReuseAddress(true);

......

步驟八:將新接入的客戶端連線註冊到Reactor執行緒的多路複用器上,監聽讀操作,用來讀取客戶端傳送的網路訊息,程式碼如下。

SelectionKey key = socketChannel.register( selector, SelectionKey.OP_READ, ioHandler);

步驟九:非同步讀取客戶端請求訊息到緩衝區,示例程式碼如下。

int readNumber = channel.read(receivedBuffer);

步驟十:對ByteBuffer進行編解碼,如果有半包訊息指標reset,繼續讀取後續的報文,將解碼成功的訊息封裝成Task,投遞到業務執行緒池中,進行業務邏輯編排。

Object message = null;

while(buffer.hasRemain())

{

  byteBuffer.mark();

  Object message = decode(byteBuffer);

  if (message == null)

  {

    byteBuffer.reset();

    break;

  }

  messageList.add(message );

}

if (!byteBuffer.hasRemain())

  byteBuffer.clear();

else

  byteBuffer.compact();

if (messageList != null & !messageList.isEmpty())

{

  for(Object messageE : messageList)

    handlerTask(messageE);

}

步驟十一:將POJO物件encode成ByteBuffer,呼叫SocketChannel的非同步write介面,將訊息非同步傳送給客戶端,示例程式碼如下。

socketChannel.write(buffer);

注意:如果傳送區TCP緩衝區滿,會導致寫半包,此時,需要註冊監聽寫操作位,迴圈寫,直到整包訊息寫入TCP緩衝區。

服務端程式碼示例:

import java.io.IOException;

public class TimeServer {

    public static void main(String[] args) throws IOException {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {
                // 採用預設值
            }
        }
        //MultiplexerTimeServer的多路複用類,它是個一個獨立的執行緒,
        //負責輪詢多路複用器Selector,可以處理多個客戶端的併發接入。
        MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
        new Thread (timeServer, "NIO-MultiplexerTimeServer-001").start();
    }
}

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class MultiplexerTimeServer implements Runnable {

    private Selector selector;

    private ServerSocketChannel servChannel;

    private volatile boolean stop;

    //在構造方法中進行資源初始化,建立多路複用器Selector、ServerSocketChannel,對Channel和TCP引數進行配置。
    //例如,將ServerSocketChannel設定為非同步非阻塞模式,它的backlog設定為1024。
    //系統資源初始化成功後,將ServerSocket Channel註冊到Selector,監聽SelectionKey.OP_ACCEPT操作位;如果資源初始化失敗(例如埠被佔用),則退出。
    public MultiplexerTimeServer(int port) {
        try {
            selector = Selector.open();
            servChannel = ServerSocketChannel.open();
            servChannel.configureBlocking(false);
            servChannel.socket().bind(new InetSocketAddress(port), 1024);
            servChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("The time server is start in port : " + port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop() {
        this.stop = true;
    }

    @Override
    public void run() {
        while (!stop) {
            try {
                //線上程的run方法的while迴圈體中迴圈遍歷selector,它的休眠時間為1s,
                //無論是否有讀寫等事件發生,selector每隔1s都被喚醒一次,selector也提供了一個無參的select方法。
                //當有處於就緒狀態的Channel時,selector將返回就緒狀態的Channel的SelectionKey集合,
                //通過對就緒狀態的Channel集合進行迭代,可以進行網路的非同步讀寫操作。
                selector.select(1000);
                Set selectedKeys = selector.selectedKeys();
                Iterator it = selectedKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = (SelectionKey) it.next();
                    it.remove();
                    try {
                        handleInput(key);//這裡可以用執行緒池啟執行緒去單獨處理客戶端的請求業務
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null)
                                key.channel().close();
                        }
                    }
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }

        // 多路複用器關閉後,所有註冊在上面的Channel和Pipe等資源都會被自動去註冊並關閉,所以不需要重複釋放資源
        if (selector != null)
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }

    private void handleInput(SelectionKey key) throws IOException {

        if (key.isValid()) {
            //根據SelectionKey的操作位進行判斷即可獲知網路事件的型別,
            if (key.isAcceptable()) {
                //通過ServerSocketChannel的accept接收客戶端的連線請求並建立SocketChannel例項,
                //完成上述操作後,相當於完成了TCP的三次握手,TCP物理鏈路正式建立。
                //注意,我們需要將新建立的SocketChannel設定為非同步非阻塞,同時也可以對其TCP引數進行設定,
                //例如TCP接收和傳送緩衝區的大小等,作為入門的例子,沒有進行額外的引數設定。
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssc.accept();
                sc.configureBlocking(false);
                // Add the new connection to the selector
                sc.register(selector, SelectionKey.OP_READ);
            }
            if (key.isReadable()) {
                //首先建立一個ByteBuffer,由於我們事先無法得知客戶端傳送的碼流大小,
                //作為例程,我們開闢一個1M的緩衝區。然後呼叫SocketChannel的read方法讀取請求碼流。
                //注意,由於我們已經將SocketChannel設定為非同步非阻塞模式,因此它的read是非阻塞的。
                //使用返回值進行判斷,看讀取到的位元組數
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                //返回值有以下三種可能的結果
                //返回值大於0:讀到了位元組,對位元組進行編解碼;
                //返回值等於0:沒有讀取到位元組,屬於正常場景,忽略;
                //返回值為-1:鏈路已經關閉,需要關閉SocketChannel,釋放資源。
                if (readBytes > 0) {
                    //當讀取到碼流以後,我們進行解碼,首先對readBuffer進行flip操作,
                    //它的作用是將緩衝區當前的limit設定為position,position設定為0,用於後續對緩衝區的讀取操作。
                    //然後根據緩衝區可讀的位元組個數建立位元組陣列,
                    //呼叫ByteBuffer的get操作將緩衝區可讀的位元組陣列複製到新建立的位元組陣列中,
                    //最後呼叫字串的建構函式建立請求訊息體並列印。
                    //如果請求指令是"QUERY TIME ORDER"則把伺服器的當前時間編碼後返回給客戶端
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("The time server receive order : "
                            + body);
                    String currentTime = "QUERY TIME ORDER"
                            .equalsIgnoreCase(body) ? new java.util.Date(
                            System.currentTimeMillis()).toString()
                            : "BAD ORDER";
                    //非同步傳送應答訊息給客戶端
                    doWrite(sc, currentTime);
                } else if (readBytes < 0) {
                    // 對端鏈路關閉
                    key.cancel();
                    sc.close();
                } else
                    ; // 讀到0位元組,忽略
            }
        }
    }

    private void doWrite(SocketChannel channel, String response)
            throws IOException {
        //首先將字串編碼成位元組陣列,根據位元組陣列的容量建立ByteBuffer,
        //呼叫ByteBuffer的put操作將位元組陣列複製到緩衝區中,然後對緩衝區進行flip操作,
        //最後呼叫SocketChannel的write方法將緩衝區中的位元組陣列傳送出去。
        //需要指出的是,由於SocketChannel是非同步非阻塞的,它並不保證一次能夠把需要傳送的位元組陣列傳送完,
        //此時會出現“寫半包”問題,我們需要註冊寫操作,不斷輪詢Selector將沒有傳送完的ByteBuffer傳送完畢,
        //可以通過ByteBuffer的hasRemain()方法判斷訊息是否傳送完成。
        //此處僅僅是個簡單的入門級例程,沒有演示如何處理“寫半包”場景。
        if (response != null && response.trim().length() > 0) {
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
        }
    }
}
回到頂部

NIO客戶端序列圖

步驟一:開啟SocketChannel,繫結客戶端本地地址(可選,預設系統會隨機分配一個可用的本地地址),示例程式碼如下。

SocketChannel clientChannel = SocketChannel.open();

步驟二:設定SocketChannel為非阻塞模式,同時設定客戶端連線的TCP引數,示例程式碼如下。

clientChannel.configureBlocking(false);

socket.setReuseAddress(true);

socket.setReceiveBufferSize(BUFFER_SIZE);

socket.setSendBufferSize(BUFFER_SIZE);

步驟三:非同步連線服務端,示例程式碼如下。

boolean connected=clientChannel.connect(new InetSocketAddress(“ip”,port));

步驟四:判斷是否連線成功,如果連線成功,則直接註冊讀狀態位到多路複用器中,如果當前沒有連線成功(非同步連線,返回false,說明客戶端已經發送sync包,服務端沒有返回ack包,物理鏈路還沒有建立),示例程式碼如下。

if (connected)

{

  clientChannel.register( selector, SelectionKey.OP_READ, ioHandler);

}

else

{

  clientChannel.register( selector, SelectionKey.OP_CONNECT, ioHandler);

}

步驟五:向Reactor執行緒的多路複用器註冊OP_CONNECT狀態位,監聽服務端的TCP ACK應答,示例程式碼如下。

  clientChannel.register( selector, SelectionKey.OP_CONNECT, ioHandler);

步驟六:建立Reactor執行緒,建立多路複用器並啟動執行緒,程式碼如下。

  Selector selector = Selector.open();

  new Thread(new ReactorTask()).start();

步驟七:多路複用器線上程run方法的無限迴圈體內輪詢準備就緒的Key,程式碼如下。

int num = selector.select();

Set selectedKeys = selector.selectedKeys();

Iterator it = selectedKeys.iterator();

while (it.hasNext()) {

  SelectionKey key = (SelectionKey)it.next();

  // ... deal with I/O event ...

}

步驟八:接收connect事件進行處理,示例程式碼如下。

if (key.isConnectable())

  handlerConnect();

步驟九:判斷連線結果,如果連線成功,註冊讀事件到多路複用器,示例程式碼如下。

if (channel.finishConnect())

  registerRead();

步驟十:註冊讀事件到多路複用器,示例程式碼如下。

clientChannel.register( selector, SelectionKey.OP_READ, ioHandler);

步驟十一:非同步讀客戶端請求訊息到緩衝區,示例程式碼如下。

int readNumber = channel.read(receivedBuffer);

步驟十二:對ByteBuffer進行編解碼,如果有半包訊息接收緩衝區Reset,繼續讀取後續的報文,將解碼成功的訊息封裝成Task,投遞到業務執行緒池中,進行業務邏輯編排,示例程式碼如下。

Object message = null;

while(buffer.hasRemain())

{

  byteBuffer.mark();

  Object message = decode(byteBuffer);

  if (message == null)

  {

    byteBuffer.reset();

    break;

  }

  messageList.add(message );

}

if (!byteBuffer.hasRemain())

  byteBuffer.clear();

else

  byteBuffer.compact();

if (messageList != null & !messageList.isEmpty())

{

  for(Object messageE : messageList)

    handlerTask(messageE);

}

步驟十三:將POJO物件encode成ByteBuffer,呼叫SocketChannel的非同步write介面,將訊息非同步傳送給客戶端,示例程式碼如下。

socketChannel.write(buffer);

客戶端程式碼示例:

public class TimeClient {

    public static void main(String[] args) {
        int port = 8080;
        new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient- 001").start();
    }
}

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class TimeClientHandle implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop;

    public TimeClientHandle(String host, int port) {
        //建構函式用於初始化NIO的多路複用器和SocketChannel物件。
        //需要注意的是,建立SocketChannel之後,需要將其設定為非同步非阻塞模式。
        //我們可以設定SocketChannel的TCP引數,例如接收和傳送的TCP緩衝區大小。
        this.host = host == null ? "127.0.0.1" : host;
        this.port = port;
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    @Override
    public void run() {
        try {
            //作為示例,連線是成功的,所以不需要做重連操作,因此將其放到迴圈之前。
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        while (!stop) {
            try {
                //在迴圈體中輪詢多路複用器Selector,當有就緒的Channel時,執行handleInput(key)方法
                selector.select(1000);
                Set selectedKeys = selector.selectedKeys();
                Iterator it = selectedKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = (SelectionKey) it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null)
                                key.channel().close();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                System.exit(1);
            }
        }

        //執行緒退出迴圈後,我們需要對連線資源進行釋放,以實現“優雅退出”.
        //由於多路複用器上可能註冊成千上萬的Channel或者pipe,如果一一對這些資源進行釋放顯然不合適。
        //因此,JDK底層會自動釋放所有跟此多路複用器關聯的資源。
        //多路複用器關閉後,所有註冊在上面的Channel和Pipe等資源都會被自動去註冊並關閉,所以不需要重複釋放資源
        if (selector != null)
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }

    private void handleInput(SelectionKey key) throws IOException {
        //我們首先對SelectionKey進行判斷,看它處於什麼狀態。
        if (key.isValid()) {
            // 判斷是否連線成功
            SocketChannel sc = (SocketChannel) key.channel();
            //如果是處於連線狀態,說明服務端已經返回ACK應答訊息。
            //這時我們需要對連線結果進行判斷,呼叫SocketChannel的finishConnect()方法,
            //如果返回值為true,說明客戶端連線成功;如果返回值為false或者直接丟擲IOException,說明連線失敗。
            //在本例程中,返回值為true,說明連線成功。
            if (key.isConnectable()) {
                if (sc.finishConnect()) {
                    //將SocketChannel註冊到多路複用器上,註冊SelectionKey.OP_READ操作位,
                    //監聽網路讀操作,然後傳送請求訊息給服務端。
                    sc.register(selector, SelectionKey.OP_READ);
                    doWrite(sc);
                } else
                    System.exit(1);// 連線失敗,程序退出
            }
            //客戶端是如何讀取時間伺服器應答訊息的。
            if (key.isReadable()) {
                //如果客戶端接收到了服務端的應答訊息,則SocketChannel是可讀的,
                //由於無法事先判斷應答碼流的大小,我們就預分配1M的接收緩衝區用於讀取應答訊息,
                //呼叫SocketChannel的read()方法進行非同步讀取操作。由於是非同步操作,所以必須對讀取的結果進行判斷。
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0) {
                    //如果讀取到了訊息,則對訊息進行解碼,最後列印結果。執行完成後將stop置為true,執行緒退出迴圈。
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("Now is : " + body);
                    this.stop = true;
                } else if (readBytes < 0) {
                    // 對端鏈路關閉
                    key.cancel();
                    sc.close();
                } else
                    ; // 讀到0位元組,忽略
            }
        }

    }

    //首先對SocketChannel的connect()操作進行判斷,如果連線成功,
    //則將SocketChannel註冊到多路複用器Selector上,註冊SelectionKey.OP_READ,
    //如果沒有直接連線成功,則說明服務端沒有返回TCP握手應答訊息,
    //但這並不代表連線失敗,我們需要將SocketChannel註冊到多路複用器Selector上,
    //註冊SelectionKey.OP_CONNECT,當服務端返回TCP syn-ack訊息後,
    //Selector就能夠輪詢到這個SocketChannel處於連線就緒狀態。
    private void doConnect() throws IOException {
        // 如果直接連線成功,則註冊到多路複用器上,傳送請求訊息,讀應答
        if (socketChannel.connect(new InetSocketAddress(host, port))) {
            socketChannel.register(selector, SelectionKey.OP_READ);
            doWrite(socketChannel);
        } else {
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    //構造請求訊息體,然後對其編碼,寫入到傳送緩衝區中,最後呼叫SocketChannel的write方法進行傳送。
    //由於傳送是非同步的,所以會存在“半包寫”問題。最後通過hasRemaining()方法對傳送結果進行判斷,
    //如果緩衝區中的訊息全部發送完成,列印"Send order 2 server succeed."
    private void doWrite(SocketChannel sc) throws IOException {
        byte[] req = "QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        sc.write(writeBuffer);
        if (!writeBuffer.hasRemaining())
            System.out.println("Send order 2 server succeed.");
    }
}

我們發現NIO程式設計難度確實比同步阻塞BIO大很多,我們的NIO例程並沒有考慮“半包讀”和“半包寫”,如果加上這些,程式碼將會更加複雜。NIO程式碼既然這麼複雜,為什麼它的應用卻越來越廣泛呢,使用NIO程式設計的優點總結如下。

(1)客戶端發起的連線操作是非同步的,可以通過在多路複用器註冊OP_CONNECT等待後續結果,不需要像之前的客戶端那樣被同步阻塞。

(2)SocketChannel的讀寫操作都是非同步的,如果沒有可讀寫的資料它不會同步等待,直接返回,這樣I/O通訊執行緒就可以處理其他的鏈路,不需要同步等待這個鏈路可用。

(3)執行緒模型的優化:由於JDK的Selector在Linux等主流作業系統上通過epoll實現,它沒有連線控制代碼數的限制(只受限於作業系統的最大控制代碼數或者對單個程序的控制代碼限制),這意味著一個Selector執行緒可以同時處理成千上萬個客戶端連線,而且效能不會隨著客戶端的增加而線性下降,因此,它非常適合做高效能、高負載的網路伺服器。

JDK1.7升級了NIO類庫,升級後的NIO類庫被稱為NIO2.0,引人注目的是,Java正式提供了非同步檔案I/O操作,同時提供了與UNIX網路程式設計事件驅動I/O對應的AIO。