【網絡編程】NIO編程

NIO是jdk1.4引入的java.nio包,它提供了高速的、面向塊的IO。通過定義包含數據的類,以及通過塊的形式處理這些數據。NIO類庫包含緩衝區Buffer、多路複用選擇器Selector、通道Channel等新的抽象,可以構建多路複用、同步非阻塞的IO程序,同時提供了更接近操作系統底層高性能的數據操作方式。

緩衝區Buffer,包含一些要寫入或者要讀出的數據。在面向流的IO中,可以將數據直接寫入或者將數據直接讀到Stream對象中。緩衝區提供了對數據的結構化訪問以及維護讀寫位置等信息。

緩衝區Buffer是一個數組,通常是一個字節數組ByteBuffer,還有其他類型的數組字符緩衝區CharBuffer、短整型緩衝區ShortBuffer、整型緩衝區IntBuffer、長整形緩衝區LongBuffer、浮點型整型區FloatBuffer、雙精度浮點型緩衝區。每一種Buffer的類都是Buffer接口的一個子實例。所以它們有完全一樣的操作,只是操作的數據類型不一樣。

通道Channel,Channel好比自來水管,網絡數據通過Channel讀取和寫入。通道與流的不同之處在於,通道是雙向的,而流是單向的,流只能在一個方向上移動,一個流必須是InputStream或者OutputStream的子類,通道可以用於讀寫或者兩者同時進行。通道Channel是全雙工的,所以它可以比流更好地映射底層操作系統的API。Channel可以分爲兩大類,一類用於網絡對象,一類用於文件操作。

多路複用選擇器Selector,Selector會不斷輪詢註冊在其上的Channel,如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,通過SelectionKey可以獲得就緒Channel的集合,進行後續的IO操作。

一個多路複用器Selector可以同時輪詢多個Channel,由於JDK使用epoll()代替傳統的select實現,所以它並沒有最大連接句柄1024/2048的限制。只要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。

下面來看服務器端代碼:

package com.test.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * NIO服務器端
 * @author 程就人生
 * @Date
 */
public class HelloServer {

  public static void main( String[] args ){
    int port = 8080;
    // 多路複用服務類
    MultiplexerHelloServer helloServer = new MultiplexerHelloServer(port);
        new Thread(helloServer,"多路複用服務類").start();
    }
}

class MultiplexerHelloServer implements Runnable{

  private Selector selector;

  private ServerSocketChannel serverChannel;

  private volatile boolean stop;

  /**
   * 初始化多路複用,綁定監聽端口
   * @param port
   */
  MultiplexerHelloServer(int port){
    try {
      // 初始化多路複用器,創建Selector
      selector = Selector.open();
      // 打開ServerSocketChannel
      serverChannel = ServerSocketChannel.open();
      // 設置爲非堵塞模式
      serverChannel.configureBlocking(false);
      // 綁定監聽端口
      serverChannel.socket().bind(new InetSocketAddress(port), 1024);
      // 將ServerSocketChannel註冊到Selector上去,監聽accept事件
      serverChannel.register(selector, SelectionKey.OP_ACCEPT);
      System.out.println("服務器端已啓動,啓動端口爲:" + port);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public void stop(){
    this.stop = true;
  }

  public void run() {
    while(!stop){
      try {
        SelectionKey key = null;
        // 每隔一秒被喚醒一次
        selector.select(1000);
        // 獲取就緒狀態的SelectionKey
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        // 對就緒狀態的SelectionKey進行迭代
        Iterator<SelectionKey> it = selectedKeys.iterator();        
        while(it.hasNext()){
          key = it.next();
          it.remove();
          try{
            // 對網絡事件進行操作(連接和讀寫操作)
            handleInput(key);
          }catch(Exception e){
            if(key != null){
              key.cancel();
              if(key.channel() != null){
                try {
                  key.channel().close();
                } catch (IOException e1) {
                  e1.printStackTrace();
                }
              }
            }
          }          
        }
      } catch (IOException e) {
        e.printStackTrace();
      }      
    }  
    // 多路複用器關閉後,所有註冊在上面的channel和pipe等資源都會被自動去註冊並關閉,所以不需要重複釋放資源
    if(selector != null){
      try {
        selector.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  private void handleInput(SelectionKey key) throws IOException{
    if(key.isValid()){
      // 處理新接入的客戶端請求信息
      if(key.isAcceptable()){
        // 接入新的連接
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        SocketChannel sc = ssc.accept();
        // 設置爲異步非阻塞
        sc.configureBlocking(false);
        // 監聽讀操作
        sc.register(selector, SelectionKey.OP_READ);
      }
      // 處理客戶端發來的信息,讀取操作
      if(key.isReadable()){
        SocketChannel sc = (SocketChannel) key.channel();
        // 開闢一個1KB的緩衝區
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        int readBytes = sc.read(readBuffer);
        // 讀取到了字節
        if(readBytes > 0){
          readBuffer.flip();
          byte[] bytes = new byte[readBuffer.remaining()];
          readBuffer.get(bytes);
          // 對字節碼進行解碼
          String body = new String(bytes, "UTF-8");
          System.out.println("服務器端收到的:" + body);
          // 回寫客戶端
          doWrite(sc);        
        }
      }
    }
  }

  // 回寫客戶端
  private void doWrite(SocketChannel sc) throws IOException{
    byte[] bytes = "服務器端的響應來了".getBytes();
    ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
    writeBuffer.put(bytes);
    writeBuffer.flip();
    sc.write(writeBuffer);
  }
}

在40-56行,在構造方法中對資源進行初始化。創建多路複用選擇器Selector、ServerSocketChannel,對Channel和TCP參數進行配置。

在63行代碼,對網絡事件進行輪詢監聽;在67行代碼中,每隔1s喚醒一次,監聽多路複用選擇器中是否有就緒的SelectionKey,如果有則進行遍歷。

在105行代碼中,在handleInput方法中,對SelectionKey進行判斷。判斷SelectionKey目前所處的狀態,是接入的新連接,還是處於網絡讀狀態。如果是新連接,則監聽網絡讀操作。如果是網絡讀操作,在通過doWrite方法回寫客戶端。

客戶端代碼:

package com.test.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
 * NIO客戶端
 * @author 程就人生
 * @Date
 */
public class HelloClient {

  public static void main( String[] args ){
    int port = 8080;
    new Thread(new HelloClientHandle("127.0.0.1", port)).start();    
  }
}

/**
 * 客戶端處理器
 * @author 程就人生
 * @Date
 */
class HelloClientHandle implements Runnable{

  private Selector selector;

  private SocketChannel socketChannel;

  private volatile boolean stop;

  private String host;

  private int port;

  public HelloClientHandle(String host, int port) {
    try {
      this.host = host;
      this.port = port;
      // 創建多路複用選擇器
      selector = Selector.open();
      // 打開SocketChannel
      socketChannel = SocketChannel.open();
      // 設置爲非阻塞
      socketChannel.configureBlocking(false);      
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public void run() {
    try {
      // 連接服務器端判斷
      if(!socketChannel.connect(new InetSocketAddress(host,port))){        
        // 將socketChannel註冊到多路複用器,並監聽連接操作
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    while(!stop){
      try {
        selector.select(1000);
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        Iterator<SelectionKey> it = selectedKeys.iterator();
        SelectionKey key = null;
        while(it.hasNext()){
          key = it.next();
          it.remove();
          handleInput(key);
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    if(selector != null){
      try {
        selector.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }    
  }

  private void handleInput(SelectionKey key) throws IOException{
    if(key.isValid()){
      SocketChannel sc = (SocketChannel) key.channel();
      if(key.isConnectable()){
        // 完成連接
        if(sc.finishConnect()){
          // 監聽讀時間
          sc.register(selector, SelectionKey.OP_READ);
          // 給服務器端發送消息
          doWrite(sc);
        }else{
          // 退出
          System.exit(1);
        }
      }
      // 是否爲可讀的
      if(key.isReadable()){
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        int readBytes =sc.read(readBuffer);
        // 讀到了字節
        if(readBytes > 0){
          readBuffer.flip();
          byte[] bytes = new byte[readBuffer.remaining()];
          readBuffer.get(bytes);
          String body = new String(bytes, "utf-8");
          System.out.println("客戶端收到的信息是:" + body);
          this.stop = true;

          // 沒讀到字節
        } else if(readBytes < 0){
          // 取消
          key.cancel();
          // 關閉連接
          sc.close();
        }
      }
    }    
  }

  /**
   * 網絡寫操作
   * @param sc
   * @throws IOException
   */
  private void doWrite(SocketChannel sc) throws IOException{
    byte[] req = "來自客戶端的消息".getBytes();
    ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
    writeBuffer.put(req);
    writeBuffer.flip();
    sc.write(writeBuffer);
    if(!writeBuffer.hasRemaining()){
      System.out.println("已發送給服務器端~!");
    }
  }
}

客戶端的連接步驟:先創建多路複用選擇器,打開SocketChannel,綁定本地端地址,設置SocketChannel爲非阻塞,異步連接服務器。將SocketChannel註冊到多路選擇器中,註冊監聽事件。

SocketChannel連接服務器連接成功後,對SelectionKey進行輪詢監聽,每隔10s喚醒一次。在97行,連接成功後,監聽網絡讀事件,並給服務器端發送消息。

在第106行,堅挺到網絡讀事件後,將字節讀出,並打印出來。如果讀取完畢,則關閉通道,關閉連接。

客戶端發起的連接操作是異步的,通過在多路複用器註冊OP_CONNECT等待後續結果,不需要之前那樣被同步阻塞。SocketChannel的讀寫操作都是異步的。如果沒有可讀寫的數據不會同步等待。

以上便是來自java.nio包的非阻塞服務器端、客戶端編碼的簡單演示。

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