4.3.3 管理選擇鍵 既然我們已經理解了問題的各個部分是怎樣結合在一起的,那么是時候看看它們在正常的使用中是如何交互的了。為了有效地利用選擇器和鍵提供的信息,合理地管理鍵是非常重要的。 選擇是累積的。一旦一個選擇器將一個鍵添加到它的已選擇的鍵的集合中,它就不會移除這個鍵。并且,一旦一個鍵處于已選擇的鍵的集合中,這個鍵的ready集合將只會被設置,而不會被清理。乍一看,這好像會引起麻煩,因為選擇操作可能無法表現出已注冊的通道的正確狀態(tài)。它提供了極大的靈活性,但把合理地管理鍵以確保它們表示的狀態(tài)信息不會變得陳舊的任務交給了程序員。 合理地使用選擇器的秘訣是理解選擇器維護的選擇鍵集合所扮演的角色。(參見 4.3.1 小節(jié),特別是選擇過程的第二步。)最重要的部分是當鍵已經不再在已選擇的鍵的集合中時將會發(fā)生什么。當通道上的至少一個感興趣的操作就緒時,鍵的ready集合就會被清空,并且當前已經就緒的操作將會被添加到ready集合中。該鍵之后將被添加到已選擇的鍵的集合中。 清理一個SelectKey的ready集合的方式是將這個鍵從已選擇的鍵的集合中移除。選擇鍵的就緒狀態(tài)只有在選擇器對象在選擇操作過程中才會修改。處理思想是只有在已選擇的鍵的集合中的鍵才被認為是包含了合法的就緒信息的。這些信息將在鍵中長久地存在,直到鍵從已選擇的鍵的集合中移除,以通知選擇器您已經看到并對它進行了處理。如果下一次通道的一些感興趣的操作發(fā)生時,鍵將被重新設置以反映當時通道的狀態(tài)并再次被添加到已選擇的鍵的集合中。 這種框架提供了很多靈活性。通常的做法是在選擇器上調用一次select操作(這將更新已選擇的鍵的集合),然后遍歷selectKeys()方法返回的鍵的集合。在按順序進行檢查每個鍵的過程中,相關的通道也根據鍵的就緒集合進行處理。然后鍵將從已選擇的鍵的集合中被移除(通過在Iterator對象上調用remove()方法),然后檢查下一個鍵。完成后,通過再次調用select()方法重復這個循環(huán)。例 4-1 中的代碼是典型的服務器的例子。 /* *例 4-1. 使用 select()來為多個通道提供服務 */ package com.ronsoft.books.nio.channels; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.Selector; import java.nio.channels.SelectionKey; import java.nio.channels.SelectableChannel; import java.net.Socket; import java.net.ServerSocket; import java.net.InetSocketAddress; import java.util.Iterator; /** * Simple echo-back server which listens for incoming stream connections and * echoes back whatever it reads. A single Selector object is used to listen to * the server socket (to accept new connections) and all the active socket * channels. * * @author Ron Hitchens (ron@ronsoft.com) */ public class SelectSockets { public static int PORT_NUMBER = 1234; public static void main(String[] argv) throws Exception { new SelectSockets().go(argv); } public void go(String[] argv) throws Exception { int port = PORT_NUMBER; if (argv.length > 0) { // Override default listen port port = Integer.parseInt(argv[0]); } System.out.println("Listening on port " port); // Allocate an unbound server socket channel ServerSocketChannel serverChannel = ServerSocketChannel.open(); // Get the associated ServerSocket to bind it with ServerSocket serverSocket = serverChannel.socket(); // Create a new Selector for use below Selector selector = Selector.open(); // Set the port the server channel will listen to serverSocket.bind(new InetSocketAddress(port)); // Set nonblocking mode for the listening socket serverChannel.configureBlocking(false); // Register the ServerSocketChannel with the Selector serverChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // This may block for a long time. Upon returning, the // selected set contains keys of the ready channels. int n = selector.select(); if (n == 0) { continue; // nothing to do } // Get an iterator over the set of selected keys Iterator it = selector.selectedKeys().iterator(); // Look at each key in the selected set while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); // Is a new connection coming in? if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel = server.accept(); registerChannel(selector, channel, SelectionKey.OP_READ); sayHello(channel); } // Is there data to read on this channel? if (key.isReadable()) { readDataFromSocket(key); } // Remove key from selected set; it's been handled it.remove(); } } } // ---------------------------------------------------------- /** * Register the given channel with the given selector for the given * operations of interest */ protected void registerChannel(Selector selector, SelectableChannel channel, int ops) throws Exception { if (channel == null) { return; // could happen } // Set the new channel nonblocking channel.configureBlocking(false); // Register it with the selector channel.register(selector, ops); } // ---------------------------------------------------------- // Use the same byte buffer for all channels. A single thread is // servicing all the channels, so no danger of concurrent acccess. private ByteBuffer buffer = ByteBuffer.allocateDirect(1024); /** * Sample data handler method for a channel with data ready to read. * * @param key * A SelectionKey object associated with a channel determined by * the selector to be ready for reading. If the channel returns * an EOF condition, it is closed here, which automatically * invalidates the associated key. The selector will then * de-register the channel on the next select call. */ protected void readDataFromSocket(SelectionKey key) throws Exception { SocketChannel socketChannel = (SocketChannel) key.channel(); int count; buffer.clear(); // Empty buffer // Loop while data is available; channel is nonblocking while ((count = socketChannel.read(buffer)) > 0) { buffer.flip(); // Make buffer readable // Send the data; don't assume it goes all at once while (buffer.hasRemaining()) { socketChannel.write(buffer); } // WARNING: the above loop is evil. Because // it's writing back to the same nonblocking // channel it read the data from, this code can // potentially spin in a busy loop. In real life // you'd do something more useful than this. buffer.clear(); // Empty buffer } if (count < 0) { // Close channel on EOF, invalidates the key socketChannel.close(); } } // ---------------------------------------------------------- /** * Spew a greeting to the incoming client connection. * * @param channel * The newly connected SocketChannel to say hello to. */ private void sayHello(SocketChannel channel) throws Exception { buffer.clear(); buffer.put("Hi there!\r\n".getBytes()); buffer.flip(); channel.write(buffer); } } 例 4-1 實現了一個簡單的服務器。它創(chuàng)建了ServerSocketChannel和Selector對象,并將通道注冊到選擇器上。我們不在注冊的鍵中保存服務器socket的引用,因為它永遠不會被注銷。這個無限循環(huán)在最上面先調用了select(),這可能會無限期地阻塞。當選擇結束時,就遍歷選擇鍵并檢查已經就緒的通道。 如果一個鍵指示與它相關的通道已經準備好執(zhí)行一個accecpt()操作,我們就通過鍵獲取關聯(lián)的通道,并將它轉換為SeverSocketChannel對象。我們都知道這么做是安全的,因為只有ServerSocketChannel支持OP_ACCEPT操作。我們也知道我們的代碼只把對一個單一的ServerSocketChannel對象的OP_ACCEPT操作進行了注冊。通過對服務器socket通道的引用,我們調用了它的accept()方法,來獲取剛到達的socket的句柄。返回的對象的類型是SocketChannel,也是一個可選擇的通道類型。這時,與創(chuàng)建一個新線程來從新的連接中讀取數據不同,我們只是簡單地將socket同多注冊到選擇器上。我們通過傳入OP_READ標記,告訴選擇器我們關心新的socket通道什么時候可以準備好讀取數據。 如果鍵指示通道還沒有準備好執(zhí)行accept(),我們就檢查它是否準備好執(zhí)行read()。任何一個這么指示的socket通道一定是之前ServerSocketChannel創(chuàng)建的SocketChannel對象之一,并且被注冊為只對讀操作感興趣。對于每個有數據需要讀取的socket通道,我們調用一個公共的方法來讀取并處理這個帶有數據的socket。需要注意的是這個公共方法需要準備好以非阻塞的方式處理socket上的不完整的數據。它需要迅速地返回,以其他帶有后續(xù)輸入的通道能夠及時地得到處理。例 4-1 中只是簡單地對數據進行響應,將數據寫回socket,傳回給發(fā)送者。 在循環(huán)的底部,我們通過調用Iterator(迭代器)對象的remove()方法,將鍵從已選擇的鍵的集合中移除。鍵可以直接從selectKeys()返回的Set中移除,但同時需要用Iterator來檢查集合,您需要使用迭代器的remove()方法來避免破壞迭代器內部的狀態(tài)。 Java nio入門教程詳解(三十七) 00 我們認為:用戶的主要目的,是為了獲取有用的信息,而不是來點擊廣告的。因此本站將竭力做好內容,并將廣告和內容進行分離,確保所有廣告不會影響到用戶的正常閱讀體驗。用戶僅憑個人意愿和興趣愛好點擊廣告。 我們堅信:只有給用戶帶來價值,用戶才會給我們以回報。 |
|