標籤:java nio selector serversocketchannel 即時通訊
上篇文章對NIO進行了簡介,對Channel和Buffer介面的使用進行了說明,並舉了一個簡單的例子來說明其使用方法。
本篇則重點說明selector,Selector(選取器)是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否為諸如讀寫事件做好準備的組件。這樣,一個單獨的線程可以管理多個channel,從而管理多個網路連接。
與selector聯絡緊密的是ServerSocketChannel和SocketChannel,他們的使用與上篇文章描述的FileChannel的使用方法類似,然後與ServerSocket和Socket也有一些聯絡。
本篇首先簡單的進selector進行說明,然後一個簡單的樣本程式,來示範即時通訊。
Selector
使用傳統IO進行網路編程,如所示:
每一個到服務端的串連,都需要一個單獨的線程(或者線程池)來處理其對應的socket,當串連數多的時候,對服務端的壓力極大。並使用socket的getInputStream。Read方法來不斷的輪訓每個socket,效率可想而知。
而selector則可以在同一個線程中監聽多個channel的狀態,當某個channel有selector感興趣的事情發現,selector則被啟用。即不會主動去輪詢。如所示:
Selector使用如下示意:
public static void main(String[] args) throws IOException { Selector selector = Selector.open();//聲明selector ServerSocketChannel sc = ServerSocketChannel.open(); sc.configureBlocking(false);//必須設定為非同步 sc.socket().bind(new InetSocketAddress(8081));//綁定連接埠 //把channel 註冊到 selector上 sc.register(selector, SelectionKey.OP_ACCEPT|SelectionKey.OP_CONNECT|SelectionKey.OP_READ|SelectionKey.OP_WRITE); while(true){ selector.select();//阻塞,直到註冊的channel上某個感興趣的事情發生 Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> 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的使用方法。
本例子包含服務端和用戶端兩個部分,其中服務端採用兩個selector,用來建立串連和資料的讀寫。兩個selector在兩個線程中。
服務端
/** * 簡單的即時通訊服務端,採用建立串連 selector和資料 selector分離。很不完善 * */public class ServerSocketChannelTest { private static final int SERVER_PORT = 8081; private ServerSocketChannel server; private volatile Boolean isStop = false; //負責建立串連的selector private Selector conn_Sel; //負責資料讀寫的selector private Selector read_Sel; // private ExecutorService sendService = Executors.newFixedThreadPool(3); //鎖,用來在建立串連後,喚醒read_Sel時使用的同步 private Object lock = new Object(); //註冊的使用者 private Map<String, ClientInfo> clents = new HashMap<String, ClientInfo>(); /** * 初始化,綁定連接埠 */ public void init() throws IOException { //建立ServerSocketChannel server = ServerSocketChannel.open(); //綁定連接埠 server.socket().bind(new InetSocketAddress(SERVER_PORT)); server.configureBlocking(false); //定義兩個selector conn_Sel = Selector.open(); read_Sel = Selector.open(); //把channel註冊到selector上,第二個參數為興趣的事件 server.register(conn_Sel, SelectionKey.OP_ACCEPT); } // 負責建立串連。 private void beginListen() { System.out.println("--------開始監聽----------"); while (!isStop) { try { conn_Sel.select(); } catch (IOException e) { e.printStackTrace(); continue; } Iterator<SelectionKey> it = conn_Sel.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey con = it.next(); it.remove(); if (con.isAcceptable()) { try { SocketChannel newConn = ((ServerSocketChannel) con .channel()).accept(); handdleNewInConn(newConn); } catch (IOException e) { e.printStackTrace(); continue; } } else if (con.isReadable()) {//廢代碼,執行不到。 try { handleData((SocketChannel) con.channel()); } catch (IOException e) { e.printStackTrace(); } } } } } /** * 負責接收資料 */ private void beginReceive(){ System.out.println("---------begin receiver data-------"); while (true) { synchronized (lock) { } try { read_Sel.select(); } catch (IOException e) { e.printStackTrace(); continue; } Iterator<SelectionKey> it = read_Sel.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey con = it.next(); it.remove(); if (con.isReadable()) { try { handleData((SocketChannel) con.channel()); } catch (IOException e) { e.printStackTrace(); } } } } } private void handdleNewInConn(SocketChannel newConn) throws IOException { newConn.configureBlocking(false); //這裡必須先喚醒read_Sel,然後加鎖,防止讀寫線程的中select方法再次鎖定。 synchronized (lock) { read_Sel.wakeup(); newConn.register(read_Sel, SelectionKey.OP_READ); } //newConn.register(conn_Sel, SelectionKey.OP_READ); } private void handleData(final SocketChannel data) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(512); try { int size= data.read(buffer); if (size==-1) { System.out.println("-------串連斷開-----"); //這裡暫時不處理,這裡可以移除已經註冊的用戶端 } } catch (IOException e) { e.printStackTrace(); return; } buffer.flip(); byte[] msgByte = new byte[buffer.limit()]; buffer.get(msgByte); Message msg = Message.getMsg(new String(msgByte)); //這裡讀完資料其實已經可以另開線程了下一步的處理,理想情況下,根據不同的訊息類型,建立不同的隊列,把待發送的訊息放進隊列 //當然也可以持久化。如果在資料沒有讀取前,另開線程的話,讀寫線程中 read_Sel.select(),會立刻返回。可以把 if (msg.getType().equals("0")) {// 註冊 ClientInfo info = new ClientInfo(msg.getFrom(), data); clents.put(info.getClentID(), info); System.out.println(msg.getFrom() + "註冊成功"); } else {// 轉寄 System.out.println("收到"+msg.getFrom()+"發給"+msg.getTo()+"的訊息"); ClientInfo to = clents.get(msg.getTo()); buffer.rewind(); if (to != null) { SocketChannel sendChannel = to.getChannel(); try { while (buffer.hasRemaining()) { sendChannel.write(buffer); } } catch (Exception e) { } finally { buffer.clear(); } } } } public static void main(String[] args) throws IOException { final ServerSocketChannelTest a = new ServerSocketChannelTest(); a.init(); new Thread("receive..."){ public void run() { a.beginReceive(); }; }.start(); a.beginListen(); } }
用戶端
/** * new 次對象,然後調用start方法,其中self 是自己id * * to 是接收人id * */public class Client { /** * 自己的ID */ private String self; /** * 接收人ID */ private String to; //通道管理器 private Selector selector; private ByteBuffer writeBuffer = ByteBuffer.allocate(512); private SocketChannel channel; private Object lock = new Object(); private volatile boolean isInit = false; public Client(String self, String to) { super(); this.self = self; this.to = to; } /** * 獲得一個Socket通道,並對該通道做一些初始化的工作 * @param ip 已連線的服務器的ip * @param port 已連線的服務器的連接埠號碼 * @throws IOException */ public void initClient(String ip,int port) throws IOException { // 獲得一個Socket通道 channel = SocketChannel.open(); // 設定通道為非阻塞 channel.configureBlocking(false); // 獲得一個通道管理器 this.selector = Selector.open(); // 用戶端串連伺服器,其實方法執行並沒有實現串連,需要在listen()方法中調 //用channel.finishConnect();才能完成串連 channel.connect(new InetSocketAddress(ip,port)); //將通道管理器和該通道綁定,並為該通道註冊SelectionKey.OP_CONNECT事件。 channel.register(selector, SelectionKey.OP_CONNECT); } /** * 採用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理 * @throws IOException */ @SuppressWarnings("unchecked") public void listen() throws IOException { // 輪詢訪問selector while (true) { synchronized (lock) { } selector.select(); // 獲得selector中選中的項的迭代器 Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator(); while (ite.hasNext()) { SelectionKey key = ite.next(); // 刪除已選的key,以防重複處理 ite.remove(); // 串連事件發生 if (key.isConnectable()) { SocketChannel channel = (SocketChannel) key .channel(); // 如果正在串連,則完成串連 if(channel.isConnectionPending()){ channel.finishConnect(); } // 設定成非阻塞 channel.configureBlocking(false); //在和服務端串連成功之後,為了可以接收到服務端的資訊,需要給通道設定讀的許可權。 channel.register(this.selector, SelectionKey.OP_READ); isInit = true; // 獲得了可讀的事件 } else if (key.isReadable()) { read(key); } } } } /** * 處理讀取服務端發來的資訊的事件 * @param key * @throws IOException */ public void read(SelectionKey key) throws IOException{ SocketChannel data = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(512) ; try { data.read(buffer ); } catch (IOException e) { e.printStackTrace(); data.close(); return; } buffer.flip(); byte[] msgByte = new byte[buffer.limit()]; buffer.get(msgByte); Message msg = Message.getMsg(new String(msgByte)); System.out.println("---收到訊息--"+msg+" 來自 "+msg.getFrom()); } private void sendMsg(String content){ writeBuffer.put(content.getBytes()); writeBuffer.flip(); try { while (writeBuffer.hasRemaining()) { channel.write(writeBuffer); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } writeBuffer.clear(); } /** * 啟動用戶端測試 * @throws IOException */ public void start() throws IOException { initClient("localhost",8081); new Thread("reading"){ public void run() { try { listen(); } catch (IOException e) { e.printStackTrace(); } }; }.start(); int time3 = 0; while(!isInit&&time3<3){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } time3 ++; } System.out.println("--------開始註冊------"); Message re = new Message("", self, ""); sendMsg(re.toString()); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----註冊成功----"); String content =""; System.out.println("---- 請輸入要發送的訊息,按斷行符號發送,輸入 123 退出----------"); Scanner s = new Scanner(System.in); while (!content.equals("123")&&s.hasNext()) { content = s.next(); Message msg = new Message(content, self, to); msg.setType("1"); sendMsg(msg.toString()); if (content.equals("123")) { break; } System.out.println("---發送成功---"); } channel.close(); } }
用戶端測試
public class TestClient1 { public static void main(String[] args) throws IOException { Client c1 =new Client("1", "2"); c1.start(); }}public class TestClient2 { public static void main(String[] args) throws IOException { Client c2 =new Client("2", "1"); c2.start(); }}
結束
本文的例子極為簡單,但是都經過測試。在編碼的過程中,遇到的問題主要有兩點:
1. channel.register()方法阻塞
2. 使用線程池遇到問題。本文最後在服務端的讀寫線程中,沒有使用線程池,原因注釋說的比較明白,也說明了使用線程池的一種設想。
另外在本文編碼過程中,遇到了一些問題,去網上尋求答案,遇到了一些不錯的文章,本文某些部分由參考。
selector的講解,官方文檔翻譯 http://ifeve.com/selectors/
NIO就緒的OP_write http://blog.csdn.net/zhouhl_cn/article/details/6582435
此文不錯:http://blog.csdn.net/jjzhk/article/details/39553613
http://www.2cto.com/kf/201312/267592.html
另外還有兩個反面教材:
http://www.oschina.net/code/snippet_860673_22507錯誤很大
http://www.oschina.net/code/snippet_246601_22883代碼本身是正確的,但底下的評論人沒有好好看書。
Java之NIO(二)selector socketChannel