1. 程式人生 > >java學習-NIO(五)NIO學習總結以及NIO新特性介紹

java學習-NIO(五)NIO學習總結以及NIO新特性介紹

我們知道是NIO是在2002年引入到J2SE 1.4裡的,很多Java開發者比如我還是不知道怎麼充分利用NIO,更少的人知道在Java SE 7裡引入了更新的輸入/輸出 API(NIO.2)。但是對於普通的開發者來說基本的I/O操作就夠用了,而NIO則是在處理I/O效能優化方面帶來顯著性效果。更快的速度則意味著NIO和NIO.2的API暴露了更多低層次的系統操作的入口,這對於開發者而言則意味著更復雜的操作和精巧的程式設計。從前面的幾節的講解來看NIO的操作無不繁瑣。要完全掌握還是有點難度的。前面我們講解了Buffer,Channel,Selector,都是從大的面上去探討NIO的主要元件。這一節我們則從NIO的特性方面去探討更細節的一些問題。

1.NIO的新特性

總的來說java 中的IO 和NIO的區別主要有3點:

  1. IO是面向流的,NIO是面向緩衝的;
  2. IO是阻塞的,NIO是非阻塞的;
  3. IO是單執行緒的,NIO 是通過選擇器來模擬多執行緒的;

NIO在基礎的IO流上發展處新的特點,分別是:記憶體對映技術,字元及編碼,非阻塞I/O和檔案鎖定。下面我們分別就這些技術做一些說明。

2. 記憶體對映

這個功能主要是為了提高大檔案的讀寫速度而設計的。記憶體對映檔案(memory-mappedfile)能讓你建立和修改那些大到無法讀入記憶體的檔案。有了記憶體對映檔案,你就可以認為檔案已經全部讀進了記憶體,然後把它當成一個非常大的陣列來訪問了。將檔案的一段區域對映到記憶體中,比傳統的檔案處理速度要快很多。記憶體對映檔案它雖然最終也是要從磁碟讀取資料,但是它並不需要將資料讀取到OS核心緩衝區,而是直接將程序的使用者私有地址空間中的一部分割槽域與檔案物件建立起對映關係,就好像直接從記憶體中讀、寫檔案一樣,速度當然快了。

NIO中記憶體對映主要用到以下兩個類:

  1. java.nio.MappedByteBuffer
  2. java.nio.channels.FileChannel

下面我們通過一個例子來看一下記憶體對映讀取檔案和普通的IO流讀取一個150M大檔案的速度對比:

public class MemMap {
    public static void main(String[] args) {
        try {
            RandomAccessFile file = new RandomAccessFile("c://1.pdf","rw");
            FileChannel channel = file.getChannel();
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY,0
,channel.size()); ByteBuffer buffer1 = ByteBuffer.allocate(1024); byte[] b = new byte[1024]; long len = file.length(); long startTime = System.currentTimeMillis(); //讀取記憶體對映檔案 for(int i=0;i<file.length();i+=1024*10){ if (len - i > 1024) { buffer.get(b); } else { buffer.get(new byte[(int)(len - i)]); } } long endTime = System.currentTimeMillis(); System.out.println("使用記憶體對映方式讀取檔案總耗時: "+(endTime - startTime)); //普通IO流方式 long startTime1 = System.currentTimeMillis(); while(channel.read(buffer1) > 0){ buffer1.flip(); buffer1.clear(); } long endTime1 = System.currentTimeMillis(); System.out.println("使用普通IO流方式讀取檔案總耗時: "+(endTime1 - startTime1)); } catch (Exception e) { e.printStackTrace(); } } }

實驗結果為:

效果對比還是挺明顯的。我們看到在上面程式中呼叫FileChannel類的map方法進行記憶體對映,第一個引數設定對映模式,現在支援3種模式:

  1. FileChannel.MapMode.READ_ONLY:只讀緩衝區,在緩衝區中如果發生寫操作則會產生ReadOnlyBufferException;

  2. FileChannel.MapMode.READ_WRITE:讀寫緩衝區,任何時刻如果通過記憶體對映的方式修改了檔案則立刻會對磁碟上的檔案執行相應的修改操作。別的程序如果也共享了同一個對映,則也會同步看到變化。而不是像標準IO那樣每個程序有各自的核心緩衝區,比如JAVA程式碼中,沒有執行 IO輸出流的 flush() 或者 close() 操作,那麼對檔案的修改不會更新到磁碟去,除非程序執行結束;

  3. FileChannel.MapMode.PRIVATE :這個比較狠,可寫緩衝區,但任何修改是緩衝區私有的,不會回到檔案中。所以盡情的修改吧,結局跟突然停電是一樣的。

我們注意到FileChannel類中有map方法來建立記憶體對映,按理說是否應用的有相應的unmap方法來解除安裝對映記憶體呢。但是竟然沒有找到該方法。一旦建立對映保持有效,直到MappedByteBuffer物件被垃圾收集。 此外,對映緩衝區不會繫結到建立它們的通道。 關閉相關的FileChannel不會破壞對映; 只有緩衝物件本身的處理打破了對映。

記憶體對映檔案的優點:

  1. 使用者程序將檔案資料視為記憶體,因此不需要發出read()或write()系統呼叫。
  2. 當用戶程序觸控對映的記憶體空間時,將自動生成頁面錯誤,以從磁碟引入檔案資料。 如果使用者修改對映的記憶體空間,受影響的頁面將自動標記為髒,並隨後重新整理到磁碟以更新檔案。
  3. 作業系統的虛擬記憶體子系統將執行頁面的智慧快取,根據系統負載自動管理記憶體。
  4. 資料始終是頁面對齊的,不需要緩衝區複製。
  5. 可以對映非常大的檔案,而不消耗大量記憶體來複制資料。

下面我們再寫一個複製檔案的例子來看一下對於一個120M的檔案通過這種方式到底能有多快速度的提升:

public class MemMapReadWrite {

    private static int len;

    /**
     * 讀檔案
     *
     * @param fileName
     * @return
     */
    public static ByteBuffer readFile(String fileName) {
        try {
            RandomAccessFile file = new RandomAccessFile(fileName, "rw");
            len = (int) file.length();
            FileChannel channel = file.getChannel();
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, len);

            return buffer.get(new byte[(int) file.length()]);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 寫檔案
     *
     * @param readFileName
     * @param writeFileName
     */
    public static void writeFile(String readFileName, String writeFileName) {
        try {
            RandomAccessFile file = new RandomAccessFile(writeFileName, "rw");
            FileChannel channel = file.getChannel();
            ByteBuffer buffer = readFile(readFileName);

            MappedByteBuffer bytebuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, len);
            long startTime = System.currentTimeMillis();
            for (int i = 0; i < len; i++) {
                bytebuffer.put(i, buffer.get(i));
            }
            bytebuffer.flip();
            long endTime = System.currentTimeMillis();
            System.out.println("寫檔案耗時: " + (endTime - startTime));


        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        String readFileName = "c://1.pdf";
        String writeFileName = "c://2.pdf";

        writeFile(readFileName, writeFileName);
    }
}

結果為:

這個速度還是相當驚人的!

2. 字元及編碼

說到字元和編碼,我們的先說一個概念,字元編碼方案

編碼方案定義瞭如何把字元編碼的序列表達為位元組序列。字元編碼的數值不需要與編碼位元組相同,也不需要是一對一或一對多個的關係。原則上,把字符集編碼和解碼近似視為物件的序列化和反序列化。

通常字元資料編碼是用於網路傳輸或檔案儲存。編碼方案不是字符集,它是對映;但是因為它們之間的緊密聯絡,大部分編碼都與一個獨立的字符集相關聯。例如,UTF-8,僅用來編碼Unicode字符集。儘管如此,用一個編碼方案處理多個字符集還是可能發生的。例如,EUC可以對幾個亞洲語言的字元進行編碼。

目前字元編碼方案有US-ASCII,UTF-8,GB2312, BIG5,GBK,GB18030,UTF-16BE, UTF-16LE, UTF-16,UNICODE。其中Unicode試圖把全世界所有語言的字符集統一到全面的對映之中。雖然戰友一定的市場份額,但是目前其餘的字元方案仍然廣被採用。大部分的作業系統在I/O與檔案儲存方面仍是以位元組為導向的,所以無論使用何種編碼,Unicode或其他編碼,在位元組序列和字符集編碼之間仍需要進行轉化。

由java.nio.charset包組成的類滿足了這個需求。這不是Java平臺第一次處理字符集編碼,但是它是最系統、最全面、以及最靈活的解決方式。
下面我們通過一個小例子來看一下通過不同的Charset實現如何把字元翻譯成位元組序列:

public class CharsetTest {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        String str = input.next();
        String[] charsetNames = {"US-ASCII", "ISO-8859-1", "UTF-8", "UTF-16BE",
                "UTF-16LE", "UTF-16"
        };

        for (int i = 0; i < charsetNames.length; i++) {
            doEncode(Charset.forName(charsetNames[i]), str);
        }
    }

    private static void doEncode(Charset cs, String input) {
        ByteBuffer bb = cs.encode(input);
        System.out.println("Charset: " + cs.name());
        System.out.println(" Input: " + input);
        System.out.println("Encoded: ");
        for (int i = 0; bb.hasRemaining(); i++) {
            int b = bb.get();
            int ival = ((int) b) & 0xff;
            char c = (char) ival;
            // Keep tabular alignment pretty
            if (i < 10) System.out.print(" ");
            // 列印索引序列
            System.out.print(" " + i + ": ");
            // Better formatted output is coming someday...
            if (ival < 16)
                System.out.print("0");
            // 輸出該位元組位值的16進位制形式
            System.out.print(Integer.toHexString(ival));
            // 打印出剛才我們輸入的字元,如果是空格或者標準字符集中沒有包含
            //該字元輸出空格,否則輸出該字元
            if (Character.isWhitespace(c) || Character.isISOControl(c)) {
                System.out.println("");
            } else {
                System.out.println(" (" + c + ")");
            }
        }
        System.out.println("");
    }

}

輸出為:

abc
Charset: US-ASCII
 Input: abc
Encoded: 
  0: 61 (a)
  1: 62 (b)
  2: 63 (c)

Charset: ISO-8859-1
 Input: abc
Encoded: 
  0: 61 (a)
  1: 62 (b)
  2: 63 (c)

Charset: UTF-8
 Input: abc
Encoded: 
  0: 61 (a)
  1: 62 (b)
  2: 63 (c)

Charset: UTF-16BE
 Input: abc
Encoded: 
  0: 00
  1: 61 (a)
  2: 00
  3: 62 (b)
  4: 00
  5: 63 (c)

Charset: UTF-16LE
 Input: abc
Encoded: 
  0: 61 (a)
  1: 00
  2: 62 (b)
  3: 00
  4: 63 (c)
  5: 00

Charset: UTF-16
 Input: abc
Encoded: 
  0: fe (þ)
  1: ff (ÿ)
  2: 00
  3: 61 (a)
  4: 00
  5: 62 (b)
  6: 00
  7: 63 (c)


Process finished with exit code 0
2.1 字符集編碼器和解碼器

字元的編碼和解碼是使用很頻繁的,試想如果使用UTF-8字符集進行編碼,但是卻是用UTF-16字符集進行解碼,那麼這條資訊對於使用者來說其實是無用的。因為沒人能看得懂。在NIO中提供了兩個類CharsetEncoder和CharsetDecoder來實現編碼轉換方案。

CharsetEncoder類是一個狀態編碼引擎。實際上,編碼器有狀態意味著它們不是執行緒安全的:CharsetEncoder物件不應該線上程中共享。CharsetEncoder物件是一個狀態轉換引擎:字元進去,位元組出來。一些編碼器的呼叫可能需要完成轉換。編碼器儲存在呼叫之間轉換的狀態。

字符集解碼器是編碼器的逆轉。通過特殊的編碼方案把位元組編碼轉化成16-位Unicode字元的序列。與CharsetEncoder類似的, CharsetDecoder也是狀態轉換引擎。

3. 非阻塞IO

一般來說 I/O 模型可以分為:同步阻塞,同步非阻塞,非同步阻塞,非同步非阻塞 四種IO模型。

同步阻塞 IO :

在此種方式下,使用者程序在發起一個 IO 操作以後,必須等待 IO 操作的完成,只有當真正完成了 IO 操作以後,使用者程序才能執行。 JAVA傳統的 IO 模型屬於此種方式!

同步非阻塞 IO:

在此種方式下,使用者程序發起一個 IO 操作以後可以返回做其它事情,但是使用者程序需要時不時的詢問 IO 操作是否就緒,這就要求使用者程序不停的去詢問,從而引入不必要的 CPU 資源浪費。其中目前 JAVA 的 NIO 就屬於同步非阻塞 IO 。

非同步阻塞 IO :

此種方式下是指應用發起一個 IO 操作以後,不等待核心 IO 操作的完成,等核心完成 IO 操作以後會通知應用程式,這其實就是同步和非同步最關鍵的區別,同步必須等待或者主動的去詢問 IO 是否完成,那麼為什麼說是阻塞的呢?因為此時是通過 select 系統呼叫來完成的,而 select 函式本身的實現方式是阻塞的,而採用 select 函式有個好處就是它可以同時監聽多個檔案控制代碼,從而提高系統的併發性!

非同步非阻塞 IO:

在此種模式下,使用者程序只需要發起一個 IO 操作然後立即返回,等 IO 操作真正的完成以後,應用程式會得到 IO 操作完成的通知,此時使用者程序只需要對資料進行處理就好了,不需要進行實際的 IO 讀寫操作,因為 真正的 IO讀取或者寫入操作已經由 核心完成了。目前 Java 中還沒有支援此種 IO 模型。

上面我們說到nio是使用了同步非阻塞模型。我們知道典型的非阻塞IO模型一般如下:

while(true){
    data = socket.read();
    if(data!= error){
        處理資料
        break;
    }
}

但是對於非阻塞IO就有一個非常嚴重的問題,在while迴圈中需要不斷地去詢問核心資料是否就緒,這樣會導致CPU佔用率非常高,因此一般情況下很少使用while迴圈這種方式來讀取資料。所以這就不得不說到下面這個概念–多路複用IO模型。

多路複用IO模型

在多路複用IO模型中,會有一個執行緒不斷去輪詢多個socket的狀態,只有當socket真正有讀寫事件時,才真正呼叫實際的IO讀寫操作。因為在多路複用IO模型中,只需要使用一個執行緒就可以管理多個socket,系統不需要建立新的程序或者執行緒,也不必維護這些執行緒和程序,並且只有在真正有socket讀寫事件進行時,才會使用IO資源,所以它大大減少了資源佔用。

NIO 的非阻塞 I/O 機制是圍繞 選擇器和 通道構建的。 Channel 類表示伺服器和客戶機之間的一種通訊機制。Selector 類是 Channel 的多路複用器。 Selector 類將傳入客戶機請求多路分用並將它們分派到各自的請求處理程式。NIO 設計背後的基石是反應器(Reactor)設計模式。

關於Reactor模式在此就不多做介紹,網上很多。Reactor負責IO事件的響應,一旦有事件發生,便廣播發送給相應的handler去處理。而NIO的設計則是完全按照Reactor模式來設計的。Selector發現某個channel有資料時,會通過SelectorKey來告知,然後實現事件和handler的繫結。

在Reactor模式中,包含如下角色:

  • Reactor 將I/O事件發派給對應的Handler
  • Acceptor 處理客戶端連線請求
  • Handlers 執行非阻塞讀/寫

我們簡單寫一個利用了Reactor模式的NIO服務端:

public class NIOServer {
    private static final Logger LOGGER = LoggerFactory.getLogger(NIOServer.class);

    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(1234));
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            if(selector.selectNow() < 0) {
                continue;
            }
            //獲取註冊的channel
            Set<SelectionKey> keys = selector.selectedKeys();
            //遍歷所有的key
            Iterator<SelectionKey> iterator = keys.iterator();
            while(iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                //如果通道上有事件發生
                if (key.isAcceptable()) {
                    //獲取該通道
                    ServerSocketChannel acceptServerSocketChannel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = acceptServerSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    LOGGER.info("Accept request from {}", socketChannel.getRemoteAddress());
                    //同時將SelectionKey標記為可讀,以便讀取。
                    SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ);
                    //利用SelectionKey的attache功能繫結Acceptor 如果有事情,觸發Acceptor
                    //Processor物件為自定義處理請求的類
                    readKey.attach(new Processor());
                } else if (key.isReadable()) {
                    Processor processor = (Processor) key.attachment();
                    processor.process(key);
                }
            }
        }
    }
}

/**
 * Processor類中設定一個執行緒池來處理請求,
 * 這樣就可以充分利用多執行緒的優勢
 */
class Processor {
    private static final Logger LOGGER = LoggerFactory.getLogger(Processor.class);
    private static final ExecutorService service = Executors.newFixedThreadPool(16);

    public void process(final SelectionKey selectionKey) {
        service.submit(new Runnable() {
            @Override
            public void run() {
                ByteBuffer buffer = null;
                SocketChannel socketChannel = null;
                try {
                    buffer = ByteBuffer.allocate(1024);
                    socketChannel = (SocketChannel) selectionKey.channel();
                    int count = socketChannel.read(buffer);
                    if (count < 0) {
                        socketChannel.close();
                        selectionKey.cancel();
                        LOGGER.info("{}\t Read ended", socketChannel);
                    } else if(count == 0) {
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                LOGGER.info("{}\t Read message {}", socketChannel, new String(buffer.array()));
            }
        });
    }
}

這種方式帶來的好處也是不言而喻的。利用多路複用機制避免了執行緒的阻塞,提高了連線的數量。一個執行緒就可以管理多個socket,只有當socket真正有讀寫事件發生才會佔用資源來進行實際的讀寫操作。雖然多執行緒+ 阻塞IO 達到類似的效果,但是由於在多執行緒 + 阻塞IO 中,每個socket對應一個執行緒,這樣會造成很大的資源佔用,並且尤其是對於長連線來說,執行緒的資源一直不會釋放,如果後面陸續有很多連線的話,就會造成效能上的瓶頸。

另外多路複用IO為何比非阻塞IO模型的效率高是因為在非阻塞IO中,不斷地詢問socket狀態時通過使用者執行緒去進行的,而在多路複用IO中,輪詢每個socket狀態是核心在進行的,這個效率要比使用者執行緒要高的多。

4. 檔案鎖定

NIO中的檔案通道(FileChannel)在讀寫資料的時候主 要使用了阻塞模式,它不能支援非阻塞模式的讀寫,而且FileChannel的物件是不能夠直接例項化的, 他的例項只能通過getChannel()從一個開啟的檔案物件上邊讀取(RandomAccessFile、 FileInputStream、FileOutputStream),並且通過呼叫getChannel()方法返回一個 Channel物件去連線同一個檔案,也就是針對同一個檔案進行讀寫操作。

檔案鎖的出現解決了很多Java應用程式和非Java程式之間共享檔案資料的問題,在以前的JDK版本中,沒有檔案鎖機制使得Java應用程式和其他非Java程序程式之間不能夠針對同一個檔案共享 資料,有可能造成很多問題,JDK1.4裡面有了FileChannel,它的鎖機制使得檔案能夠針對很多非 Java應用程式以及其他Java應用程式可見。但是Java裡面 的檔案鎖機制主要是基於共 享鎖模型,在不支援共享鎖模型的作業系統上,檔案鎖本身也起不了作用,JDK1.4使用檔案通道讀寫方式可以向一些檔案 傳送鎖請求,
FileChannel的 鎖模型主要針對的是每一個檔案,並不是每一個執行緒和每一個讀寫通道,也就是以檔案為中心進行共享以及獨佔,也就是檔案鎖本身並不適合於同一個JVM的不同 執行緒之間。

我們簡要看一下相關API:

// 如果請求的鎖定範圍是有效的,阻塞直至獲取鎖
 public final FileLock lock()  
// 嘗試獲取鎖非阻塞,立刻返回結果  
 public final FileLock tryLock()  

// 第一個引數:要鎖定區域的起始位置  
// 第二個引數:要鎖定區域的尺寸,  
// 第三個引數:true為共享鎖,false為獨佔鎖  
 public abstract FileLock lock (long position, long size, boolean shared)  
 public abstract FileLock tryLock (long position, long size, boolean shared) 

鎖定區域的範圍不一定要限制在檔案的size值以內,鎖可以擴充套件從而超出檔案尾。因此,我們可以提前把待寫入資料的區域鎖定,我們也可以鎖定一個不包含任何檔案內容的區域,比如檔案最後一個位元組以外的區域。如果之後檔案增長到達那塊區域,那麼你的檔案鎖就可以保護該區域的檔案內容了。相反地,如果你鎖定了檔案的某一塊區域,然後檔案增長超出了那塊區域,那麼新增加 的檔案內容將不會受到您的檔案鎖的保護。

我們寫一個簡單例項:

public class NIOLock {
    private static final Logger LOGGER = LoggerFactory.getLogger(NIOServer.class);
    public static void main(String[] args) throws IOException {
        FileChannel fileChannel = new RandomAccessFile("c://1.txt", "rw").getChannel();
        // 寫入4個位元組
        fileChannel.write(ByteBuffer.wrap("abcd".getBytes()));
        // 將前2個位元組區域鎖定(共享鎖)
        FileLock lock1 = fileChannel.lock(0, 2, true);
        // 當前鎖持有鎖的型別(共享鎖/獨佔鎖)
        lock1.isShared();
        // IOException 不能修改只讀的共享區域
        // fileChannel.write(ByteBuffer.wrap("a".getBytes()));
        // 可以修改共享鎖之外的區域,從第三個位元組開始寫入
        fileChannel.write(ByteBuffer.wrap("ef".getBytes()), 2);

        // OverlappingFileLockException 重疊的檔案鎖異常
        // FileLock lock2 = fileChannel.lock(0, 3, true);
        // FileLock lock3 = fileChannel.lock(0, 3, false);

        //得到建立鎖的通道
        lock1.channel();

        //鎖的起始位置
        long position = lock1.position();

        //鎖的範圍
        long size = lock1.size();

        //判斷鎖是否與指定檔案區域有重疊
        lock1.overlaps(position, size);

        // 記得用try/catch/finally{release()}方法釋放鎖
        lock1.release();
    }
}

上面我們總結了NIO的4個新特性,對於IO來說都是很重要的功能以及效能的升級。下面我們寫一個完整的NIO Socket客戶端和服務端,總結一下NIO 的用法,每一行都加了註釋:

服務端:

public class Server {

    //標識數字/
    private int flag = 0;
    //緩衝區大小/
    private int BLOCK = 4096;
    //接受資料緩衝區/
    private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
    //傳送資料緩衝區/
    private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
    private Selector selector;


    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        int port = 7788;
        Server server = new Server(port);
        server.listen();
    }

    public Server(int port) throws IOException {
        // 開啟伺服器套接字通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 伺服器配置為非阻塞
        serverSocketChannel.configureBlocking(false);
        // 檢索與此通道關聯的伺服器套接字
        ServerSocket serverSocket = serverSocketChannel.socket();
        // 進行服務的繫結
        serverSocket.bind(new InetSocketAddress(port));
        // 通過open()方法找到Selector
        selector = Selector.open();
        // 註冊到selector,等待連線
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Server Start----7788:");
    }


    // 監聽
    private void listen() throws IOException {
        while (true) {
            // 選擇一組鍵,並且相應的通道已經開啟
            selector.select();
            // 返回此選擇器的已選擇鍵集。
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                iterator.remove();
                handleKey(selectionKey);
            }
        }
    }

    // 處理請求
    private void handleKey(SelectionKey selectionKey) throws IOException {
        // 接受請求
        ServerSocketChannel server = null;
        SocketChannel client = null;
        String receiveText;
        String sendText;
        int count = 0;
        // 測試此鍵的通道是否已準備好接受新的套接字連線。
        if (selectionKey.isAcceptable()) {
            // 返回為之建立此鍵的通道。
            server = (ServerSocketChannel) selectionKey.channel();
            // 接受到此通道套接字的連線。
            // 此方法返回的套接字通道(如果有)將處於阻塞模式。
            client = server.accept();
            // 配置為非阻塞
            client.configureBlocking(false);
            // 註冊到selector,等待連線
            client.register(selector, SelectionKey.OP_READ);
        } else if (selectionKey.isReadable()) {
            // 返回為之建立此鍵的通道。
            client = (SocketChannel) selectionKey.channel();
            //將緩衝區清空以備下次讀取
            receivebuffer.clear();
            //讀取伺服器傳送來的資料到緩衝區中
            count = client.read(receivebuffer);
            if (count > 0) {
                receiveText = new String(receivebuffer.array(), 0, count);
                System.out.println("伺服器端接受客戶端資料--:" + receiveText);
                client.register(selector, SelectionKey.OP_WRITE);
            }
        } else if (selectionKey.isWritable()) {
            //將緩衝區清空以備下次寫入
            sendbuffer.clear();
            // 返回為之建立此鍵的通道。
            client = (SocketChannel) selectionKey.channel();
            sendText = "message from server--" + flag++;
            //向緩衝區中輸入資料
            sendbuffer.put(sendText.getBytes());
            //將緩衝區各標誌復位,因為向裡面put了資料標誌被改變要想從中讀取資料發向伺服器,就要復位
            sendbuffer.flip();
            //輸出到通道
            client.write(sendbuffer);
            System.out.println("伺服器端向客戶端傳送資料--:" + sendText);
            client.register(selector, SelectionKey.OP_READ);
        }
    }


}

客戶端:

public class Client {
    //標識數字/
    private static int flag = 0;
    //緩衝區大小/
    private static int BLOCK = 4096;
    //接受資料緩衝區/
    private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
    //傳送資料緩衝區/
    private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
    //伺服器端地址/
    private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
            "localhost", 7788);

    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        // 開啟socket通道
        SocketChannel socketChannel = SocketChannel.open();
        // 設定為非阻塞方式
        socketChannel.configureBlocking(false);
        // 開啟選擇器
        Selector selector = Selector.open();
        // 註冊連線服務端socket動作
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        // 連線
        socketChannel.connect(SERVER_ADDRESS);
        // 分配緩衝區大小記憶體

        Set<SelectionKey> selectionKeys;
        Iterator<SelectionKey> iterator;
        SelectionKey selectionKey;
        SocketChannel client;
        String receiveText;
        String sendText;
        int count = 0;

        while (true) {
            //選擇一組鍵,其相應的通道已為 I/O 操作準備就緒。
            //此方法執行處於阻塞模式的選擇操作。
            selector.select();
            //返回此選擇器的已選擇鍵集。
            selectionKeys = selector.selectedKeys();
            //System.out.println(selectionKeys.size());
            iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                selectionKey = iterator.next();
                if (selectionKey.isConnectable()) {
                    System.out.println("client connect");
                    client = (SocketChannel) selectionKey.channel();
                    // 判斷此通道上是否正在進行連線操作。
                    // 完成套接字通道的連線過程。
                    if (client.isConnectionPending()) {
                        client.finishConnect();
                        System.out.println("完成連線!");
                        sendbuffer.clear();
                        sendbuffer.put("Hello,Server".getBytes());
                        sendbuffer.flip();
                        client.write(sendbuffer);
                    }
                    client.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    client = (SocketChannel) selectionKey.channel();
                    //將緩衝區清空以備下次讀取
                    receivebuffer.clear();
                    //讀取伺服器傳送來的資料到緩衝區中
                    count = client.read(receivebuffer);
                    if (count > 0) {
                        receiveText = new String(receivebuffer.array(), 0, count);
                        System.out.println("客戶端接受伺服器端資料--:" + receiveText);
                        client.register(selector, SelectionKey.OP_WRITE);
                    }

                } else if (selectionKey.isWritable()) {
                    sendbuffer.clear();
                    client = (SocketChannel) selectionKey.channel();
                    sendText = "message from client--" + (flag++);
                    sendbuffer.put(sendText.getBytes());
                    //將緩衝區各標誌復位,因為向裡面put了資料標誌被改變要想從中讀取資料發向伺服器,就要復位
                    sendbuffer.flip();
                    client.write(sendbuffer);
                    System.out.println("客戶端向伺服器端傳送資料--:" + sendText);
                    client.register(selector, SelectionKey.OP_READ);
                }
            }
            selectionKeys.clear();
        }
    }
}