您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關Java中怎么實現NIO非阻塞網絡編程,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
緩沖區本質上是一個可以寫入數據的內存塊(類似數組),然后可以再次讀取。此內存塊包含在NIO Buffer對象中,該對象提供了一組方法,可以更輕松的使用內存塊。
相對于直接操作數組,Buffer API提供了更加容易的操作和管理,其進行數據的操作分為寫入和讀取,主要步驟如下:
將數據寫入緩沖區
調用buffer.flip(),轉換為讀取模式
緩沖區讀取數據
調用buffer.clear()或buffer.compact()清除緩沖區
Buffer中有三個重要屬性:
capacity(容量):作為一個內存塊,Buffer具有一定的固定大小,也稱為容量
position(位置):寫入模式時代表寫數據的位置,讀取模式時代表讀取數據的位置
limit(限制):寫入模式等于Buffer的容量,讀取模式時等于寫入的數據量
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為性能關鍵型代碼提供了直接內存(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類似于傳統的IO Stream,Channel本身不能直接訪問數據,Channel只能與Buffer進行交互。
Channel的API涵蓋了TCP/UDP網絡和文件IO,常用的類有FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel
標準IO Stream通常是單向的(InputStream/OutputStream),而Channel是一個雙向的通道,可以在一個通道內進行讀取和寫入,可以非阻塞的讀取和寫入通道,而且通道始終讀取和寫入緩沖區(即Channel必須配合Buffer進行使用)。
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可以監聽新建的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也是Java NIO核心組件,可以檢查一個或多個NIO通道,并確定哪些通道已經準備好進行讀取或寫入。實現單個線程可以管理多個通道,從而管理多個網絡連接。
一個線程使用Selector可以監聽多個Channel的不同事件,其中主要有四種事件,分別對應SelectionKey中的四個常量,分別為:
連接事件 SelectionKey.OP_CONNECT
準備就緒事件 SelectionKey.OP_ACCEPT
讀取事件 SelectionKey.OP_READ
寫入事件 SelectionKey.OP_WRITE
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();
}
}
服務端代碼示例:
// 結合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();
}
}
關于Java中怎么實現NIO非阻塞網絡編程就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。