基於NIO實現非阻塞Socket編程

一、 描述

Java 提供的 NIO API 來開發高性能網絡服務器, JDK 1.4 以前 的網絡通信程序是基於阻塞式 API 的——即當程序執行輸入、輸出操作後,在這些操作返回之前會一直阻塞該線程,所以服務器必須爲每個客戶端都提供一條獨立線程進行處理,當服務器需要同時處理大量客戶端時,這種做法會導致性能下降。使用 NIO API 則可以讓服務器使用一個或有限幾個線程來同時處理連接到服務器上的所有客戶端。

NIO 使用面向緩衝 (buffer) 的模型。這就是說, NIO 主要處理大塊的數據。這就避免了利用流模型處理所引起的問題,在有可能的情況下,它甚至可以爲了得到最大的吞吐量而使用系統級的工具。基本流 InputStream OutputStream 能夠讀寫字節數據;它們的子類可以讀寫各種各樣的數據。在 NIO 中,所有的數據都通過緩衝讀寫。從圖 1 可以看到兩種模型的比較:

 

 

 

 

 

 

1. 流模型使用 Streams Bytes NIO 模型使用 Channels Buffers

使用緩衝的好處:

A.         它可以大塊的處理數據。你可以讀寫大塊數據,緩衝的大小隻受你所分配的內存數量的限制。

B.         它可以表示系統級的緩衝。多種系統採用統一的內存配置完成 I/O 處理,而不需要將數據從系統內存中拷貝到應用程序的內存空間。 buffer 對象的不同實現可以直接表示這些系統級的緩衝,這就意味着你可以用最少的拷貝次數來完成對數據的讀寫。

二、 Select 工具

select 提供了一種很好的方法來完成大量的數據源並行處理。它的名字來源於 Unix 系統中提供相同功能的 C 程序系統調用 select()

   阻塞式編程特點:

通常, I/O 屬於阻塞式系統調用。當你對輸入流調用 read() 方法,直到數據讀入完成之前方法一直被阻塞。如果你讀入本地文件就不需要等待很長時間。但是如果你從文件服務器或這是 socket 連接讀取數據的話,那麼你就要等很長時間。但你在等待過程中,你讀取數據的線程將不能做任何事。

當然,在 Java 中你很容易爲多個流創建多個線程。但是線程需要消耗大量的資源。在很多實現中,每個線程需要佔用一塊內存,即使它什麼也不做。同時太多的線程會對性能造成很大的影響。

   Select 編程特點:

select 採用不同的工作方式。通過 selet 你把輸入流注冊到一個 Selector 對象上。當某個流發生 I/O 活動時, selector 將會通知你。以這種方式就可以只用一個線程讀入多個數據源。儘管 Selector 不能幫你讀取數據,但是它可以監聽網絡連接請求和越過較慢的通道進行寫數據。

Java NIO 爲非阻塞式的 Socket 通信提供瞭如下幾個特殊類:

Selector

它是 SelectableChannel 對象的多路複用器,所有希望採用非阻塞方式進行通信的 Channel 都應該註冊到 Selector 對象。可通過調用此類的靜態 open() 方法來創建 Selector 實例,該方法將使用系統默認的 Selector 來返回新的 Selector Selector 可以同時監控多個 SelectableChannel IO 狀況,是非阻塞 IO 的核心。

一個 Selector 實例有 3 SelectionKey 的集合:

A.         所有 SelectionKey 集合:代表了註冊在該 Selector 上的 Channel ,這個集合可以通過 keys() 方法返回。

B.         被選擇的 SelectionKey 集合:代表了所有可通過 select() 方法監測到、需要進行 IO 處理的 Channel ,這個集合可以通過 selectedKeys() 返回。

C.         被取消的 SelectionKey 集合:代表了所有被取消註冊關係的 Channel ,在下一次執行 select() 方法時,這些 Channel 對應的 SelectionKey 會被徹底刪除,程序通常無須直接訪問該集合。

Select 相關的方法:

A.         int select() :監控所有註冊的 Channel ,當它們中間有需要處理的 IO 操作時,該方法返回,並將對應的 SelectionKey 加入被選擇的 SelectionKey 集合中,該方法返回這些 Channel 的數量。

B.         int select(long timeout) :可以設置超時時長的 select() 操作。

C.         int selectNow() :執行一個立即返回的 select() 操作,相對於無參數的 select() 方法而言,該方法不會阻塞線程。

D.         Selector wakeup() :使一個還未返回的 select() 方法立刻返回。

SelectableChannel

它代表可以支持非阻塞 IO 操作的 Channel 對象,可以將其註冊到 Selector 上,這種註冊的關係由 SelectionKey 實例表示。應用程序可調用 SelectableChannel register() 方法將其註冊到指定 Selector 上,當該 Selector 上某些 SelectableChannel 上有需要處理的 IO 操作時,程序可以調用 Selector 實例的 select() 方法獲取它們的數量,並可以通過 selectedKeys() 方法返回它們對應的 SelectKey 集合——通過該集合就可以獲取所有需要處理 IO 操作的 SelectableChannel 集。

SelectableChannel 對象支持阻塞和非阻塞兩種模式(所有 channel 默認都是阻塞模式),必須使用非阻塞式模式纔可以利用非阻塞 IO 操作。

SelectableChannel 提供瞭如下兩個方法來設置和返回該 Channel 的模式狀態:

SelectableChannel configureBlocking(boolean block) :設置是否採用阻塞模式。

boolean isBlocking() :返回該 Channel 是否是阻塞模式。

使用 NIO 實現非阻塞式服務器的示意圖:

 

 

 

 

從圖中可以看出,服務器上所有 Channel (包括 ServerSocketChannel SocketChannel )都需要向 Selector 註冊,而該 Selector 則負責監視這些 Socket IO 狀態,當其中任意一個或多個 Channel 具有可用的 IO 操作時,該 Selector select() 方法將會返回大於 0 的整數,該整數值就表示該 Selector 上有多少個 Channel 具有可用的 IO 操作,並提供了 selectedKeys() 方法來返回這些 Channel 對應的 SelectionKey 集合。正是通過 Selector ,使得服務器端只需要不斷地調用 Selector 實例的 select() 方法即可知道當前所有 Channel 是否有需要處理的 IO 操作。

 

 

 

 

 

三、 應用範例

   服務端代碼:

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;  

class AsyncServer implements Runnable {  
      
    private ByteBuffer r_buff = ByteBuffer.allocate(1024);  
    private ByteBuffer w_buff = ByteBuffer.allocate(1024);  
      
    private static int port = 8848;  

    public AsyncServer() {  
        new Thread(this).start();  
    }  

    private void info(String str){  
        System.out.println(str);  
    }  
      
    public void run() {  
        try {  
              
              
            Selector s = Selector.open(); //// 生成一個信號監視器  
            ServerSocketChannel ssc = ServerSocketChannel.open(); // 生成一個偵聽端  
            ssc.configureBlocking(false);// 將偵聽端設爲異步方式  

            // 偵聽端綁定到一個端口  
            ssc.socket().bind(new InetSocketAddress(port));  
            ssc.register(s, SelectionKey.OP_ACCEPT,new NetEventHandler());// 設置偵聽端所選的異步信號OP_ACCEPT  

            info(”開始啓動服務器”);  

            while (true) {  
                  
                int n = s.select(100);  
                if (n == 0) {// 沒有指定的I/O事件發生  
                    continue;  
                }  
                  
                  
                Set<SelectionKey> readys = s.selectedKeys();  
                if(readys.size() == 0){  
                    continue;  
                }  
                  
                Iterator<SelectionKey>  i = readys.iterator();
                while (i.hasNext()) { 
                     
                    SelectionKey key = i.next(); 
                   i.remove(); 
                      
                    if (key.isAcceptable()) {// 偵聽端信號觸發  
                        info(”偵聽端信號觸發”);  
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();  
                        SocketChannel sc = server.accept();  
                        sc.configureBlocking(false);  
                        sc.register(s, SelectionKey.OP_READ,new NetEventHandler());  
                    }  
                      
                    if (key.isReadable()) {// 某socket可讀信號  
                        DealwithData(key);  
                    }  
                      
                  
                  
                }  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
      
      

    public void DealwithData(SelectionKey key) throws IOException {  
          
        NetEventHandler eventHandler = (NetEventHandler)key.attachment();  
        info(”eventHandler:” + eventHandler);  
          
        // 由key獲取指定socketchannel的引用  
        SocketChannel sc = (SocketChannel) key.channel();  
        r_buff.clear();  
          
        int count;  
        while ((count = sc.read(r_buff)) > 0);  
          
        // 將r_buff內容拷入w_buff  
        r_buff.flip();  
        w_buff.clear();  
        w_buff.put(r_buff);  
        w_buff.flip();  
          
        // 將數據返回給客戶端  
        EchoToClient(sc);  

        w_buff.clear();  
        r_buff.clear();  
    }  

    public void EchoToClient(SocketChannel sc) throws IOException {  
        while (w_buff.hasRemaining())  
            sc.write(w_buff);  
    }  

    public static void main(String args[]) {  
        if (args.length > 0) {  
            port = Integer.parseInt(args[0]);  
        }  
        new AsyncServer();  
    }  
}
客戶端代碼:

import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.InputStreamReader;  
import java.net.InetSocketAddress;  
import java.nio.ByteBuffer;  
import java.nio.channels.SocketChannel;  

class AsyncClient {  
    private SocketChannel sc;  
    private final int MAX_LENGTH = 1024;  
    private ByteBuffer r_buff = ByteBuffer.allocate(MAX_LENGTH);  
    private ByteBuffer w_buff = ByteBuffer.allocate(MAX_LENGTH);  
    private static String host ;  
    private static int port = 8848;  

    public AsyncClient() {  
        try {  
            InetSocketAddress addr = new InetSocketAddress(host, port);  
            // 生成一個socketchannel  
            sc = SocketChannel.open();  

            // 連接到server  
            sc.connect(addr);  
            while (!sc.finishConnect())  
                ;  
            System.out.println(”connection has been established!…”);  

            while (true) {  
                // 回射消息  
                String echo;  
                try {  
                    System.err.println(”Enter msg you’d like to send: “);  
                    BufferedReader br = new BufferedReader(  
                            new InputStreamReader(System.in));  
                    // 輸入回射消息  
                    echo = br.readLine();  

                    // 把回射消息放入w_buff中  
                    w_buff.clear();  
                    w_buff.put(echo.getBytes());  
                    w_buff.flip();  
                } catch (IOException ioe) {  
                    System.err.println(”sth. is wrong with br.readline() “);  
                }  

                // 發送消息  
                while (w_buff.hasRemaining())  
                    sc.write(w_buff);  
                w_buff.clear();  

                // 進入接收狀態  
                Rec();  
                // 間隔1秒  
                Thread.currentThread().sleep(1000);  
            }  
        } catch (IOException ioe) {  
            ioe.printStackTrace();  
        } catch (InterruptedException ie) {  
            ie.printStackTrace();  
        }  
    }  

    public void Rec() throws IOException {  
        int count;  
        r_buff.clear();  
        count = sc.read(r_buff);  

        r_buff.flip();  
        byte[] temp = new byte[r_buff.limit()];  
        r_buff.get(temp);  
        System.out.println(”reply is ” + count + ” long, and content is: “
                + new String(temp));  
    }  

    public static void main(String args[]) {  
        if (args.length < 1) {// 輸入需有主機名或IP地址  
            try {  
                System.err.println(”Enter host name: “);  
                BufferedReader br = new BufferedReader(new InputStreamReader(  
                        System.in));  
                host = br.readLine();  
            } catch (IOException ioe) {  
                System.err.println(”sth. is wrong with br.readline() “);  
            }  
        } else if (args.length == 1) {  
            host = args[0];  
        } else if (args.length > 1) {  
            host = args[0];  
            port = Integer.parseInt(args[1]);  
        }  

        new AsyncClient();  
    }  
}

轉自:http://hi.baidu.com/chenweifighting/blog/item/38d7760e3d8efc226159f378.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章