NIO選擇器(Selector)

NIO簡介

NIO的核心組件包括:Channel(通道),Buffer(緩衝區),Selector(選擇器),其中Channel和Buffer比較好理解 

簡單來說 NIO是面向通道和緩衝區的,意思就是:數據總是從通道中讀到buffer緩衝區內,或者從buffer寫入到通道中。

selector

選擇器提供選擇執行已經就緒的任務的能力,從底層看,Selector提供了詢問通道是否已經準備好執行每個I/O操作的能力,Selector 允許單線程處理多個Channel。僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。事實上,可以只用一個線程處理所有的通道,這樣會大量的減少線程之間上下文切換的開銷。

選擇器(selector)

Selector選擇器類管理着一個被註冊的通道集合的信息和它們的就緒狀態。通道是和選擇器一起被註冊的,並且使用選擇器來更新通道的就緒狀態。當這麼做的時候,可以選擇將被激發的線程掛起,直到有就緒的的通道。

可選擇通道(selectablechannel)

SelectableChannel這個抽象類提供了實現通道的可選擇性所需要的公共方法。它是所有支持就緒檢查的通道類的父類。因爲FileChannel類沒有繼承SelectableChannel因此是不是可選通道,而所有socket通道都是可選擇的,包括從管道(Pipe)對象的中獲得的通道。SelectableChannel可以被註冊到Selector對象上,同時可以指定對那個選擇器而言,那種操作是感興趣的。一個通道可以被註冊到多個選擇器上,但對每個選擇器而言只能被註冊一次。

選擇鍵(selectionKey)

選擇鍵封裝了特定的通道與特定的選擇器的註冊關係。選擇鍵對象被SelectableChannel.register()返回並提供一個表示這種註冊關係的標記。選擇鍵包含了兩個比特集(以整數的形式進行編碼),指示了該註冊關係所關心的通道操作,以及通道已經準備好的操作。

下面是使用Selector管理多個channel的結構圖: 

Selector的使用

Selector selector = Selector.open()     //Selector對象是通過調用靜態工廠方法open()來實例化的

//類方法open()實際上向SPI1發出請求,通過默認的SelectorProvider對象獲取一個新的實例。

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

ServerSocket scoket = serverSocketChannel.socket();

scoket.bind(new InetSocketAddress(PORT));

serverSocketChannel.configureBlocking(false);

serverKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

//要實現Selector管理Channel,需要將channel註冊到相應的Selector上

通過調用通道的register()方法會將它註冊到一個選擇器上。與Selector一起使用時,Channel必須處於非阻塞模式下,否則將拋出IllegalBlockingModeException異常,這意味着不能將FileChannel與Selector一起使用,因爲FileChannel不能切換到非阻塞模式,而套接字通道都可以。另外通道一旦被註冊,將不能再回到阻塞狀態,此時若調用通道的configureBlocking(true)將拋出BlockingModeException異常。

register()方法的第二個參數是“interest集合”,表示選擇器所關心的通道操作,它實際上是一個表示選擇器在檢查通道就緒狀態時需要關心的操作的比特掩碼。比如一個選擇器對通道的read和write操作感興趣,那麼選擇器在檢查該通道時,只會檢查通道的read和write操作是否已經處在就緒狀態。 
它有以下四種操作類型:

  • Connect 連接
  • Accept 接受
  • Read 讀
  • Write 寫

需要注意並非所有的操作在所有的可選擇通道上都能被支持,比如ServerSocketChannel支持Accept,而SocketChannel中不支持。我們可以通過通道上的validOps()方法來獲取特定通道下所有支持的操作集合。

Java中定義了四個常量來表示這四種操作類型:

SelectionKey.OP_CONNECT 
SelectionKey.OP_ACCEPT 
SelectionKey.OP_READ 
SelectionKey.OP_WRITE

interest集合是Selector感興趣的集合,用於指示選擇器對通道關心的操作,可通過SelectionKey對象的interestOps()獲取。最初,該興趣集合是通道被註冊到Selector時傳進來的值。該集合不會被選擇器改變,但是可通過interestOps()改變。我們可以通過以下方法來判斷Selector是否對Channel的某種事件感興趣:

   int interestSet=selectionKey.interestOps();
   boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
  • 1
  • 2

read集合是通道已經就緒的操作的集合,表示一個通道準備好要執行的操作了,可通過SelctionKey對象的readyOps()來獲取相關通道已經就緒的操作。它是interest集合的子集,並且表示了interest集合中從上次調用select()以後已經就緒的那些操作。(比如選擇器對通道的read,write操作感興趣,而某時刻通道的read操作已經準備就緒可以被選擇器獲知了,前一種就是interest集合,後一種則是read集合。)。JAVA中定義以下幾個方法用來檢查這些操作是否就緒:

    //int readSet=selectionKey.readOps();
    selectionKey.isAcceptable();//等價於selectionKey.readyOps()&SelectionKey.OP_ACCEPT
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();

需要注意的是,通過相關的選擇鍵的readyOps()方法返回的就緒狀態指示只是一個提示,底層的通道在任何時候都會不斷改變,而其他線程也可能在通道上執行操作並影響到它的就緒狀態。另外,我們不能直接修改read集合。

取出SelectionKey所關聯的Selector和Channel 
通過SelectionKey訪問對應的Selector和Channel:

Channel channel =selectionKey.channel();
Selector selector=selectionKey.selector();
  • 1
  • 2

關於取消SelectionKey對象的那點事

我們可以通過SelectionKey對象的cancel()方法來取消特定的註冊關係。該方法調用之後,該SelectionKey對象將會被”拷貝”至已取消鍵的集合中,該鍵此時已經失效,但是該註冊關係並不會立刻終結。在下一次select()時,已取消鍵的集合中的元素會被清除,相應的註冊關係也真正終結。

爲SelectionKey綁定附加對象

可以將一個或者多個附加對象綁定到SelectionKey上,以便容易的識別給定的通道。通常有兩種方式: 
1 在註冊的時候直接綁定:

SelectionKey key=channel.register(selector,SelectionKey.OP_READ,theObject);

2 在綁定完成之後附加:

selectionKey.attach(theObject);//綁定

綁定之後,可通過對應的SelectionKey取出該對象:

selectionKey.attachment();。

如果要取消該對象,則可以通過該種方式:

selectionKey.attach(null).

需要注意的是如果附加的對象不再使用,一定要人爲清除,因爲垃圾回收器不會回收該對象,若不清除的話會成內存泄漏。

一個單獨的通道可被註冊到多個選擇器中,有些時候我們需要通過isRegistered()方法來檢查一個通道是否已經被註冊到任何一個選擇器上。 通常來說,我們並不會這麼做。

通過Selector選擇通道

我們知道選擇器維護註冊過的通道的集合,並且這種註冊關係都被封裝在SelectionKey當中。接下來我們簡單的瞭解一下Selector維護的三種類型SelectionKey集合:

已註冊的鍵的集合(Registered key set)

所有與選擇器關聯的通道所生成的鍵的集合稱爲已經註冊的鍵的集合。並不是所有註冊過的鍵都仍然有效。這個集合通過keys()方法返回,並且可能是空的。這個已註冊的鍵的集合不是可以直接修改的;試圖這麼做的話將引發java.lang.UnsupportedOperationException。

已選擇的鍵的集合(Selected key set)

已註冊的鍵的集合的子集。這個集合的每個成員都是相關的通道被選擇器(在前一個選擇操作中)判斷爲已經準備好的,並且包含於鍵的interest集合中的操作。這個集合通過selectedKeys()方法返回(並有可能是空的)。 
不要將已選擇的鍵的集合與ready集合弄混了。這是一個鍵的集合,每個鍵都關聯一個已經準備好至少一種操作的通道。每個鍵都有一個內嵌的ready集合,指示了所關聯的通道已經準備好的操作。鍵可以直接從這個集合中移除,但不能添加。試圖向已選擇的鍵的集合中添加元素將拋出java.lang.UnsupportedOperationException。

已取消的鍵的集合(Cancelled key set)

已註冊的鍵的集合的子集,這個集合包含了cancel()方法被調用過的鍵(這個鍵已經被無效化),但它們還沒有被註銷。這個集合是選擇器對象的私有成員,因而無法直接訪問。

在剛初始化的Selector對象中,這三個集合都是空的。通過Selector的select()方法可以選擇已經準備就緒的通道(這些通道包含你感興趣的的事件)。比如你對讀就緒的通道感興趣,那麼select()方法就會返回讀事件已經就緒的那些通道。下面是Selector幾個重載的select()方法:

select():阻塞到至少有一個通道在你註冊的事件上就緒了。 
select(long timeout):和select()一樣,但最長阻塞事件爲timeout毫秒。 
selectNow():非阻塞,只要有通道就緒就立刻返回。

select()方法返回的int值表示有多少通道已經就緒,是自上次調用select()方法後有多少通道變成就緒狀態。之前在select()調用時進入就緒的通道不會在本次調用中被記入,而在前一次select()調用進入就緒但現在已經不在處於就緒的通道也不會被記入。例如:首次調用select()方法,如果有一個通道變成就緒狀態,返回了1,若再次調用select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法調用之間,只有一個通道就緒了。

一旦調用select()方法,並且返回值不爲0時,則可以通過調用Selector的selectedKeys()方法來訪問已選擇鍵集合。如下: 
Set selectedKeys=selector.selectedKeys(); 
進而可以放到和某SelectionKey關聯的Selector和Channel。如下所示:

Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

關於Selector執行選擇的過程

我們知道調用select()方法進行通道,現在我們再來深入一下選擇的過程,也就是select()執行過程。當select()被調用時將執行以下幾步:

  1. 首先檢查已取消鍵集合,也就是通過cancle()取消的鍵。如果該集合不爲空,則清空該集合裏的鍵,同時該集合中每個取消的鍵也將從已註冊鍵集合和已選擇鍵集合中移除。(一個鍵被取消時,並不會立刻從集合中移除,而是將該鍵“拷貝”至已取消鍵集合中,這種取消策略就是我們常提到的“延遲取消”。)

  2. 再次檢查已註冊鍵集合(準確說是該集合中每個鍵的interest集合)。系統底層會依次詢問每個已經註冊的通道是否準備好選擇器所感興趣的某種操作,一旦發現某個通道已經就緒了,則會首先判斷該通道是否已經存在在已選擇鍵集合當中,如果已經存在,則更新該通道在已註冊鍵集合中對應的鍵的ready集合,如果不存在,則首先清空該通道的對應的鍵的ready集合,然後重設ready集合,最後將該鍵存至已註冊鍵集合中。這裏需要明白,當更新ready集合時,在上次select()中已經就緒的操作不會被刪除,也就是ready集合中的元素是累積的,比如在第一次的selector對某個通道的read和write操作感興趣,在第一次執行select()時,該通道的read操作就緒,此時該通道對應的鍵中的ready集合存有read元素,在第二次執行select()時,該通道的write操作也就緒了,此時該通道對應的ready集合中將同時有read和write元素。

深入已註冊鍵集合的管理

到現在我們已經知道一個通道的的鍵是如何被添加到已選擇鍵集合中的,下面我們來繼續瞭解對已選擇鍵集合的管理 。首先要記住:選擇器不會主動刪除被添加到已選擇鍵集合中的鍵,而且被添加到已選擇鍵集合中的鍵的ready集合只能被設置,而不能被清理。如果我們希望清空已選擇鍵集合中某個鍵的ready集合該怎麼辦?我們知道一個鍵在新加入已選擇鍵集合之前會首先置空該鍵的ready集合,這樣的話我們可以人爲的將某個鍵從已註冊鍵集合中移除最終實現置空某個鍵的ready集合。被移除的鍵如果在下一次的select()中再次就緒,它將會重新被添加到已選擇的鍵的集合中。這就是爲什麼要在每次迭代的末尾調用keyIterator.remove()。

停止選擇

選擇器執行選擇的過程,系統底層會依次詢問每個通道是否已經就緒,這個過程可能會造成調用線程進入阻塞狀態,那麼我們有以下三種方式可以喚醒在select()方法中阻塞的線程。

  1. 通過調用Selector對象的wakeup()方法讓處在阻塞狀態的select()方法立刻返回 
    該方法使得選擇器上的第一個還沒有返回的選擇操作立即返回。如果當前沒有進行中的選擇操作,那麼下一次對select()方法的一次調用將立即返回。
  2. 通過close()方法關閉Selector** 
    該方法使得任何一個在選擇操作中阻塞的線程都被喚醒(類似wakeup()),同時使得註冊到該Selector的所有Channel被註銷,所有的鍵將被取消,但是Channel本身並不會關閉。
  3. 調用interrupt() 
    調用該方法會使睡眠的線程拋出InterruptException異常,捕獲該異常並在調用wakeup()

NIO多人聊天室

服務端

public class ChatServer implements Runnable{

    private Selector selector;
    private SelectionKey serverKey;
    private Vector<String> usernames;
    private static final int PORT = 9999;

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public ChatServer(){
        usernames = new Vector<String>();
        init();
    }

    public void init(){
        try {
            selector = Selector.open();
            //創建serverSocketChannel
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            ServerSocket socket = serverChannel.socket();
            socket.bind(new InetSocketAddress(PORT));
            //加入到selector中
            serverChannel.configureBlocking(false);
            serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            printInfo("server starting.......");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            while(true){
                //獲取就緒channel
                int count = selector.select();
                if(count > 0){
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while(iterator.hasNext()){
                        SelectionKey key = iterator.next();

                        //若此key的通道是等待接受新的套接字連接
                        if(key.isAcceptable()){
                            System.out.println(key.toString() + " : 接收");
                            //一定要把這個accpet狀態的服務器key去掉,否則會出錯
                            iterator.remove();
                            ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                            //接受socket
                            SocketChannel socket = serverChannel.accept();
                            socket.configureBlocking(false);
                            //將channel加入到selector中,並一開始讀取數據
                            socket.register(selector, SelectionKey.OP_READ);
                        }
                        //若此key的通道是有數據可讀狀態
                        if(key.isValid() && key.isReadable()){
                            System.out.println(key.toString() + " : 讀");
                            readMsg(key);
                        }
                        //若此key的通道是寫數據狀態
                        if(key.isValid() && key.isWritable()){
                            System.out.println(key.toString() + " : 寫");
                            writeMsg(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readMsg(SelectionKey key) {
        SocketChannel channel = null;
        try {
            channel = (SocketChannel) key.channel();
            //設置buffer緩衝區
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //假如客戶端關閉了通道,這裏在對該通道read數據,會發生IOException,捕獲到Exception後,關閉掉該channel,取消掉該key
            int count = channel.read(buffer);
            StringBuffer buf = new StringBuffer();
            //如果讀取到了數據
            if(count > 0){
                //讓buffer翻轉,把buffer中的數據讀取出來
                buffer.flip();
                buf.append(new String(buffer.array(), 0, count));
            }
            String msg = buf.toString();

            //如果此數據是客戶端連接時發送的數據
            if(msg.indexOf("open_") != -1){
                String name = msg.substring(5);//取出名字
                printInfo(name + " --> online");
                usernames.add(name);
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey skey = iter.next();
                    //若不是服務器套接字通道的key,則將數據設置到此key中  
                    //並更新此key感興趣的動作  
                    if(skey != serverKey){
                        skey.attach(usernames);
                        skey.interestOps(skey.interestOps() | SelectionKey.OP_WRITE);
                    }
                }
                //如果是下線時發送的數據
            }else if(msg.indexOf("exit_") != -1){
                String username = msg.substring(5);
                usernames.remove(username);
                key.attach("close");
                //要退出的當前channel加上close的標示,並把興趣轉爲寫,如果write中收到了close,則中斷channel的鏈接
                key.interestOps(SelectionKey.OP_WRITE);
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey sKey = iter.next();
                    sKey.attach(usernames);
                    sKey.interestOps(sKey.interestOps() | SelectionKey.OP_WRITE);
                }
                //如果是聊天發送數據
            }else{
                String uname = msg.substring(0, msg.indexOf("^"));
                msg = msg.substring(msg.indexOf("^") + 1);
                printInfo("("+uname+")說:" + msg);
                String dateTime = sdf.format(new Date());
                String smsg = uname + " " + dateTime + "\n  " + msg + "\n";
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey sKey = iter.next();
                    sKey.attach(smsg);
                    sKey.interestOps(sKey.interestOps() | SelectionKey.OP_WRITE);
                }
            }
            buffer.clear();
        } catch (IOException e) {
            //當客戶端關閉channel時,服務端再往通道緩衝區中寫或讀數據,都會報IOException,解決方法是:在服務端這裏捕獲掉這個異常,並且關閉掉服務端這邊的Channel通道
            key.cancel();
            try {
                channel.socket().close();
                channel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    private void writeMsg(SelectionKey key) {
        try {
            SocketChannel channel = (SocketChannel) key.channel();
            Object attachment = key.attachment();
            //獲取key的值之後,要把key的值置空,避免影響下一次的使用
            key.attach("");
            channel.write(ByteBuffer.wrap(attachment.toString().getBytes()));
            key.interestOps(SelectionKey.OP_READ);

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

    private void printInfo(String str) {
        System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
    }

    public static void main(String[] args) {
        ChatServer server = new ChatServer();
        new Thread(server).start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167

注意這裏readMsg 和 writeMsg中,read操作的key重新設置interest要遍歷所有key,而write操作的key重新設置interest只需要設置傳入的當前key,原因: 
讀操作之所以要遍歷key,是因爲這裏channel的讀寫操作的流程是: 
1. read到數據後,把數據加到每一個key的attach中 
2. 寫數據時,從key的attach中取出數據,從而把該數據寫到buffer中

例如:當選擇器有3個channel的情況下,實現多人聊天,流程: 
1. 其中一個channel發送數據,該channel接受到數據 
2. 在該channel的讀操作中,遍歷所有的channel,爲每一個channel的attach加上該數據 
3. 每一個channel在寫操作時,從key的attach中取出數據,分別把該數據寫到各自的buffer中 
4. 於是每一個channel的界面都能看到其中一個channel發送的數據

客戶端:

public class ChatClient {

    private static final String HOST = "127.0.0.1";
    private static int PORT = 9999;
    private static SocketChannel socket;
    private static ChatClient client;

    private static byte[] lock = new byte[1];
    //單例模式管理
    private ChatClient() throws IOException{
        socket = SocketChannel.open();
        socket.connect(new InetSocketAddress(HOST, PORT));
        socket.configureBlocking(false);
    }

    public static ChatClient getIntance(){
        synchronized(lock){
            if(client == null){
                try {
                    client = new ChatClient();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return client;
        }
    }

    public void sendMsg(String msg){
        try {
            socket.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String receiveMsg(){
        String msg = null;
        try {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            StringBuffer buf = new StringBuffer();
            int count = 0;
            //不一定一次就能讀滿,連續讀
            while((count = socket.read(buffer)) > 0){
                buf.append(new String(buffer.array(), 0, count));
            }
            //有數據
            if(buf.length() > 0){
                msg = buf.toString();
                if(buf.toString().equals("close")){
                    //不過不sleep會導致ioException的發生,因爲如果這裏直接關閉掉通道,在server裏,
                    //該channel在read(buffer)時會發生讀取異常,通過sleep一段時間,使得服務端那邊的channel先關閉,客戶端
                    //的channel後關閉,這樣就能防止read(buffer)的ioException
                    //但是這是一種笨方法
                    //Thread.sleep(100);
                    //更好的方法是,在readBuffer中捕獲異常後,手動進行關閉通道
                    socket.socket().close();
                    socket.close();
                    msg = null;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return msg;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

界面代碼:設置姓名

public class SetNameFrame extends JFrame {
    private static final long serialVersionUID = 1L;
    private static JTextField txtName;
    private static JButton btnOK;
    private static JLabel label;

    public SetNameFrame() {
        this.setLayout(null);
        Toolkit kit = Toolkit.getDefaultToolkit();
        int w = kit.getScreenSize().width;
        int h = kit.getScreenSize().height;
        this.setBounds(w / 2 - 230 / 2, h / 2 - 200 / 2, 230, 200);
        this.setTitle("設置名稱");
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.setResizable(false);
        txtName = new JTextField(4);
        this.add(txtName);
        txtName.setBounds(10, 10, 100, 25);
        btnOK = new JButton("OK");
        this.add(btnOK);
        btnOK.setBounds(120, 10, 80, 25);
        label = new JLabel("[w:" + w + ",h:" + h + "]");
        this.add(label);
        label.setBounds(10, 40, 200, 100);
        label.setText("<html>在上面的文本框中輸入名字<br/>顯示器寬度:" + w + "<br/>顯示器高度:" + h
                + "</html>");

        btnOK.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String uname = txtName.getText();
                ChatClient service = ChatClient.getIntance();
                ChatFrame chatFrame = new ChatFrame(service, uname);
                chatFrame.show();
                setVisible(false);
            }
        });
    }

    public static void main(String[] args) {
        SetNameFrame setNameFrame = new SetNameFrame();
        setNameFrame.setVisible(true);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

界面代碼:聊天界面

public class ChatFrame {

    private JTextArea readContext = new JTextArea(18, 30);// 顯示消息文本框
    private JTextArea writeContext = new JTextArea(6, 30);// 發送消息文本框

    private DefaultListModel modle = new DefaultListModel();// 用戶列表模型
    private JList list = new JList(modle);// 用戶列表

    private JButton btnSend = new JButton("發送");// 發送消息按鈕
    private JButton btnClose = new JButton("關閉");// 關閉聊天窗口按鈕

    private JFrame frame = new JFrame("ChatFrame");// 窗體界面

    private String uname;// 用戶姓名

    private ChatClient service;// 用於與服務器交互

    private boolean isRun = false;// 是否運行

    public ChatFrame(ChatClient service, String uname) {
        this.isRun = true;
        this.uname = uname;
        this.service = service;
    }

    // 初始化界面控件及事件
    private void init() {
        frame.setLayout(null);
        frame.setTitle(uname + " 聊天窗口");
        frame.setSize(500, 500);
        frame.setLocation(400, 200);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        JScrollPane readScroll = new JScrollPane(readContext);
        readScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        frame.add(readScroll);
        JScrollPane writeScroll = new JScrollPane(writeContext);
        writeScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        frame.add(writeScroll);
        frame.add(list);
        frame.add(btnSend);
        frame.add(btnClose);
        readScroll.setBounds(10, 10, 320, 300);
        readContext.setBounds(0, 0, 320, 300);
        readContext.setEditable(false);
        readContext.setLineWrap(true);// 自動換行
        writeScroll.setBounds(10, 315, 320, 100);
        writeContext.setBounds(0, 0, 320, 100);
        writeContext.setLineWrap(true);// 自動換行
        list.setBounds(340, 10, 140, 445);
        btnSend.setBounds(150, 420, 80, 30);
        btnClose.setBounds(250, 420, 80, 30);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                isRun = false;
                service.sendMsg("exit_" + uname);
                System.exit(0);
            }
        });

        btnSend.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String msg = writeContext.getText().trim();
                if(msg.length() > 0){
                    service.sendMsg(uname + "^" + writeContext.getText());
                }
                writeContext.setText(null);
                writeContext.requestFocus();
            }
        });

        btnClose.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                isRun = false;
                service.sendMsg("exit_" + uname);
                System.exit(0);
            }
        });

        list.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                // JOptionPane.showMessageDialog(null,
                // list.getSelectedValue().toString());
            }
        });

        writeContext.addKeyListener(new KeyListener() {

            @Override
            public void keyTyped(KeyEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void keyReleased(KeyEvent e) {
                if(e.getKeyCode() == KeyEvent.VK_ENTER){
                    String msg = writeContext.getText().trim();
                    if(msg.length() > 0){
                        service.sendMsg(uname + "^" + writeContext.getText());
                    }
                    writeContext.setText(null);
                    writeContext.requestFocus();
                }
            }

            @Override
            public void keyPressed(KeyEvent e) {
                // TODO Auto-generated method stub

            }
        });
    }

    // 此線程類用於輪詢讀取服務器發送的消息
    private class MsgThread extends Thread {
        @Override
        public void run() {
            while (isRun) {
                String msg = service.receiveMsg();
                if (msg != null) {
                    //如果存在[],這是verctor裝的usernames的toString生成的
                    if (msg.indexOf("[") != -1 && msg.lastIndexOf("]") != -1) {
                        msg = msg.substring(1, msg.length() - 1);
                        String[] userNames = msg.split(",");
                        modle.removeAllElements();
                        for (int i = 0; i < userNames.length; i++) {
                            modle.addElement(userNames[i].trim());
                        }
                    } else {//如果是普通的消息
                        String str = readContext.getText() + msg;
                        readContext.setText(str);
                        readContext.selectAll();
                    }
                }
            }
        }
    }

    // 顯示界面
    public void show() {
        this.init();
        service.sendMsg("open_" + uname);
        MsgThread msgThread = new MsgThread();
        msgThread.start();
        this.frame.setVisible(true);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152

分析整個程序的流程:

只有一個客戶端連接的註釋:

[2017-01-23 21:26:14] -> server starting……. 
sun.nio.ch.SelectionKeyImpl@99436c6 : 接收 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 讀 
[2017-01-23 21:26:19] -> a –> online 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 寫

可以看出流程是:服務端接受通道 -> 通道進行讀操作 -> 通道進行寫操作 
1. 當客戶端的channel調用connect後,服務端接受到該Channel,於是把該通道的興趣改爲read就緒 
2. 客戶端connect後,立馬寫數據”open_”到通道緩衝區中,於是該通道進入了有數據可讀狀態(即讀狀態),且該通道的興趣爲read,所以select()的返回值爲1,進入了readMsg(); 
3. readMsg中把每一個key的狀態改爲了寫狀態,而此時客戶端一直在read數據,要求你服務端要給我數據,於是服務器的channel此時是寫狀態,且該通道的興趣爲write,所以select()的返回值爲1,進入了writeMsg();

有兩個個客戶端連接的註釋:

sun.nio.ch.SelectionKeyImpl@99436c6 : 接收 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 讀 
[2017-01-23 21:26:19] -> a –> online 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 寫 
sun.nio.ch.SelectionKeyImpl@99436c6 : 接收 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 寫 
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 讀 
[2017-01-23 21:32:30] -> b –> online 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 寫 
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 寫 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 寫

可以看到,@99436c6是ServerSocketChannel,@3ee5015是第一個鏈接的Channel,@12cb94b7是第二個連接的Channel,可以看見,第二個Channel連接之後

sun.nio.ch.SelectionKeyImpl@3ee5015 : 寫 
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 讀 
[2017-01-23 21:32:30] -> b –> online 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 寫 
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 寫 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 寫

兩個Channel是交替運行的,說明Selector處理Channle,是輪詢處理的



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