你好,我是彤哥,本篇是netty系列的第三篇。
簡介
上一章我們介紹了IO的五種模型,實際上Java只支持其中的三種,即BIO/NIO/AIO。
本文將介紹Java中這三種IO的進化史,並從使用的角度剖析它們背后的故事。
Java BIO
BIO概念解析
BIO,Blocking IO,阻塞IO,它是Java的上古產品,自出生就有的東西(JDK 1.0)。
使用BIO則數據准備和數據從內核空間拷貝到用戶空間兩個階段都是阻塞的。
BIO使用案例
public class EchoServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
System.out.println("start accept");
Socket socket = serverSocket.accept();
System.out.println("new conn: " + socket.getRemoteSocketAddress());
new Thread(()->{
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
// 讀取消息,本文來源公從號彤哥讀源碼
while ((msg = reader.readLine()) != null) {
if (msg.equalsIgnoreCase("quit")) {
reader.close();
socket.close();
break;
} else {
System.out.println("receive msg: " + msg);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
客戶端可以使用telnet來測試,而且你可以使用多個telnet來測試:
[c:\~]$ telnet 127.0.0.1 8080
Connecting to 127.0.0.1:8080...
Connection established.
To escape to local shell, press 'Ctrl+Alt+]'.
hello world
我是人才
quit
Connection closed by foreign host.
BIO的使用方式非常簡單,服務端接收到一個連接就啟動一個線程來處理這個連接的所有請求。
所以,BIO最大的缺點就是浪費資源,只能處理少量的連接,線程數隨着連接數線性增加,連接越多線程越多,直到抗不住。
Java NIO
NIO概念解析
NIO,New IO,JDK1.4開始支持,內部是基於多路復用的IO模型。
這里有個歧義,很多人認為Java的NIO是Non-Blocking IO的縮寫,其實並不是。
使用NIO則多條連接的數據准備階段會阻塞在select上,數據從內核空間拷貝到用戶空間依然是阻塞的。
因為第一階段並不是連接本身處於阻塞階段,所以通常來說NIO也可以看作是同步非阻塞IO。
NIO使用案例
public class EchoServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
// 將accept事件綁定到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞在select上
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍歷selectKeys
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果是accept事件
if (selectionKey.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 如果是讀取事件,本文來源公從號彤哥讀源碼
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 將數據讀入到buffer中
int length = socketChannel.read(buffer);
if (length > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 將數據讀入到byte數組中
buffer.get(bytes);
// 換行符會跟着消息一起傳過來
String content = new String(bytes, "UTF-8").replace("\r\n", "");
if (content.equalsIgnoreCase("quit")) {
selectionKey.cancel();
socketChannel.close();
} else {
System.out.println("receive msg: " + content);
}
}
}
iterator.remove();
}
}
}
}
這里同樣使用telnet測試,而且你可以使用多個telnet來測試:
[c:\~]$ telnet 127.0.0.1 8080
Connecting to 127.0.0.1:8080...
Connection established.
To escape to local shell, press 'Ctrl+Alt+]'.
hello world
我是人才
quit
Connection closed by foreign host.
NIO的使用方式就有點復雜了,但是一個線程就可以處理很多連接。
首先,需要注冊一個ServerSocketChannel並把它注冊到selector上並監聽accept事件,然后accept到連接后會獲取到SocketChannel,同樣把SocketChannel也注冊到selector上,但是監聽的是read事件。
NIO最大的優點,就是一個線程就可以處理大量的連接,缺點是不適合處理阻塞性任務,因為阻塞性任務會把這個線程占有着,其它連接的請求將得不到及時處理。
Java AIO
AIO概念介紹
AIO,Asynchronous IO,異步IO,JDK1.7開始支持,算是一種比較完美的IO,Windows下比較成熟,但Linux下還不太成熟。
使用異步IO則會在請求時立即返回,並在數據已准備且已拷貝到用戶空間后進行回調處理,兩個階段都不會阻塞。
AIO使用案例
public class EchoServer {
public static void main(String[] args) throws IOException {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
// 監聽accept事件,本文來源公從號彤哥讀源碼
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try {
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
// 再次監聽accept事件
serverSocketChannel.accept(null, this);
// 消息的處理
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 將數據讀入到buffer中
Future<Integer> future = socketChannel.read(buffer);
if (future.get() > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 將數據讀入到byte數組中
buffer.get(bytes);
String content = new String(bytes, "UTF-8");
// 換行符會當成另一條消息傳過來
if (content.equals("\r\n")) {
continue;
}
if (content.equalsIgnoreCase("quit")) {
socketChannel.close();
break;
} else {
System.out.println("receive msg: " + content);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("failed");
}
});
// 阻塞住主線程
System.in.read();
}
}
這里同樣使用telnet測試,而且你可以使用多個telnet來測試:
[c:\~]$ telnet 127.0.0.1 8080
Connecting to 127.0.0.1:8080...
Connection established.
To escape to local shell, press 'Ctrl+Alt+]'.
hello world
我是人才
quit
Connection closed by foreign host.
AIO的使用方式不算太復雜,默認會啟一組線程來處理用戶的請求,而且如果在處理阻塞性任務,還會自動增加新的線程來處理其它連接的任務。
首先,創建一個AsynchronousServerSocketChannel並調用其accept方法,這一步相當於監聽了accept事件,在收到accept事件后會獲取到AsynchronousSocketChannel,然后就可以在回調方法completed()里面讀取數據了,當然也要繼續監聽accept事件。
AIO最大的優點,就是少量的線程就可以處理大量的連接,而且可以處理阻塞性任務,但不能大量阻塞,否則線程數量會膨脹。
槽點
(1)三種IO的實現方式中對於換行符的處理竟然都不一樣,BIO中不會把換行符帶過來(其實是帶過來了,因為用了readLine()方法,所以換行符沒了),NIO中會把換行符加在消息末尾,AIO中會把換行符當成一條新的消息傳過來,很神奇,為啥不統一處理呢,也很疑惑。
(2)JDK自帶的ByteBuffer是一個難用的東西。
總結
本文我們從概念和使用兩個角度分別介紹了BIO/NIO/AIO三種IO模型。
問題
看起來JDK的實現似乎很完美啊,為什么還會有Netty呢?
最后,也歡迎來我的公從號彤哥讀源碼系統地學習源碼&架構的知識。