Thinking in Java--使用NIO實現非阻塞Socket通訊

acm_lkl發表於2020-04-04

Java1.4提供了一種新的IO讀取方式,稱為NIO。NIO中使用了通道和緩衝器的概念,並且以塊的形式運算元據,這樣更接近作業系統IO操作的形式,提高了JavaIO的效率。NIO的核心類有兩個Channel和Buffer。但是其實除了提升了基本IO操作的效能外,NIO還提供了非阻塞IO的功能。這裡先介紹下阻塞IO和非阻塞IO的概念。考慮到應用程式傳送出IO請求,如果這個IO請求會阻塞執行緒(就是執行緒停在這裡直到讀取到了資料再繼續執行下去),那麼就是阻塞IO;如果這個IO請求沒有阻塞執行緒(執行緒發出了IO請求,但是並停在這裡等資料的到來而是先去做別的事情)就稱為非阻塞IO。可以很顯然的看到,非阻塞的IO可以提高程式的效能。這篇部落格下面會先介紹用於Socket通訊的非阻塞IO的具體類,然後再利用這些類實現一個非阻塞的Socket通訊伺服器。

一.用於非阻塞Socket通訊的幾個類
(1).Selector類
它是SelectableChannel物件的多路複用器,所有希望採用非阻塞方式進行通訊的Channel都應該註冊到Selector物件。但是這個類的物件是不能通過呼叫構造器得到的,而是通過這個類靜態的open()方法得到,該方法將使用系統預設的Selector來返回新的Selector。
Selector物件可以同時監聽多個SelectableChannel的IO狀況,是非阻塞IO的核心。一個Selector例項有3個SelectionKey集合。
1)所有的SelectionKey集合:代表了註冊在該Selector上的Channel,這個集合可以通過keys()方法返回
2)被選擇的SelectionKey集合:代表了所有可以通過select()方法獲取的,需要進行IO處理的Channel,這個集合可以通過selectedKeys()返回。
3)被取消的SelectionKey集合:代表了所有被取消註冊關係的Channel,下一次執行select()方法時,這些Channel對應的SelectionKey就會被徹底刪除,程式通常無須直接訪問這個集合。
Selector類還提供了一系列和select()相關的方法,這些方法比較重要,需要了解一下:
int select():監控所有註冊的Channel,當他們中有需要處理的IO操作時,該方法返回,並將對應的SelectionKey加入到被選擇的SelectionKey集合中,並返回這些Channel的數量。
int select(long timeout):可以設定超時時長的select()操作
int selectNow():執行一個立即返回的select()操作,相對與無引數的select()方法而言,該方法不會阻塞執行緒
Selector wakeup():使一個還未返回的select()方法立刻返回。

(2)SelectableChannel類
Selectabel類是一種支援阻塞I/O和非阻塞I/O的通道。應用程式可以呼叫SelectabelChanel的register()方法將其註冊到指定的Selector上。SelectableChannel物件支援阻塞和非阻塞兩種模式,但是預設情況下是阻塞的(所有的Channel預設都是阻塞模式),必須使用非阻塞模式才能支援非阻塞IO。但是不同的SelectableChannel支援的操作是不一樣的,向ServerSocketChannel代表一個ServerSocket,它只支援OP_ACCEPT操作。而SocketChannle代表一個socket,支援OP_READ操作。下面是幾個SelectableChannel常用的方法:
boolean isBlocking():返回該Channel是否為阻塞模式
SelectabelChannel configureBlocking(boolean block):設定是否採用阻塞模式。
int valiOps():返回一個整數值,表示這個Channel所支援的操作。
boolean isRegistered():返回該Channel是否已經註冊在一個或多個Selector上。

(3)SelectionKey類
該類物件代表SelectableChannel和Selector之間的註冊關係。

(4)ServerSocketChannel類
支援非阻塞操作,對應與ServerSocket這個類,支援OP_ACCEPT操作;該類也提供了accept()方法,功能相當於ServerSocket提供的accept()方法。

(5).SocketChannel類
支援非阻塞操作,對應Socket這個類,支援OP_CONNECT,OP_READ和OP_WRITE操作。這個類還實現了ByteChannel,可以通過SocketChannel來讀寫ByteBuffer物件。

二.利用非阻塞IO實現一個聊天室的伺服器
前面我自己寫了一個仿QQ的C/S區域網聊天工具,在這個工具中,伺服器使用SeverSocket進行監聽,每新加入一個人就新建一個socekt與其通訊並且還要單獨為其開啟一個服務執行緒。這樣如果加入的使用者比較多,那麼就要開啟很多的服務執行緒了,伺服器的壓力就會比較大。現在我們用非阻塞IO,伺服器只需要一個執行緒就可以同時與多個客戶端進行通訊。
具體的思路是:原先伺服器中使用ServerSocket進行監聽,現在改用ServerSocketChannel物件進行監聽。原先每接入一個客戶端,就新建一個Socket進行通訊,現在新建一個SocketChannel進行通訊。最重要的是這些SelectableChannel物件,都必須註冊到一個Selector物件上;然後我們只需要檢測這個Selector物件就行了,我們可以呼叫這個Selector物件的select()方法監聽,這樣就可以實時監聽所有客戶端的行為,並可以通過selectedKeys()方法返回需要處理的SelectionKey物件,SelectionKey物件可以判定返回訊息的內容(是連線請求還是具體的訊息),並且這個物件的Channel方法可以返回被選中的客戶端的Channel。更具體的思路見下面的程式碼及註釋:

package IO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.*;


public class NServer {

    //用於檢測所有Channel狀態的Selector
    private Selector selector = null;
    ///預設埠
    static final int PORT = 30000;
    //定義實現編碼和解碼的字符集物件
    private Charset charset = Charset.forName("UTF-8");
    public void init() throws IOException
    {
        //Selector物件是不能通過構造器得到的,必須通過靜態的open()方法得到
        selector=Selector.open();

        //SeverSocktChannel物件也不能通過構造器得到,所以需要通過open()方法開啟
        ServerSocketChannel server = ServerSocketChannel.open();
        InetSocketAddress isa  = new InetSocketAddress("127.0.0.1",PORT);

        //將ServerSocketChannel繫結到指定的IP地址
        server.bind(isa);

        //設定ServerSocketChannel以非阻塞方式工作(預設是阻塞的)
        server.configureBlocking(false);

        //將server註冊到指定的Selector物件
        server.register(selector, SelectionKey.OP_ACCEPT);

        //返回值大於0,表示有Channel中含有需要處理的資料
        while(selector.select()>0){

            //依次處理selector上的每個已選擇的SelectionKey
            for(SelectionKey sk : selector.selectedKeys()){

                //從selector已選擇的Key集中刪除正在處理的SelectionKey
                selector.selectedKeys().remove(sk);
                //如果sk對應的Channel包含客戶端的連線請求
                if(sk.isAcceptable()){
                    //呼叫accept方法接受連線,產生服務端的SocketChannel
                    SocketChannel sc = server.accept();
                    //設定採用非阻塞模式
                    sc.configureBlocking(false);
                    //將該SocketChannel也註冊到selector上去
                    sc.register(selector, SelectionKey.OP_READ);
                    //將sk的Channel設定成準備接收其它的請求
                    sk.interestOps(SelectionKey.OP_ACCEPT);
                }
                //如果sk對應的Channel有資料需要讀取
                if(sk.isReadable()){
                    //獲取SelectionKey對應的Channel,該Channel中有需要讀取的資料
                    SocketChannel sc =(SocketChannel)sk.channel();
                    //定義準備執行讀取資料的ByteBufferer
                    ByteBuffer buff = ByteBuffer.allocate(1024);
                    String content="";
                    try{
                        while(sc.read(buff)>0){
                            buff.flip();
                            content+=charset.decode(buff);
                        }
                        //將sk對應的Channel設定成準備洗一次讀取
                        sk.interestOps(SelectionKey.OP_READ);
                    }

                    //如果捕獲到該sk對應的Channel出現了異常,即表明該Channel
                    //對應的Client出現了異常,所以從Selector中取消掉sk的註冊
                    catch(IOException e){

                        //從Selector中刪除掉指定的SelectionKey
                        sk.cancel();
                        if(sk.channel()!=null){
                            sk.channel().close();
                        }
                    }
                    //如果content的長度不為空,即該聊天資訊不為空
                    if(content.length()>0){

                        //遍歷該selector中註冊的所有SelectionKey
                        for(SelectionKey key: selector.keys()){

                            //獲取key對應的Chanel
                            Channel targetChannel = key.channel();
                            //如果該Channel是SocketChannel物件
                            if(targetChannel instanceof SocketChannel){
                                //將讀到的內容寫入到Channel中
                                SocketChannel dest =(SocketChannel)targetChannel;
                                dest.write(charset.encode(content));
                            }
                        }
                    }
                }
            }
        }
    }
}

相關文章