nio的知識以操作系統,網絡原理,java的bio,socket,線程爲基礎,基礎知識的掌握程度決定了對nio,netty的掌握程度。
阻塞io
簡化後的bio示例代碼,客戶端向服務器發送字符串:QUERY TIME ORDER
,服務器收到確認字符串後返回當前時間。
- 服務器主線程負責等待接受請求,當收到一個請求後,開啓一個線程去處理。
- 使用jdk 1.7的
try-with-resources Statement
來消除樣板代碼,即關閉資源相關代碼。 - 服務器使用線程池來處理請求,避免耗盡系統資源。
- 客戶端的
in.readLine();
是阻塞的,直到服務器返回數據後,輸出響應內容。 - 服務器每次處理完一個請求,關閉socket連接。
public class TimeClient {
public static void main(String[] args) {
try (Socket socket = new Socket("127.0.0.1", 8080);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
out.println("QUERY TIME ORDER");
String resp = in.readLine();
System.out.println("Now is : " + resp);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class TimeServer {
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100), r -> {
Thread thread = new Thread(r);
thread.setName("TimeServer thread pool");
return thread;
});
public static void main(String[] args) {
int port = 8080;
try (ServerSocket server = new ServerSocket(port)) {
System.out.println("The time server is start in port : " + port);
while (true) {
Socket socket = server.accept();
threadPool.submit(() -> {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
String command = in.readLine();
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(command) ? new Date().toString() : "BAD ORDER";
out.println(currentTime);
} catch (IOException e) {
e.printStackTrace();
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
之所以是阻塞的,是因爲:
- ServerSocket 上的 accept()方法將會一直阻塞到一個連接建立 ,隨後返回一個 新的Socket 用於客戶端和服務器之間的通信。該 ServerSocket 將繼續監聽傳入的 連接。
- readLine()方法將會阻塞,直到一個由換行符或者回車符結尾的字符串被讀取。
非阻塞io
首先不管是bio還是nio,都是大量的樣板代碼,基本都是固定的寫法。
nio的核心概念:Channel,Buffer,Selector。之所以是非阻塞的,是藉助了操作系統的能力,它使用了事件通知 API以確定在一組非阻塞套接字中有哪些已經就緒能夠進行 I/O 相關的操作。因爲可以在任何的時間檢查任意的讀操作或者寫操作的完成狀態,一個單一的線程便可以處理多個併發的連接。nio的核心是selector,一個selecter可以管理多個channel。nio是一組全新的api,與bio的對應關係應當如下:
- Socket--->ServerSocket,SocketChannel-->ServerSocketChannel
- xxxStream-->Buffer
- 線程池-->Selector註冊,輪詢
服務器端是等待連接,接受連接,客戶端是主動發起連接。服務端開啓ServerSocketChannel
,開啓Selector
,並將channel註冊到select,並且要訂閱要關注的事件,事件包括:接受連接,已連接,可讀,可寫。
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);
selector = Selector.open();
servChannel.register(selector, SelectionKey.OP_ACCEPT);
然後就是死循環輪詢,是否有事件發生。
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
通過監聽Accept事件,建立連接後產生的SocketChannel對象也註冊到selector上。
if (key.isAcceptable()) {
// Accept the new connection
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// Add the new connection to the selector
sc.register(selector, SelectionKey.OP_READ);
}
根據讀寫事件來處理每一條連接建立後的通信,使用Buffer的子類讀寫數據。
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
nio避免創建多個線程,線程間切換的開銷,藉助操作系統異步通知機制,沒有 I/O 操作需要處理的時候,線程也可以被用於其他任務。
參考文獻:
- 《netty權威指南》及源碼
- 《netty實戰》