Netty 是基於Java NIO 封裝的網路通訊框架,只有充分理解了 Java NIO 才能理解好Netty的底層設計。Java NIO 由三個核心元件元件:
-
Buffer
-
Channel
-
Selector
緩衝區 Buffer
Buffer 是一個資料物件,我們可以把它理解為固定數量的資料的容器,它包含一些要寫入或者讀出的資料。
在 Java NIO 中,任何時候訪問 NIO 中的資料,都需要通過緩衝區(Buffer)進行操作。讀取資料時,直接從緩衝區中讀取,寫入資料時,寫入至緩衝區。NIO 最常用的緩衝區則是 ByteBuffer。下圖是 Buffer 繼承關係圖:
每一個 Java 基本型別都對應著一種 Buffer,他們都包含這相同的操作,只不過是所處理的資料型別不同而已。
通道 Channel
Channel 是一個通道,它就像自來水管一樣,網路資料通過 Channel 這根水管讀取和寫入。傳統的 IO 是基於流進行操作的,Channle 和類似,但又有些不同:
區別 | 流 | 通過Channel |
---|---|---|
支援非同步 | 不支援 | 支援 |
是否可雙向傳輸資料 | 不能,只能單向 | 可以,既可以從通道讀取資料,也可以向通道寫入資料 |
是否結合 Buffer 使用 | 不 | 必須結合 Buffer 使用 |
效能 | 較低 | 較高 |
正如上面說到的,Channel 必須要配合 Buffer 一起使用,我們永遠不可能將資料直接寫入到 Channel 中,同樣也不可能直接從 Channel 中讀取資料。都是通過從 Channel 讀取資料到 Buffer 中或者從 Buffer 寫入資料到 Channel 中,如下:
簡單點說,Channel 是資料的源頭或者資料的目的地,用於向 buffer 提供資料或者讀取 buffer 資料,並且對 I/O 提供非同步支援。
下圖是 Channel 的類圖
Channel 為最頂層介面,所有子 Channel 都實現了該介面,它主要用於 I/O 操作的連線。定義如下:
public interface Channel extends Closeable {
/**
* 判斷此通道是否處於開啟狀態。
*/
public boolean isOpen();
/**
*關閉此通道。
*/
public void close() throws IOException;
}
最為重要的Channel實現類為:
-
FileChannel:一個用來寫、讀、對映和操作檔案的通道
-
DatagramChannel:能通過 UDP 讀寫網路中的資料
-
SocketChannel: 能通過 TCP 讀寫網路中的資料
-
ServerSocketChannel:可以監聽新進來的 TCP 連線,像 Web 伺服器那樣。對每一個新進來的連線都會建立一個 SocketChannel
多路複用器 Selector
多路複用器 Selector,它是 Java NIO 程式設計的基礎,它提供了選擇已經就緒的任務的能力。從底層來看,Selector 提供了詢問通道是否已經準備好執行每個 I/O 操作的能力。簡單來講,Selector 會不斷地輪詢註冊在其上的 Channel,如果某個 Channel 上面發生了讀或者寫事件,這個 Channel 就處於就緒狀態,會被 Selector 輪詢出來,然後通過 SelectionKey 可以獲取就緒 Channel 的集合,進行後續的 I/O 操作。
Selector 允許一個執行緒處理多個 Channel ,也就是說只要一個執行緒複雜 Selector 的輪詢,就可以處理成千上萬個 Channel ,相比於多執行緒來處理勢必會減少執行緒的上下文切換問題。下圖是一個 Selector 連線三個 Channel :
例項
服務端
public class NIOServer {
/*接受資料緩衝區*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(1024);
/*傳送資料緩衝區*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(1024);
private Selector selector;
public NIOServer(int port) throws IOException {
// 開啟伺服器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 伺服器配置為非阻塞
serverSocketChannel.configureBlocking(false);
// 檢索與此通道關聯的伺服器套接字
ServerSocket serverSocket = serverSocketChannel.socket();
// 進行服務的繫結
serverSocket.bind(new InetSocketAddress(port));
// 通過open()方法找到Selector
selector = Selector.open();
// 註冊到selector,等待連線
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server Start----:");
}
private void listen() throws IOException {
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
handleKey(selectionKey);
}
}
}
private void handleKey(SelectionKey selectionKey) throws IOException {
// 接受請求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText;
String sendText;
int count=0;
// 測試此鍵的通道是否已準備好接受新的套接字連線。
if (selectionKey.isAcceptable()) {
// 返回為之建立此鍵的通道。
server = (ServerSocketChannel) selectionKey.channel();
// 接受到此通道套接字的連線。
// 此方法返回的套接字通道(如果有)將處於阻塞模式。
client = server.accept();
// 配置為非阻塞
client.configureBlocking(false);
// 註冊到selector,等待連線
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 返回為之建立此鍵的通道。
client = (SocketChannel) selectionKey.channel();
//將緩衝區清空以備下次讀取
receivebuffer.clear();
//讀取伺服器傳送來的資料到緩衝區中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("伺服器端接受客戶端資料--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
//將緩衝區清空以備下次寫入
sendbuffer.clear();
// 返回為之建立此鍵的通道。
client = (SocketChannel) selectionKey.channel();
sendText="message from server--";
//向緩衝區中輸入資料
sendbuffer.put(sendText.getBytes());
//將緩衝區各標誌復位,因為向裡面put了資料標誌被改變要想從中讀取資料發向伺服器,就要復位
sendbuffer.flip();
//輸出到通道
client.write(sendbuffer);
System.out.println("伺服器端向客戶端傳送資料--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
public static void main(String[] args) throws IOException {
int port = 8080;
NIOServer server = new NIOServer(port);
server.listen();
}
}
客戶端
public class NIOClient {
/*接受資料緩衝區*/
private static ByteBuffer sendbuffer = ByteBuffer.allocate(1024);
/*傳送資料緩衝區*/
private static ByteBuffer receivebuffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws IOException {
// 開啟socket通道
SocketChannel socketChannel = SocketChannel.open();
// 設定為非阻塞方式
socketChannel.configureBlocking(false);
// 開啟選擇器
Selector selector = Selector.open();
// 註冊連線服務端socket動作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 連線
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
Set<SelectionKey> selectionKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count=0;
while (true) {
//選擇一組鍵,其相應的通道已為 I/O 操作準備就緒。
//此方法執行處於阻塞模式的選擇操作。
selector.select();
//返回此選擇器的已選擇鍵集。
selectionKeys = selector.selectedKeys();
//System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("client connect");
client = (SocketChannel) selectionKey.channel();
// 判斷此通道上是否正在進行連線操作。
// 完成套接字通道的連線過程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("完成連線!");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
//將緩衝區清空以備下次讀取
receivebuffer.clear();
//讀取伺服器傳送來的資料到緩衝區中
count=client.read(receivebuffer);
if(count>0){
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("客戶端接受伺服器端資料--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "message from client--";
sendbuffer.put(sendText.getBytes());
//將緩衝區各標誌復位,因為向裡面put了資料標誌被改變要想從中讀取資料發向伺服器,就要復位
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客戶端向伺服器端傳送資料--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
selectionKeys.clear();
}
}
}
執行結果