您好,登錄后才能下訂單哦!
小編給大家分享一下JavaNIOSelector怎么用,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
具體如下:
Java NIO的核心組件包括:Channel(通道),Buffer(緩沖區),Selector(選擇器),其中Channel和Buffer比較好理解
簡單來說 NIO是面向通道和緩沖區的,意思就是:數據總是從通道中讀到buffer緩沖區內,或者從buffer寫入到通道中。
關于Channel 和 Buffer的詳細講解請看:Java NIO 教程
1. Selector簡介
選擇器提供選擇執行已經就緒的任務的能力.從底層來看,Selector提供了詢問通道是否已經準備好執行每個I/O操作的能力。Selector 允許單線程處理多個Channel。僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。事實上,可以只用一個線程處理所有的通道,這樣會大量的減少線程之間上下文切換的開銷。
在開始之前,需要回顧一下Selector、SelectableChannel和SelectionKey:
選擇器(Selector)
Selector選擇器類管理著一個被注冊的通道集合的信息和它們的就緒狀態。通道是和選擇器一起被注冊的,并且使用選擇器來更新通道的就緒狀態。當這么做的時候,可以選擇將被激發的線程掛起,直到有就緒的的通道。
可選擇通道(SelectableChannel)
SelectableChannel這個抽象類提供了實現通道的可選擇性所需要的公共方法。它是所有支持就緒檢查的通道類的父類。因為FileChannel類沒有繼承SelectableChannel因此是不是可選通道,而所有socket通道都是可選擇的,包括從管道(Pipe)對象的中獲得的通道。SelectableChannel可以被注冊到Selector對象上,同時可以指定對那個選擇器而言,那種操作是感興趣的。一個通道可以被注冊到多個選擇器上,但對每個選擇器而言只能被注冊一次。
選擇鍵(SelectionKey)
選擇鍵封裝了特定的通道與特定的選擇器的注冊關系。選擇鍵對象被SelectableChannel.register()返回并提供一個表示這種注冊關系的標記。選擇鍵包含了兩個比特集(以整數的形式進行編碼),指示了該注冊關系所關心的通道操作,以及通道已經準備好的操作。
下面是使用Selector管理多個channel的結構圖:
2. Selector的使用
(1)創建Selector
Selector對象是通過調用靜態工廠方法open()來實例化的,如下:
Selector Selector=Selector.open();
類方法open()實際上向SPI1發出請求,通過默認的SelectorProvider對象獲取一個新的實例。
(2)將Channel注冊到Selector
要實現Selector管理Channel,需要將channel注冊到相應的Selector上,如下:
channel.configureBlocking(false);SelectionKey key= channel.register(selector,SelectionKey,OP_READ);
通過調用通道的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_CONNECTSelectionKey.OP_ACCEPTSelectionKey.OP_READSelectionKey.OP_WRITE
如果Selector對通道的多操作類型感興趣,可以用“位或”操作符來實現:
int interestSet=SelectionKey.OP_READ|SelectionKey.OP_WRITE;
當通道觸發了某個操作之后,表示該通道的某個操作已經就緒,可以被操作。因此,某個SocketChannel成功連接到另一個服務器稱為“連接就緒”(OP_CONNECT)。一個ServerSocketChannel準備好接收新進入的連接稱為“接收就緒”(OP_ACCEPT)。一個有數據可讀的通道可以說是“讀就緒”(OP_READ)。等待寫數據的通道可以說是“寫就緒”(OP_WRITE)。
我們注意到register()方法會返回一個SelectionKey對象,我們稱之為鍵對象。該對象包含了以下四種屬性:
interest集合 read集合 Channel Selector
interest集合是Selector感興趣的集合,用于指示選擇器對通道關心的操作,可通過SelectionKey對象的interestOps()獲取。最初,該興趣集合是通道被注冊到Selector時傳進來的值。該集合不會被選擇器改變,但是可通過interestOps()改變。我們可以通過以下方法來判斷Selector是否對Channel的某種事件感興趣:
int interestSet=selectionKey.interestOps();boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
read集合是通道已經就緒的操作的集合,表示一個通道準備好要執行的操作了,可通過SelctionKey對象的readyOps()來獲取相關通道已經就緒的操作。它是interest集合的子集,并且表示了interest集合中從上次調用select()以后已經就緒的那些操作。(比如選擇器對通道的read,write操作感興趣,而某時刻通道的read操作已經準備就緒可以被選擇器獲知了,前一種就是interest集合,后一種則是read集合。)。JAVA中定義以下幾個方法用來檢查這些操作是否就緒:
//int readSet=selectionKey.readOps();selectionKey.isAcceptable();//等價于selectionKey.readyOps()&SelectionKey.OP_ACCEPTselectionKey.isConnectable();selectionKey.isReadable();selectionKey.isWritable();
需要注意的是,通過相關的選擇鍵的readyOps()方法返回的就緒狀態指示只是一個提示,底層的通道在任何時候都會不斷改變,而其他線程也可能在通道上執行操作并影響到它的就緒狀態。另外,我們不能直接修改read集合。
取出SelectionKey所關聯的Selector和Channel
通過SelectionKey訪問對應的Selector和Channel:
Channel channel =selectionKey.channel();Selector selector=selectionKey.selector();
關于取消SelectionKey對象的那點事
我們可以通過SelectionKey對象的cancel()方法來取消特定的注冊關系。該方法調用之后,該SelectionKey對象將會被”拷貝”至已取消鍵的集合中,該鍵此時已經失效,但是該注冊關系并不會立刻終結。在下一次select()時,已取消鍵的集合中的元素會被清除,相應的注冊關系也真正終結。
(3)為SelectionKey綁定附加對象
可以將一個或者多個附加對象綁定到SelectionKey上,以便容易的識別給定的通道。通常有兩種方式:
1 在注冊的時候直接綁定:
SelectionKey key=channel.register(selector,SelectionKey.OP_READ,theObject);
2 在綁定完成之后附加:
selectionKey.attach(theObject);//綁定
綁定之后,可通過對應的SelectionKey取出該對象:
selectionKey.attachment();
如果要取消該對象,則可以通過該種方式:
selectionKey.attach(null)
需要注意的是如果附加的對象不再使用,一定要人為清除,因為垃圾回收器不會回收該對象,若不清除的話會成內存泄漏。
一個單獨的通道可被注冊到多個選擇器中,有些時候我們需要通過isRegistered()方法來檢查一個通道是否已經被注冊到任何一個選擇器上。 通常來說,我們并不會這么做。
(4)通過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();}
關于Selector執行選擇的過程
我們知道調用select()方法進行通道,現在我們再來深入一下選擇的過程,也就是select()執行過程。當select()被調用時將執行以下幾步:
首先檢查已取消鍵集合,也就是通過cancle()取消的鍵。如果該集合不為空,則清空該集合里的鍵,同時該集合中每個取消的鍵也將從已注冊鍵集合和已選擇鍵集合中移除。(一個鍵被取消時,并不會立刻從集合中移除,而是將該鍵“拷貝”至已取消鍵集合中,這種取消策略就是我們常提到的“延遲取消”。) 再次檢查已注冊鍵集合(準確說是該集合中每個鍵的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()。
(5)停止選擇
選擇器執行選擇的過程,系統底層會依次詢問每個通道是否已經就緒,這個過程可能會造成調用線程進入阻塞狀態,那么我們有以下三種方式可以喚醒在select()方法中阻塞的線程。
通過調用Selector對象的wakeup()方法讓處在阻塞狀態的select()方法立刻返回 該方法使得選擇器上的第一個還沒有返回的選擇操作立即返回。如果當前沒有進行中的選擇操作,那么下一次對select()方法的一次調用將立即返回。 通過close()方法關閉Selector** 該方法使得任何一個在選擇操作中阻塞的線程都被喚醒(類似wakeup()),同時使得注冊到該Selector的所有Channel被注銷,所有的鍵將被取消,但是Channel本身并不會關閉。 調用interrupt() 調用該方法會使睡眠的線程拋出InterruptException異常,捕獲該異常并在調用wakeup()
上面有些人看到“系統底層會依次詢問每個通道”時可能在想如果已選擇鍵非常多是,會不會耗時較長?答案是肯定的。但是我想說的是通常你可以選擇忽略該過程,至于為什么,后面再說。
服務端
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(); }}
注意這里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; }}
界面代碼:設置姓名
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); }}
界面代碼:聊天界面
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); }}
分析整個程序的流程:
只有一個客戶端連接的注釋:
[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 –> onlinesun.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 –> onlinesun.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 –> onlinesun.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 –> onlinesun.nio.ch.SelectionKeyImpl@3ee5015 : 寫sun.nio.ch.SelectionKeyImpl@12cb94b7 : 寫sun.nio.ch.SelectionKeyImpl@3ee5015 : 寫
兩個Channel是交替運行的,說明Selector處理Channle,是輪詢處理的
以上是“JavaNIOSelector怎么用”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。