傳統IO模型
在開始瞭解 Netty 是什麼之前,我們先來回顧一下,如果我們需要實現一個客戶端與服務端通信的程序,使用傳統的Socket通信,應該如何來實現?
public class BIOServer {
public static void main(String[] args) throws Exception {
//創建Socket服務,監聽8000端口
ServerSocket server = new ServerSocket(8000);
System.out.println("服務端啓動!");
while (true) {
//獲取一個套接字(阻塞)
final Socket socket = server.accept();
System.out.println("出現一個新客戶端!");
//業務處理
handle(socket);
}
}
/**
* 處理數據
* @param socket
* @throws IOException
*/
private static void handle(Socket socket) throws IOException {
byte[] bytes = new byte[1024];
InputStream input = socket.getInputStream();
int read = 0;
while (read != -1) {
//讀取數據(阻塞)
read = input.read(bytes);
System.out.println(new String(bytes, 0, read));
}
}
}
這段代碼上面有兩個阻塞點,一個是server.accept()(等待客戶端連接),一個是input.read(bytes) (等待客戶端發送信息),如果客戶端一直不發數據,那麼線程就一直會阻塞在input.read(bytes)。此時在阻塞過程中,意味着這條線程是被這個Socket一直佔用着的,其它的Socket不能進來
想要服務端處理多個客戶端的信息,就需要爲每一個客戶端分配一個線程。下面我們修改一下服務端:
public class BIOServerV2 {
public static void main(String[] args) throws Exception {
//創建一個緩存線程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//創建Socket服務,監聽8000端口
ServerSocket server = new ServerSocket(8001);
System.out.println("服務端啓動!");
while (true) {
//獲取一個套接字(阻塞)
final Socket socket = server.accept();
System.out.println("出現一個新客戶端!");
//在線程池爲新客戶端開一個線程
newCachedThreadPool.execute(() -> handle(socket));
}
}
/**
* 處理數據
*
* @param socket
* @throws IOException
*/
private static void handle(Socket socket) {
try {
byte[] bytes = new byte[1024];
InputStream input = socket.getInputStream();
int read = 0;
while (read != -1) {
//讀取數據(阻塞)
read = input.read(bytes);
System.out.println(new String(bytes, 0, read));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
可以看到我們創建了一個緩存線程池,當服務端新連接了一個客戶端的時候,就創建一個新的線程爲客戶端進行服務
上面的 demo,從服務端代碼中我們可以看到,在傳統的 IO 模型中,每個連接創建成功之後都需要一個線程來維護。因爲目前我們每個客戶端都爲其分配了一個線程去運行,如果有一萬個客戶端進來,我們就要分配一萬個線程給客戶端使用,這樣的資源消耗是十分巨大的。
NIO模型
於是JDK 在 1.4 之後提出了 NIO,在 NIO 模型中,一條連接來了之後,直接把這條連接註冊到 selector 上,然後,通過檢查這個 selector,就可以批量監測出有數據可讀的連接,進而讀取數據
另外IO 讀寫是面向流的,一次性只能從流中讀取一個或者多個字節,並且讀完之後流無法再讀取,你需要自己緩存數據。 而 NIO 的讀寫是面向 Buffer 的,你可以隨意讀取裏面任何一個字節數據,不需要你自己緩存數據,這一切只需要移動讀寫指針即可。
核心代碼
/**
* 採用輪詢的方式監聽selector是否有需要處理的事件,如果有,則進行處理
*
* @throws IOException
*/
public void listen() throws IOException {
System.out.println("服務端啓動成功!");
//輪詢訪問selector
while (true) {
//當註冊的事件到達時,方法返回;否則,該方法會一直阻塞
selector.select();
//獲得selector中選中的項的迭代器,選中的項爲註冊的事件
Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = ite.next();
//刪除已選的key,以防重複處理
ite.remove();
if (key.isAcceptable()) {//客戶端請求連接事件
handlerAccept(key);
} else if (key.isReadable()) {//獲得了可讀的事件
handlerRead(key);
}
}
}
}
對於傳統IO和NIO,網上有一對圖片表達的非常好:
我們的系統就相當於一個餐廳,大門相當於ServerSocket,客人相當於socket客戶端,服務員相當於每個socket客戶端的處理線程。當在多線程的情況下處理客戶端的時候,就相當於餐廳每一個客人都配備了一個專門的服務員,這不管對系統還是餐廳,都是很大的開銷。
而對於NIO:
這裏也是將系統比喻爲一個餐廳,大門相當於serverChannel.socket().bind(new InetSocketAddress(10010)),客人相當於SocketChannel客戶端,服務員相當於線程和selector,只需要一個服務員就可以服務所有的客人了,這對於系統或餐廳來說都是一個低開銷的事情。
下表總結了Java IO和NIO之間的主要區別:
IO | NIO |
---|---|
面向流 | 面向緩衝 |
阻塞IO | 非阻塞IO |
無 | 選擇器 |