1. 程式人生 > >Java網路程式設計--NIO非阻塞網路程式設計

Java網路程式設計--NIO非阻塞網路程式設計

從Java1.4開始,為了替代Java IO和網路相關的API,提高程式的執行速度,Java提供了新的IO操作非阻塞的API即Java NIO。NIO中有三大核心元件:Buffer(緩衝區),Channel(通道),Selector(選擇器)。NIO基於Channel(通道)和Buffer(緩衝區))進行操作,資料總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中,而Selector(選擇器)主要用於監聽多個通道的事件,實現單個執行緒可以監聽多個數據通道。

Buffer(緩衝區)

緩衝區本質上是一個可以寫入資料的記憶體塊(類似陣列),然後可以再次讀取。此記憶體塊包含在NIO Buffer物件中,該物件提供了一組方法,可以更輕鬆的使用記憶體塊。 相對於直接運算元組,Buffer API提供了更加容易的操作和管理,其進行資料的操作分為寫入和讀取,主要步驟如下:

  1. 將資料寫入緩衝區
  2. 呼叫buffer.flip(),轉換為讀取模式
  3. 緩衝區讀取資料
  4. 呼叫buffer.clear()或buffer.compact()清楚緩衝區

Buffer中有三個重要屬性: capacity(容量):作為一個記憶體塊,Buffer具有一定的固定大小,也稱為容量 position(位置):寫入模式時代表寫資料的位置,讀取模式時代表讀取資料的位置 limit(限制):寫入模式等於Buffer的容量,讀取模式時等於寫入的資料量

img

Buffer使用程式碼示例:

public class BufferDemo {
  public static void main(String[] args) {
    // 構建一個byte位元組緩衝區,容量是4
    ByteBuffer byteBuffer = ByteBuffer.allocate(4);
    // 預設寫入模式,檢視三個重要的指標
    System.out.println(
        String.format(
            "初始化:capacity容量:%s, position位置:%s, limit限制:%s",
            byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));
    // 寫入資料
    byteBuffer.put((byte) 1);
    byteBuffer.put((byte) 2);
    byteBuffer.put((byte) 3);
    // 再次檢視三個重要的指標
    System.out.println(
        String.format(
            "寫入3位元組後後:capacity容量:%s, position位置:%s, limit限制:%s",
            byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));

    // 轉換為讀取模式(不呼叫flip方法,也是可以讀取資料的,但是position記錄讀取的位置不對)
    System.out.println("開始讀取");
    byteBuffer.flip();
    byte a = byteBuffer.get();
    System.out.println(a);
    byte b = byteBuffer.get();
    System.out.println(b);
    System.out.println(
        String.format(
            "讀取2位元組資料後,capacity容量:%s, position位置:%s, limit限制:%s",
            byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));

    // 繼續寫入3位元組,此時讀模式下,limit=3,position=2.繼續寫入只能覆蓋寫入一條資料
    // clear()方法清除整個緩衝區。compact()方法僅清除已閱讀的資料。轉為寫入模式
    byteBuffer.compact();
    // 清除了已經讀取的2位元組,剩餘1位元組,還可以寫入3位元組資料
    // 多寫的話會報java.nio.BufferOverflowException異常
    byteBuffer.put((byte) 3);
    byteBuffer.put((byte) 4);
    byteBuffer.put((byte) 5);
    System.out.println(
        String.format(
            "最終的情況,capacity容量:%s, position位置:%s, limit限制:%s",
            byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));
  }
}

ByteBuffer堆外記憶體

ByteBuffer為效能關鍵型程式碼提供了直接記憶體(direct,堆外)和非直接記憶體(heap,堆)兩種實現。堆外記憶體實現將記憶體物件分配在Java虛擬機器的堆以外的記憶體,這些記憶體直接受作業系統管理,而不是虛擬機器,這樣做的結果就是能夠在一定程度上減少垃圾回收對應用程式造成的影響,提供執行的速度。

堆外記憶體的獲取方式:ByteBuffer byteBuffer = ByteBuffer.allocateDirect(noBytes)

堆外記憶體的好處:

  • 進行網路IO或者檔案IO時比heap buffer少一次拷貝。(file/socket -- OS memory -- jvm heap)在寫file和socket的過程中,GC會移動物件,JVM的實現中會把資料複製到堆外,再進行寫入。
  • GC範圍之外,降低GC壓力,但實現了自動管理,DirectByteBuffer中有一個Cleaner物件(PhantomReference),Cleaner被GC執行前會執行clean方法,觸發DirectByteBuffer中定義的Deallocator

堆外記憶體的使用建議:

  • 效能確實可觀的時候才去使用,分配給大型,長壽命的物件(網路傳輸,檔案讀寫等場景)
  • 通過虛擬機器引數MaxDirectMemorySize限制大小,防止耗盡整個機器的記憶體

Channel(通道)

Channel用於源節點與目標節點之間的連線,Channel類似於傳統的IO Stream,Channel本身不能直接訪問資料,Channel只能與Buffer進行互動。

Channel的API涵蓋了TCP/UDP網路和檔案IO,常用的類有FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel

標準IO Stream通常是單向的(InputStream/OutputStream),而Channel是一個雙向的通道,可以在一個通道內進行讀取和寫入,可以非阻塞的讀取和寫入通道,而且通道始終讀取和寫入緩衝區(即Channel必須配合Buffer進行使用)。

img

SocketChannel

SocketChannel用於建立TCP網路連線,類似java.net.Socket。有兩種建立SocketChannel的形式,一個是客戶端主動發起和伺服器的連線,還有一個就是服務端獲取的新連線。SocketChannel中有兩個重要的方法,一個是write()寫方法,write()寫方法有可能在尚未寫入內容的時候就返回了,需要在迴圈中呼叫write()方法。還有一個就是read()讀方法,read()方法可能直接返回根本不讀取任何資料,可以根據返回的int值判斷讀取了多少位元組。

核心程式碼程式碼示例片段:

// 客戶端主動發起連線
SocketChannel socketChannel = SocketChannel.open();
// 設定為非阻塞模式
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
// 發生請求資料 - 向通道寫入資料
socketChannel.write(byteBuffer);
// 讀取服務端返回 - 讀取緩衝區資料
int readBytes = socketChannel.read(requestBuffer);
// 關閉連線
socketChannel.close();

ServerSocketChannel

ServerSocketChannel可以監聽新建的TCP連線通道,類似ServerSocket。ServerSocketChannel的核心方法accept()方法,如果通道處於非阻塞模式,那麼如果沒有掛起的連線,該方法將立即返回null,實際使用中必須檢查返回的SocketChannel是否為null。

核心程式碼示例片段:

// 建立網路服務端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 設定為非阻塞模式
serverSocketChannel.configureBlocking(false);
// 繫結埠
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
  // 獲取新tcp連線通道
  SocketChannel socketChannel = serverSocketChannel.accept();
  if (socketChannel != null) {
    // tcp請求 讀取/響應
  }
}

Selector選擇器

Selector也是Java NIO核心元件,可以檢查一個或多個NIO通道,並確定哪些通道已經準備好進行讀取或寫入。實現單個執行緒可以管理多個通道,從而管理多個網路連線。

一個執行緒使用Selector可以監聽多個Channel的不同事件,其中主要有四種事件,分別對應SelectionKey中的四個常量,分別為:

  • 連線事件 SelectionKey.OP_CONNECT
  • 準備就緒事件 SelectionKey.OP_ACCEPT
  • 讀取事件 SelectionKey.OP_READ
  • 寫入事件 SelectionKey.OP_WRITE

img

Selector實現一個執行緒處理多個通道的核心在於事件驅動機制,非阻塞的網路通道下,開發者通過Selector註冊對於通道感興趣的事件型別,執行緒通過監聽事件來觸發相應的程式碼執行。(更底層其實是作業系統的多路複用機制)

核心程式碼示例片段:

// 構建一個Selector選擇器,並且將channel註冊上去
Selector selector = Selector.open();
// 將serverSocketChannel註冊到selector
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
// 對serverSocketChannel上面的accept事件感興趣(serverSocketChannel只能支援accept操作)
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
while (true) {
  // 用下面輪詢事件的方式.select方法有阻塞效果,直到有事件通知才會有返回
  selector.select();
  // 獲取事件
  Set<SelectionKey> keys = selector.selectedKeys();
  // 遍歷查詢結果
  Iterator<SelectionKey> iterator = keys.iterator();
  while (iterator.hasNext()) {
    // 被封裝的查詢結果
    SelectionKey key = iterator.next();
    // 判斷不同的事件型別,執行對應的邏輯處理
    if (key.isAcceptable()) {
      // 處理連線的邏輯
    }
    if (key.isReadable()) {
      //處理讀資料的邏輯
    }
    
    iterator.remove();
  }
}

NIO網路程式設計完整程式碼

服務端程式碼示例:

// 結合Selector實現的非阻塞服務端(放棄對channel的輪詢,藉助訊息通知機制)
public class NIOServer {

  public static void main(String[] args) throws IOException {
    // 建立網路服務端ServerSocketChannel
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 設定為非阻塞模式
    serverSocketChannel.configureBlocking(false);

    // 構建一個Selector選擇器,並且將channel註冊上去
    Selector selector = Selector.open();
    // 將serverSocketChannel註冊到selector
    SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
    // 對serverSocketChannel上面的accept事件感興趣(serverSocketChannel只能支援accept操作)
    selectionKey.interestOps(SelectionKey.OP_ACCEPT);

    // 繫結埠
    serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    System.out.println("啟動成功");

    while (true) {
      // 不再輪詢通道,改用下面輪詢事件的方式.select方法有阻塞效果,直到有事件通知才會有返回
      selector.select();
      // 獲取事件
      Set<SelectionKey> keys = selector.selectedKeys();
      // 遍歷查詢結果
      Iterator<SelectionKey> iterator = keys.iterator();
      while (iterator.hasNext()) {
        // 被封裝的查詢結果
        SelectionKey key = iterator.next();
        iterator.remove();
        // 關注 Read 和 Accept兩個事件
        if (key.isAcceptable()) {
          ServerSocketChannel server = (ServerSocketChannel) key.attachment();
          // 將拿到的客戶端連線通道,註冊到selector上面
          SocketChannel clientSocketChannel = server.accept();
          clientSocketChannel.configureBlocking(false);
          clientSocketChannel.register(selector, SelectionKey.OP_READ, clientSocketChannel);
          System.out.println("收到新連線 : " + clientSocketChannel.getRemoteAddress());
        }
        if (key.isReadable()) {
          SocketChannel socketChannel = (SocketChannel) key.attachment();
          try {
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            while (socketChannel.isOpen() && socketChannel.read(byteBuffer) != -1) {
              // 長連線情況下,需要手動判斷資料有沒有讀取結束 (此處做一個簡單的判斷: 超過0位元組就認為請求結束了)
              if (byteBuffer.position() > 0) break;
            }

            if (byteBuffer.position() == 0) continue;
            byteBuffer.flip();
            byte[] content = new byte[byteBuffer.limit()];
            byteBuffer.get(content);
            System.out.println(new String(content));
            System.out.println("收到資料,來自:" + socketChannel.getRemoteAddress());

            // 響應結果 200
            String response = "HTTP/1.1 200 OK\r\n" + "Content-Length: 11\r\n\r\n" + "Hello World";
            ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
            while (buffer.hasRemaining()) {
              socketChannel.write(buffer);
            }

          } catch (Exception e) {
            e.printStackTrace();
            key.cancel(); // 取消事件訂閱
          }
        }

        selector.selectNow();
      }
    }
  }
}

客戶端程式碼示例:

public class NIOClient {

  public static void main(String[] args) throws IOException {
    // 客戶端主動發起連線
    SocketChannel socketChannel = SocketChannel.open();
    // 設定為非阻塞模式
    socketChannel.configureBlocking(false);
    socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
    while (!socketChannel.finishConnect()) {
      // 沒連線上,則一直等待
      Thread.yield();
    }

    Scanner scanner = new Scanner(System.in);
    System.out.println("請輸入:");
    // 傳送內容
    String msg = scanner.nextLine();
    ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
    while (byteBuffer.hasRemaining()) {
      socketChannel.write(byteBuffer);
    }

    // 讀取響應
    System.out.println("收到服務端響應:");
    ByteBuffer buffer = ByteBuffer.allocate(1024);

    while (socketChannel.isOpen() && socketChannel.read(buffer) != -1) {
      // 長連線情況下,需要手動判斷資料有沒有讀取結束 (此處做一個簡單的判斷: 超過0位元組就認為請求結束了)
      if (buffer.position() > 0) break;
    }

    buffer.flip();
    byte[] content = new byte[buffer.limit()];
    buffer.get(content);
    System.out.println(new String(content));
    scanner.close();
    socketChannel.close();
  }
}

NIO與BIO的比較

img

如果程式需要支撐大量的連線,使用NIO是最好的方式。 Tomcat8中已經完全移除了BIO相關的網路處理程式碼,預設採用NIO進行網路處理。

相關推薦

Java網路程式設計 -- NIO阻塞網路程式設計

從Java1.4開始,為了替代Java IO和網路相關的API,提高程式的執行速度,Java提供了新的IO操作非阻塞的API即Java NIO。NIO中有三大核心元件:Buffer(緩衝區),Channel(通道),Selector(選擇器)。NIO基於Channel(通道)和Buffer(緩衝區))進行操

Java網路程式設計--NIO阻塞網路程式設計

從Java1.4開始,為了替代Java IO和網路相關的API,提高程式的執行速度,Java提供了新的IO操作非阻塞的API即Ja

Java中的NIO阻塞程式設計

平時工作中用到的IO主要是java.io包中的操作,比較少用到java.nio包中操作,最近遇到的比較多對效能要求較高的應用問題,查詢了一些資料整理記錄一下,方便以後檢視。 在JDK1.4以前,Java的IO操作集中在java.io這個包中,是基於流的阻塞AP

Java中的NIO阻塞模式和傳統的IO的阻塞模式線上程中的資源消耗

       java中的NIO對於需要IO操作的程式來說,大大的提高了效率,但從NIO的實現模式來看(底層select的遍歷),因為其非阻塞的特性,犧牲了更多的系統資源,充分利用了硬體資源。      在java的網路程式設計中,少不了執行緒操作。那麼這兩種模式對系統的消耗

Java網路程式設計阻塞通訊UDP

轉自 http://blog.csdn.net/alangdangjia/article/details/9065845 import java.io.BufferedReader;    import java.io.ByteArrayInputStream;  

Java NIO: Non-blocking Server 阻塞網路伺服器

本文翻譯自 Jakob Jenkov 的 Java NIO: Non-blocking Server ,原文地址:http://tutorials.jenkov.com/java-nio/non-blocking-server.html 文中所有想法均來自原作者,學習之餘,覺得很不錯,對以後深入學習伺服

Java入門系列-25-NIO(實現阻塞網路通訊)

還記得之前介紹NIO時對比傳統IO的一大特點嗎?就是NIO是非阻塞式的,這篇文章帶大家來看一下非阻塞的網路操作。 補充:以陣列的形式使用緩衝區 package testnio; import java.io.IOException; import java.io.RandomAccessFile; impo

翻譯:使用Libevent的快速可移植阻塞網路程式設計:非同步IO簡介 (一) (轉)

/* For sockaddr_in */#include <netinet/in.h>/* For socket functions */#include <sys/socket.h>/* For fcntl */#include <fcntl.h>#include &l

java NIO阻塞方式的Socket程式設計

1.非阻塞方式的Socket程式設計: 傳統阻塞方式的Socket程式設計,在讀取或者寫入資料時,TCP程式會阻塞直到客戶端和服務端成功連線,UDP程式會阻塞直到讀取到資料或寫入資料。阻塞方式會影響程式效能,JDK5之後的NIO引入了非阻塞方式的Socket程式設計,非

Java Socket程式設計阻塞多執行緒,NIO

服務端:伺服器Server類public class Server implements Runnable { private int port; private volatile boolean stop; private Selector sele

網路程式設計阻塞connect詳解

一、為什麼使用非阻塞connect     TCP連線的建立涉及一個在三路握手過程,阻塞的connect一直等到客戶收到自己的SYN的ACK才返回,這需要至少一個RTT時間,RTT時間波動很大從幾毫秒到幾秒。而且在沒有響應時,會等待數秒再次傳送,(詳見TCPv2第828頁)

python 網路程式設計學習 阻塞socket

主要學習伺服器的非同步使用 SocketServer簡化了網路伺服器的編寫。它有4個類:TCPServer,UDPServer,UnixStreamServer,UnixDatagramServer。這4個類是同步進行處理的,另外通過ForkingMixIn和Threadi

NIO實現阻塞Socket程式設計

前言: 基於阿里面試時,面試官問我,我做的聊天專案裡,考慮過效能沒有,是怎麼解決程式卡頓現象的,針對客戶端,當在傳送檔案時,如果卡頓,怎麼辦,同時想聊天,當時程式我是基於多執行緒實現的,在客戶端裡,聊天時啟動一個執行緒,傳送檔案時,啟動另一個執行緒,所以

阻塞socket程式設計

一. 阻塞、非阻塞、非同步 阻塞:阻塞呼叫是指呼叫結果返回之前,當前執行緒會被掛起。該程序被標記為睡眠狀態並被排程出去。函式只有在得到結果之後才會返回。當socket工作在阻塞模式的時候, 如果沒有資料的情況下呼叫該函式,則當前執行緒就會被掛起,直到有資料為止。 非阻塞:非阻塞和阻塞的概念相

Java NIO 阻塞socket通訊案例

NIO的特性:它以塊為基本單位處理資料,所有的資料都要通過緩衝區(Buffer)來進行傳輸。它有一個用來作為原始I/O操作的抽象通道(Channel)並提供了Selector的非同步網路介面。且支援將檔案對映到記憶體,以大幅提高I/O效率。 緩衝區中有3個重要

Java NIO-阻塞通訊

NIO(Non-block IO)指非阻塞通訊,相對於其程式設計的複雜性,通常客戶端並不需要使用非阻塞通訊以提高效能,故這裡只有服務端使用非阻塞通訊方式實現 客戶端: package com.test.client; import java.io.DataInputSt

基於Socket的多執行緒和非同步阻塞模式程式設計

      剛開始接觸socket的程式設計的時候,遇到了很多的問題,費了很大勁搞懂。其實往往都是一些比較基本的知識,但是都是很重要的,只要對其熟練的掌握後,相信對基於網路的程式設計會有很大的提高,呵呵。       就拿基於C/S結構的例子來說,我們先看看伺服器和客戶端的流

另一種實現阻塞網路通訊的方法———使用libev

背景:最近終於開始了我的實習生之路,本來在進公司之前還比較緊張,儘管拿到了offer,因為畢竟這是一個新的起點,一開始從學生到員工這個身份的轉變讓我有些不太適應,但是還好在公司裡遇到了人超級好的軟體經理Alex以及其他精明能幹的小夥伴們,所以這個過渡時間也很快。 一開始Al

同步與非同步 阻塞阻塞 WinSock程式設計

首先,先推薦兩個人寫的部落格,裡面有他們對於《同步與非同步 阻塞與非阻塞》的理解,最近被這兩個概念搞的頭疼,一直也沒有什麼頭緒。 個人理解,同步與非同步的區別就是看是程式自己接受完成的訊號還是由別人通知,想WSAsyncSelect模型是非同步的,它即是通過程式實現約

理解JAVA nio阻塞

這幾天在看JAVA nio相關的東西,網上資料說nio是非阻塞的,我實在理解不到這句話的意思; 比如我用nio來複制檔案,那一句程式碼表示非阻塞呢?我冥思苦想,實在想不出。 public static void main(String[] args) throws Exc