Java3種IO模型,一次搞懂!
來源:三分惡
大家好,我是老三,上一節我們討論了Linux的五種IO模型,接下來,我們從Java語言層面,來看看對IO的實現。
在Java中,一共有三種IO模型,分別是阻塞IO(BIO)
、非阻塞IO(NIO)
和非同步IO(AIO)
。
Java BIO
Java BIO
就是Java的傳統IO模型,對應了作業系統IO模型裡的阻塞IO。
Java BIO
相關的實現都位於java.io
包下,其通訊原理是客戶端、服務端之間透過Socket
套接字建立管道連線,然後從管道中獲取對應的輸入/輸出流,最後利用輸入/輸出流物件實現傳送/接收資訊。
我們來看個Demo:
BioServer:
/**
* @Author 三分惡
* @Date 2023/4/30
* @Description BIO服務端
*/
public class BioServer {
public static void main(String[] args) throws IOException {
//定義一個ServerSocket服務端物件,併為其繫結埠號
ServerSocket server = new ServerSocket(8888);
System.out.println("===========BIO服務端啟動================");
//對BIO來講,每個Socket都需要一個Thread
while (true) {
//監聽客戶端Socket連線
Socket socket = server.accept();
new BioServerThread(socket).start();
}
}
/**
* BIO Server執行緒
*/
static class BioServerThread extends Thread{
//socket連線
private Socket socket;
public BioServerThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
//從socket中獲取輸入流
InputStream inputStream=socket.getInputStream();
//轉換為
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
String msg;
//從Buffer中讀取資訊,如果讀取到資訊則輸出
while((msg=bufferedReader.readLine())!=null){
System.out.println("收到客戶端訊息:"+msg);
}
//從socket中獲取輸出流
OutputStream outputStream=socket.getOutputStream();
PrintStream printStream=new PrintStream(outputStream);
//透過輸出流物件向客戶端傳遞資訊
printStream.println("你好,吊毛!");
//清空輸出流
printStream.flush();
//關閉socket
socket.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
BioClient
/**
* @Author 三分惡
* @Date 2023/4/30
* @Description BIO客戶端
*/
public class BioClient {
public static void main(String[] args) throws IOException {
List<String> names= Arrays.asList("帥哥","靚仔","坤坤");
//透過迴圈建立多個多個client
for (String name:names){
//建立socket並根據IP地址與埠連線服務端
Socket socket=new Socket("127.0.0.1",8888);
System.out.println("===========BIO客戶端啟動================");
//從socket中獲取位元組輸出流
OutputStream outputStream=socket.getOutputStream();
//透過輸出流向服務端傳遞資訊
String hello="你好,"+name+"!";
outputStream.write(hello.getBytes());
//清空流,關閉socket輸出
outputStream.flush();
socket.shutdownOutput();
//從socket中獲取位元組輸入流
InputStream inputStream=socket.getInputStream();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
//讀取服務端訊息
String msg;
while((msg=bufferedReader.readLine())!=null){
System.out.println("收到服務端訊息:"+msg);
}
inputStream.close();
outputStream.close();
socket.close();
}
}
}
先啟動 BioServer
,再啟動BioClient
,執行結果
===========BIO服務端啟動================
收到客戶端訊息:你好,帥哥!
收到客戶端訊息:你好,靚仔!
收到客戶端訊息:你好,坤坤!
===========BIO客戶端啟動================
收到服務端訊息:你好,吊毛!
===========BIO客戶端啟動================
收到服務端訊息:你好,吊毛!
===========BIO客戶端啟動================
收到服務端訊息:你好,吊毛!
在上述Java-BIO
的通訊過程中,如果客戶端一直沒有傳送訊息過來,服務端則會一直等待下去,從而服務端陷入阻塞狀態。同理,由於客戶端也一直在等待服務端的訊息,如果服務端一直未響應訊息回來,客戶端也會陷入阻塞狀態。
在BioServer
定義了一個類BioServerThread
,繼承了Thread
類,run
方法裡主要是透過socket和流來讀取客戶端的訊息,以及傳送訊息給客戶端,每處理一個客戶端的Socket連線,就得新建一個執行緒。
同時,IO讀寫操作也是阻塞的,如果客戶端一直沒有傳送訊息過來,執行緒就會進入阻塞狀態,一直等待下去。
在BioClient
裡,迴圈建立Socket
,向服務端收發訊息,客戶端的讀寫也是阻塞的。
在這個Demo裡就體現了BIO的兩個特點:
一個客戶端連線對應一個處理執行緒 讀寫操作都是阻塞的
毫無疑問,不管是建立太多執行緒,還是阻塞讀寫,都會浪費伺服器的資源。
Java NIO
那麼我們就進入Java的下一種IO模型——Java NIO
,它對應作業系統IO模型中的多路複用IO,底層採用了epoll
實現。
Java-NIO
則是JDK1.4
中新引入的API
,它在BIO
功能的基礎上實現了非阻塞式的特性,其所有實現都位於java.nio
包下。NIO
是一種基於通道、面向緩衝區的IO
操作,相較BIO
而言,它能夠更為高效的對資料進行讀寫操作,同時與原先的BIO
使用方式也大有不同。
我們還是先來看個Demo:
NioServer
/**
* @Author 三分惡
* @Date 2023/4/30
* @Description NIO服務端
*/
public class NioServer {
public static void main(String[] args) throws IOException {
//建立一個選擇器selector
Selector selector= Selector.open();
//建立serverSocketChannel
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//繫結埠
serverSocketChannel.socket().bind(new InetSocketAddress(8888));
//必須得設定成非阻塞模式
serverSocketChannel.configureBlocking(false);
//將channel註冊到selector並設定監聽事件為ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("===========NIO服務端啟動============");
while(true){
//超時等待
if(selector.select(1000)==0){
System.out.println("===========NIO服務端超時等待============");
continue;
}
// 有客戶端請求被輪詢監聽到,獲取返回的SelectionKey集合
Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
//迭代器遍歷SelectionKey集合
while (iterator.hasNext()){
SelectionKey key=iterator.next();
// 判斷是否為ACCEPT事件
if (key.isAcceptable()){
// 處理接收請求事件
SocketChannel socketChannel=((ServerSocketChannel) key.channel()).accept();
//非阻塞模式
socketChannel.configureBlocking(false);
// 註冊到Selector並設定監聽事件為READ
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("成功連線客戶端");
}
//判斷是否為READ事件
if (key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
try {
// 獲取以前設定的附件物件,如果沒有則新建一個
ByteBuffer buffer = (ByteBuffer) key.attachment();
if (buffer == null) {
buffer = ByteBuffer.allocate(1024);
key.attach(buffer);
}
// 清空緩衝區
buffer.clear();
// 將通道中的資料讀到緩衝區
int len = socketChannel.read(buffer);
if (len > 0) {
buffer.flip();
String message = new String(buffer.array(), 0, len);
System.out.println("收到客戶端訊息:" + message);
} else if (len < 0) {
// 接收到-1,表示連線已關閉
key.cancel();
socketChannel.close();
continue;
}
// 註冊寫事件,下次向客戶端傳送訊息
socketChannel.register(selector, SelectionKey.OP_WRITE, buffer);
} catch (IOException e) {
// 取消SelectionKey並關閉對應的SocketChannel
key.cancel();
socketChannel.close();
}
}
//判斷是否為WRITE事件
if (key.isWritable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
//獲取buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
String hello = "你好,坤坤!";
//清空buffer
buffer.clear();
//buffer中寫入訊息
buffer.put(hello.getBytes());
buffer.flip();
//向channel中寫入訊息
socketChannel.write(buffer);
buffer.clear();
System.out.println("向客戶端傳送訊息:" + hello);
// 設定下次讀寫操作,向 Selector 進行註冊
socketChannel.register(selector, SelectionKey.OP_READ, buffer);
}
// 移除本次處理的SelectionKey,防止重複處理
iterator.remove();
}
}
}
}
NioClient
public class NioClient {
public static void main(String[] args) throws IOException {
// 建立SocketChannel並指定ip地址和埠號
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
System.out.println("==============NIO客戶端啟動================");
// 非阻塞模式
socketChannel.configureBlocking(false);
String hello="你好,靚仔!";
ByteBuffer buffer = ByteBuffer.wrap(hello.getBytes());
// 向通道中寫入資料
socketChannel.write(buffer);
System.out.println("傳送訊息:" + hello);
buffer.clear();
// 將channel註冊到Selector並監聽READ事件
socketChannel.register(Selector.open(), SelectionKey.OP_READ, buffer);
while (true) {
// 讀取服務端資料
if (socketChannel.read(buffer) > 0) {
buffer.flip();
String msg = new String(buffer.array(), 0, buffer.limit());
System.out.println("收到服務端訊息:" + msg);
break;
}
}
// 關閉輸入流
socketChannel.shutdownInput();
// 關閉SocketChannel連線
socketChannel.close();
}
}
先執行NioServer,再執行NioClient,執行結果:
===========NIO服務端啟動============
===========NIO服務端超時等待============
===========NIO服務端超時等待============
成功連線客戶端
收到客戶端訊息:你好,靚仔!
向客戶端傳送訊息:你好,坤坤!
==============NIO客戶端啟動================
傳送訊息:你好,靚仔!
收到服務端訊息:你好,坤坤!
我們在這個案例裡實現了一個比較簡單的Java NIO 客戶端服務端通訊,裡面有兩個小的點需要注意,註冊到選擇器上的通道都必須要為非阻塞模型,同時透過緩衝區傳輸資料時,必須要呼叫flip()
方法切換為讀取模式。
Java-NIO中有三個核心概念:**Buffer
(緩衝區)、Channel
(通道)、Selector
(選擇器)**。
每個客戶端連連線本質上對應著一個
Channel
通道,每個通道都有自己的Buffer
緩衝區來進行讀寫,這些Channel
被Selector
選擇器管理排程Selector
負責輪詢所有已註冊的Channel
,監聽到有事件發生,才提交給服務端執行緒處理,服務端執行緒不需要做任何阻塞等待,直接在Buffer
裡處理Channel
事件的資料即可,處理完馬上結束,或返回執行緒池供其他客戶端事件繼續使用。透過
Selector
,服務端的一個Thread
就可以處理多個客戶端的請求Buffer(緩衝區)就是飯店用來存放食材的儲藏室,當服務員點餐時,需要從儲藏室中取出食材進行製作。
Channel(通道)是用於傳輸資料的車道,就像飯店裡的上菜視窗,可以快速把點好的菜品送到客人的桌上。
Selector(選擇器)就是大堂經理,負責協調服務員、廚師和客人的配合和溝通,以保證整個就餐過程的效率和順暢。
Java AIO
Java-AIO
也被成為NIO2
,它是在NIO
的基礎上,引入了新的非同步通道的概念,並提供了非同步檔案通道和非同步套接字的實現。
它們的主要區別就在於這個非同步通道,見名知意:使用非同步通道去進行IO
操作時,所有操作都為非同步非阻塞的,當呼叫read()/write()/accept()/connect()
方法時,本質上都會交由作業系統去完成,比如要接收一個客戶端的資料時,作業系統會先將通道中可讀的資料先傳入read()
回撥方法指定的緩衝區中,然後再主動通知Java程式去處理。
我們還是先來看個Demo:
AioServer
/**
* @Author 三分惡
* @Date 2023/5/1
* @Description AIO服務端
*/
public class AioServer {
public static void main(String[] args) throws Exception {
// 建立非同步通道組,處理IO事件
AsynchronousChannelGroup group = AsynchronousChannelGroup.withFixedThreadPool(10, Executors.defaultThreadFactory());
//建立非同步伺服器Socket通道,並繫結埠
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(8888));
System.out.println("=============AIO服務端啟動=========");
// 非同步等待接收客戶端連線
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
// 建立ByteBuffer
final ByteBuffer buffer = ByteBuffer.allocate(1024);
@Override
public void completed(AsynchronousSocketChannel channel, Object attachment) {
System.out.println("客戶端連線成功");
try {
buffer.clear();
// 非同步讀取客戶端傳送的訊息
channel.read(buffer, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer len, Object attachment) {
buffer.flip();
String message = new String(buffer.array(), 0, len);
System.out.println("收到客戶端訊息:" + message);
// 非同步傳送訊息給客戶端
channel.write(ByteBuffer.wrap(("你好,阿坤!").getBytes()), null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
// 關閉輸出流
try {
channel.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
// 繼續非同步等待接收客戶端連線
server.accept(null, this);
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
// 繼續非同步等待接收客戶端連線
server.accept(null, this);
}
});
// 等待所有連線都處理完畢
group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
}
AioClient
/**
* @Author 三分惡
* @Date 2023/5/1
* @Description AIO客戶端
*/
public class AioClient {
public static void main(String[] args) throws Exception {
// 建立非同步Socket通道
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
// 非同步連線伺服器
client.connect(new InetSocketAddress("127.0.0.1", 8888), null, new CompletionHandler<Void, Object>() {
// 建立ByteBuffer
final ByteBuffer buffer = ByteBuffer.wrap(("你好,靚仔!").getBytes());
@Override
public void completed(Void result, Object attachment) {
// 非同步傳送訊息給伺服器
client.write(buffer, null, new CompletionHandler<Integer, Object>() {
// 建立ByteBuffer
final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
@Override
public void completed(Integer result, Object attachment) {
readBuffer.clear();
// 非同步讀取伺服器傳送的訊息
client.read(readBuffer, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
readBuffer.flip();
String msg = new String(readBuffer.array(), 0, result);
System.out.println("收到服務端訊息:" + msg);
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
// 等待連線處理完畢
Thread.sleep(1000);
// 關閉輸入流和Socket通道
client.shutdownInput();
client.close();
}
}
看下執行結果
=============AIO服務端啟動=========
客戶端連線成功
收到客戶端訊息:你好,靚仔!
收到服務端訊息:你好,阿坤!
可以看到,所有的操作都是非同步進行,透過completed
接收非同步回撥,透過failed
接收錯誤回撥。
而且我們發現,相較於之前的NIO
而言,AIO
其中少了Selector
選擇器這個核心元件,選擇器在NIO
中充當了協調者的角色。
但在Java-AIO
中,類似的角色直接由作業系統擔當,而且不是採用輪詢的方式監聽IO
事件,而是採用一種類似於“訂閱-通知”的模式。
在AIO
中,所有建立的通道都會直接在OS
上註冊監聽,當出現IO
請求時,會先由作業系統接收、準備、複製好資料,然後再通知監聽對應通道的程式處理資料。
Java-AIO
這種非同步非阻塞式IO
也是由作業系統進行支援的,在Windows
系統中提供了一種非同步IO
技術:IOCP(I/O Completion Port
,所以Windows
下的Java-AIO
則是依賴於這種機制實現。不過在Linux
系統中由於沒有這種非同步IO
技術,所以Java-AIO
在Linux
環境中使用的還是epoll
這種多路複用技術進行模擬實現的。
因為Linux的非同步IO技術實際上不太成熟,所以Java-AIO
的實際應用並不是太多,比如大名鼎鼎的網路通訊框架Netty
就沒有采用Java-AIO,而是使用Java-NIO,在程式碼層面,自行實現非同步。
小結
那麼這期我們就快速過了一下Java的三種IO機制,它們的特點,我們直接看下圖:
我們也發現,雖然Java-NIO
、Java-AIO
,在效能上比Java-BIO
要強很多,但是可以看到,寫法上一個比一個難搞,不過好在基本也沒人直接用Java-NIO
、Java-AIO
,如果要進行網路通訊,一般都會採用Netty
,它對原生的Java-NIO
進行了封裝最佳化,接下來,我們會繼續走近Netty
,敬請期待。
參考:
[1].《Netty權威指南》
[2].
[3].
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2951113/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 一頓飯的事兒,搞懂Linux5種IO模型Linux模型
- (四)五種IO模型模型
- 【IO】Linux下的五種IO模型Linux模型
- 圖解四種 IO 模型圖解模型
- 五種網路io模型模型
- 五種傳統IO模型模型
- 【OS】5種網路IO模型模型
- 5種IO模型和5種通訊模式模型模式
- Linux 下的五種 IO 模型Linux模型
- 今天我們來聊Java IO模型,BIO、NIO、AIO三種常見IO模型Java模型AI
- 五種IO模型介紹和對比模型
- java3Java
- 【網路IO系列】IO的五種模型,BIO、NIO、AIO、IO多路複用、 訊號驅動IO模型AI
- IO模型模型
- IO模型學習(一)IO模型分類模型
- 用通俗易懂的方式講IO的五種模型模型
- Linux IO模型Linux模型
- io模型 WSAAsyncSelect模型
- RPC設計應該使用哪種網路IO模型?RPC模型
- 這一次搞懂SpringMVC原理SpringMVC
- 最後一次搞懂 Event LoopOOP
- 一次搞懂WCF 配置檔案
- 徹底搞懂徹底搞懂事件驅動模型 - Reactor事件模型React
- IO通訊模型(三)多路複用IO模型
- 【NIO系列】——之IO模型模型
- linux的IO模型Linux模型
- Java 網路 IO 模型Java模型
- NIO(二)淺析IO模型模型
- 如何給女朋友解釋什麼是Linux的五種IO模型?Linux模型
- 這一次,徹底搞懂 Go CondGo
- 一次搞懂MySQL(所有)索引及其區別MySql索引
- 高效能IO模型淺析模型
- WinSock 重疊IO模型模型
- 網路IO模型-非同步選擇模型(Delphi版)模型非同步
- 一次性搞懂JavaScript 執行機制JavaScript
- 一次ORACLE IO效能診斷案例Oracle
- IO通訊模型(二)同步非阻塞模式NIO(NonBlocking IO)模型模式BloC
- 漫話:如何給女朋友解釋什麼是Linux的五種IO模型?Linux模型