Java之NIO(二)selector socketChannel

來源:互聯網
上載者:User

標籤: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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.