一、簡介
在計算機中,IO 傳輸數據有三種工作方式,分別是: BIO、NIO、AIO 。
在講解 BIO、NIO、AIO 之前,我們先來回顧一下這幾個概念: 同步與異步,阻塞與非阻塞 。
同步與異步的區別
- 同步就是發起一個請求后,接受者未處理完請求之前,不返回結果。
- 異步就是發起一個請求后,立刻得到接受者的回應表示已接收到請求,但是接受者并沒有處理完,接受者通常依靠事件回調等機制來通知請求者其處理結果。
阻塞和非阻塞的區別
- 阻塞就是請求者發起一個請求,一直等待其請求結果返回,也就是當前線程會被掛起,無法從事其他任務,只有當條件就緒才能繼續。
- 非阻塞就是請求者發起一個請求,不用一直等著結果返回,可以先去干其他事情,當條件就緒的時候,就自動回來。
而我們要講的 BIO、NIO、AIO 就是同步與異步、阻塞與非阻塞的組合。
- BIO:同步阻塞 IO;
- NIO:同步非阻塞 IO;
- AIO:異步非阻塞 IO;
不同的工作方式,帶來的傳輸效率是不一樣的,下面我們以網絡 IO 為例,一起看看不同的工作方式下,彼此之間有何不同 。
二、BIO
BIO 俗稱同步阻塞 IO,是一種非常傳統的 IO 模型,也是最常用的網絡數據傳輸處理方式,優點就是編程簡單,但是缺點也很明顯,I/O 傳輸性能一般比較差,CPU 大部分處于空閑狀態。
采用 BIO 通信模型的服務端,通常由一個獨立的 Acceptor 線程負責監聽所有客戶端的連接,當服務端接受到多個客戶端的請求時,所有的客戶端只能排隊等待服務端一個一個的處理。
BIO 通信模型圖如下!
一般在服務端通過while(true)
循環中會調用accept()
方法監聽客戶端的連接,一旦接收到一個連接請求,就可以建立通信套接字進行讀寫操作,此時不能再接收其他客戶端連接請求,只能等待同當前連接的客戶端的操作執行完成。
服務端操作,樣例程序如下 :
public class BioServerTest {
public static void main(String[] args) throws IOException {
//初始化服務端socket并且綁定 8080 端口
ServerSocket serverSocket = new ServerSocket(8080);
//循環監聽客戶端請求
while (true){
try {
//監聽客戶端請求
Socket socket = serverSocket.accept();
//將字節流轉化成字符流,讀取客戶端輸入的內容
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//讀取一行數據
String str = bufferedReader.readLine();
//打印客戶端發送的信息
System.out.println("服務端收到客戶端發送的信息:" + str);
//向客戶端返回信息,將字符轉化成字節流,并輸出
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()),true);
printWriter.println("hello,我是服務端,已收到消息");
// 關閉流
bufferedReader.close();
printWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客戶端操作,樣例程序如下 :
public class BioClientTest {
public static void main(String[] args) {
//創建10個線程,模擬10個客戶端,同時向服務端發送請求
for (int i = 0; i < 10; i++) {
final int j = i;//定義變量
new Thread(new Runnable() {
@Override
public void run() {
try {
//通過IP和端口與服務端建立連接
Socket socket =new Socket("127.0.0.1",8080);
//將字符流轉化成字節流,并輸出
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()),true);
String str="Hello,我是" + j + "個,客戶端!";
printWriter.println(str);
//從輸入流中讀取服務端返回的信息,將字節流轉化成字符流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//讀取內容
String result = bufferedReader.readLine();
//打印服務端返回的信息
System.out.println("客戶端發送請求內容:" + str + " - > 收到服務端返回的內容:" + result);
// 關閉流
bufferedReader.close();
printWriter.close();
// 關閉socket
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
最后,依次啟動服務端、客戶端,看看控制臺輸出情況如何。
服務端控制臺結果如下:
服務端收到客戶端發送的信息:Hello,我是8個,客戶端!
服務端收到客戶端發送的信息:Hello,我是9個,客戶端!
服務端收到客戶端發送的信息:Hello,我是7個,客戶端!
服務端收到客戶端發送的信息:Hello,我是5個,客戶端!
服務端收到客戶端發送的信息:Hello,我是4個,客戶端!
服務端收到客戶端發送的信息:Hello,我是3個,客戶端!
服務端收到客戶端發送的信息:Hello,我是6個,客戶端!
服務端收到客戶端發送的信息:Hello,我是2個,客戶端!
服務端收到客戶端發送的信息:Hello,我是1個,客戶端!
服務端收到客戶端發送的信息:Hello,我是0個,客戶端!
客戶端控制臺結果如下:
客戶端發送請求內容:Hello,我是8個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
客戶端發送請求內容:Hello,我是9個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
客戶端發送請求內容:Hello,我是7個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
客戶端發送請求內容:Hello,我是5個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
客戶端發送請求內容:Hello,我是4個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
客戶端發送請求內容:Hello,我是3個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
客戶端發送請求內容:Hello,我是6個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
客戶端發送請求內容:Hello,我是2個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
客戶端發送請求內容:Hello,我是1個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
客戶端發送請求內容:Hello,我是0個,客戶端! - > 收到服務端返回的內容:hello,我是服務端,已收到消息
隨著客戶端的請求次數越來越多,可能需要排隊的時間會越來越長,因此是否可以在服務端,采用多線程編程進行處理呢?
答案是,可以的!
下面我們對服務端的代碼進行改造,服務端多線程操作,樣例程序如下:
public class BioServerTest {
public static void main(String[] args) throws IOException {
//初始化服務端socket并且綁定 8080 端口
ServerSocket serverSocket = new ServerSocket(8080);
//循環監聽客戶端請求
while (true){
//監聽客戶端請求
Socket socket = serverSocket.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
String threadName = Thread.currentThread().toString();
//將字節流轉化成字符流,讀取客戶端輸入的內容
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//讀取一行數據
String str = bufferedReader.readLine();
//打印客戶端發送的信息
System.out.println("線程名稱" + threadName + ",服務端收到客戶端發送的信息:" + str);
//向客戶端返回信息,將字符轉化成字節流,并輸出
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()),true);
printWriter.println("hello,我是服務端,已收到消息");
// 關閉流
bufferedReader.close();
printWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
依次啟動服務端、客戶端,服務端控制臺輸出結果如下:
線程名稱Thread[Thread-8,5,main],服務端收到客戶端發送的信息:Hello,我是4個,客戶端!
線程名稱Thread[Thread-4,5,main],服務端收到客戶端發送的信息:Hello,我是8個,客戶端!
線程名稱Thread[Thread-0,5,main],服務端收到客戶端發送的信息:Hello,我是1個,客戶端!
線程名稱Thread[Thread-7,5,main],服務端收到客戶端發送的信息:Hello,我是5個,客戶端!
線程名稱Thread[Thread-5,5,main],服務端收到客戶端發送的信息:Hello,我是2個,客戶端!
線程名稱Thread[Thread-9,5,main],服務端收到客戶端發送的信息:Hello,我是3個,客戶端!
線程名稱Thread[Thread-1,5,main],服務端收到客戶端發送的信息:Hello,我是0個,客戶端!
線程名稱Thread[Thread-3,5,main],服務端收到客戶端發送的信息:Hello,我是7個,客戶端!
線程名稱Thread[Thread-2,5,main],服務端收到客戶端發送的信息:Hello,我是9個,客戶端!
線程名稱Thread[Thread-6,5,main],服務端收到客戶端發送的信息:Hello,我是6個,客戶端!
當服務端接收到客戶端的請求時,會給每個客戶端創建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,最后線程會銷毀。
但是這樣的編程模型也有很大的弊端,如果出現 100、1000、甚至 10000 個客戶端同時請求服務端,采用這種編程模型,服務端也會創建與之相同的線程數量, 線程數急劇膨脹可能會導致線程堆棧溢出、創建新線程失敗等問題,最終可能導致服務端宕機或者僵死,不能對外提供服務 。
三、偽異步 BIO
為了解決上面提到的同步阻塞 I/O 面臨的一個鏈路需要一個線程處理的問題,后來有人對它的編程模型進行了優化。
在服務端通過使用 Java 中ThreadPoolExecutor
線程池機制來處理多個客戶端的請求接入,防止由于海量并發接入導致資源耗盡,讓線程的創建和回收成本相對較低,保證了系統有限的資源得以控制,實現了 N (客戶端請求數量)大于 M (服務端處理客戶端請求的線程數量)的偽異步 I/O 模型。
偽異步 IO 模型圖,如下圖:
采用線程池和任務隊列可以實現一種叫做偽異步的 I/O 通信框架,當有新的客戶端接入時,將客戶端的 Socket 封裝成一個 Task 投遞到線程池中進行處理。
服務端采用線程池處理客戶端請求,樣例程序如下:
public class BioServerTest {
public static void main(String[] args) throws IOException {
//在線程池中創建5個固定大小線程,來處理客戶端的請求
ExecutorService executorService = Executors.newFixedThreadPool(5);
//初始化服務端socket并且綁定 8080 端口
ServerSocket serverSocket = new ServerSocket(8080);
//循環監聽客戶端請求
while (true){
//監聽客戶端請求
Socket socket = serverSocket.accept();
//使用線程池執行任務
executorService.execute(new Runnable() {
@Override
public void run() {
try {
String threadName = Thread.currentThread().toString();
//將字節流轉化成字符流,讀取客戶端輸入的內容
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//讀取一行數據
String str = bufferedReader.readLine();
//打印客戶端發送的信息
System.out.println("線程名稱" + threadName + ",服務端收到客戶端發送的信息:" + str);
//向客戶端返回信息,將字符轉化成字節流,并輸出
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()),true);
printWriter.println("hello,我是服務端,已收到消息");
// 關閉流
bufferedReader.close();
printWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
}
依次啟動服務端、客戶端,服務端控制臺輸出結果如下:
線程名稱Thread[pool-1-thread-4,5,main],服務端收到客戶端發送的信息:Hello,我是6個,客戶端!
線程名稱Thread[pool-1-thread-2,5,main],服務端收到客戶端發送的信息:Hello,我是8個,客戶端!
線程名稱Thread[pool-1-thread-3,5,main],服務端收到客戶端發送的信息:Hello,我是9個,客戶端!
線程名稱Thread[pool-1-thread-5,5,main],服務端收到客戶端發送的信息:Hello,我是5個,客戶端!
線程名稱Thread[pool-1-thread-1,5,main],服務端收到客戶端發送的信息:Hello,我是7個,客戶端!
線程名稱Thread[pool-1-thread-5,5,main],服務端收到客戶端發送的信息:Hello,我是2個,客戶端!
線程名稱Thread[pool-1-thread-5,5,main],服務端收到客戶端發送的信息:Hello,我是0個,客戶端!
線程名稱Thread[pool-1-thread-1,5,main],服務端收到客戶端發送的信息:Hello,我是1個,客戶端!
線程名稱Thread[pool-1-thread-5,5,main],服務端收到客戶端發送的信息:Hello,我是3個,客戶端!
線程名稱Thread[pool-1-thread-1,5,main],服務端收到客戶端發送的信息:Hello,我是4個,客戶端!
本例中測試的客戶端數量是 10,服務端使用 java 線程池來處理任務,線程數量為 5 個,服務端不用為每個客戶端都創建一個線程,由于線程池可以設置消息隊列的大小和最大線程數,因此它的資源占用是可控的,無論多少個客戶端并發訪問,都不會導致資源的耗盡和宕機。
在活動連接數不是特別高的情況下,這種模型還是不錯的,可以讓每一個連接專注于自己的 I/O 并且編程模型簡單,也不用過多考慮系統的過載、限流等問題。
但是,它的底層仍然是同步阻塞的 BIO 模型,當面對十萬甚至百萬級請求接入的時候,傳統的 BIO 模型無能為力,因此我們需要一種更高效的 I/O 處理模型來應對更高的并發量。
四、NIO
NIO,英文全稱: Non-blocking-IO ,一種同步非阻塞的 I/O 模型。
在 Java 1.4 中引入,對應的代碼在java.nio
包下。
與傳統的 IO 不同,NIO 新增了 Channel、Selector、Buffer 等抽象概念, 支持面向緩沖、基于通道的 I/O 數據傳輸方法 。
NIO 模型圖,如下圖:
與此同時,NIO 還提供了與傳統 BIO 模型中的 Socket
和 ServerSocket
相對應的 SocketChannel
和 ServerSocketChannel
兩種不同的套接字通道實現。
NIO 這兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統中的 BIO 一樣,比較簡單,但是性能和可靠性都不好; 非阻塞模式正好與之相反 。
對于低負載、低并發的應用程序,可以使用同步阻塞 I/O 來提升開發效率和更好的維護性;對于高負載、高并發的( 網絡 )應用,使用 NIO 的非阻塞模式來開發可以顯著的提升數據傳輸效率。
在介紹樣例之前,我們先看一下 NIO 涉及到的核心關聯類圖,如下:
上圖中有三個關鍵類: Channel 、Selector 和 Buffer ,它們是 NIO 中的核心概念。
- Channel:可以理解為通道;
- Selector:可以理解為選擇器;
- Buffer:可以理解為數據緩沖區;
從名詞上看感覺很抽象,我們還是用之前介紹的城市交通工具來繼續形容 NIO 的工作方式,這里的 Channel 要比 Socket 更加具體,它可以比作為某種具體的交通工具,如汽車或是高鐵、飛機等,而 Selector 可以比作為一個車站的車輛運行調度系統,它將負責監控每輛車的當前運行狀態,是已經出站還是在路上等等,也就是說它可以輪詢每個 Channel 的狀態。
還有一個 Buffer 類,你可以將它看作為 IO 中 Stream ,但是它比 IO 中的 Stream 更加具體化,我們可以將它比作為車上的座位,Channel 如果是汽車的話,那么 Buffer 就是汽車上的座位,Channel 如果是高鐵上,那么 Buffer 就是高鐵上的座位,它始終是一個具體的概念,這一點與 Stream 不同。
Socket 中的 Stream 只能代表是一個座位,至于是什么座位由你自己去想象,也就是說你在上車之前并不知道這個車上是否還有座位,也不知道上的是什么車,因為你并不能選擇,這些信息都已經被封裝在了運輸工具( Socket )里面了。
NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 傳輸過程中涉及到的 信息具體化 ,讓程序員有機會去控制它們。
當我們進行傳統的網絡 IO 操作時,比如調用write()
往 Socket 中的SendQ
隊列寫數據時,當一次寫的數據超過SendQ
長度時,操作系統會按照SendQ
的長度進行分割的,這個過程中需要將用戶空間數據和內核地址空間進行切換,而這個切換不是程序員可以控制的,由底層操作系統來幫我們處理。
而在Buffer
中,我們可以控制Buffer
的capacity
(容量),并且是否擴容以及如何擴容都可以控制。
理解了這些概念后我們看一下,實際上它們是如何工作的呢?
我們一起來看看代碼實例!
服務端操作,樣例程序如下 :
/**
* NIO 服務端
*/
public class NioServerTest {
public static void main(String[] args) throws IOException {
// 打開服務器套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 服務器配置為非阻塞
ssc.configureBlocking(false);
// 進行服務的綁定,監聽8080端口
ssc.socket().bind(new InetSocketAddress(8080));
// 構建一個Selector選擇器,并且將channel注冊上去
Selector selector = Selector.open();
// 將serverSocketChannel注冊到selector,并對accept事件感興趣(serverSocketChannel只能支持accept操作)
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true){
// 查詢指定事件已經就緒的通道數量,select方法有阻塞效果,直到有事件通知才會有返回,如果為0就跳過
int readyChannels = selector.select();
if(readyChannels == 0) {
continue;
};
//通過選擇器取得所有key集合
Set< SelectionKey > selectedKeys = selector.selectedKeys();
Iterator< SelectionKey > iterator = selectedKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//判斷狀態是否有效
if (!key.isValid()) {
continue;
}
if (key.isAcceptable()) {
// 處理通道中的連接事件
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel sc = server.accept();
sc.configureBlocking(false);
System.out.println("接收到新的客戶端連接,地址:" + sc.getRemoteAddress());
// 將通道注冊到選擇器并處理通道中可讀事件
sc.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 處理通道中的可讀事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (channel.isOpen() && channel.read(byteBuffer) != -1) {
// 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認為請求結束了)
if (byteBuffer.position() > 0) {
break;
};
}
byteBuffer.flip();
//獲取緩沖中的數據
String result = new String(byteBuffer.array(), 0, byteBuffer.limit());
System.out.println("收到客戶端發送的信息,內容:" + result);
// 將通道注冊到選擇器并處理通道中可寫事件
channel.register(selector, SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
// 處理通道中的可寫事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("server send".getBytes());
byteBuffer.flip();
channel.write(byteBuffer);
// 將通道注冊到選擇器并處理通道中可讀事件
channel.register(selector, SelectionKey.OP_READ);
//寫完之后關閉通道
channel.close();
}
//當前事件已經處理完畢,可以丟棄
iterator.remove();
}
}
}
}
客戶端操作,樣例程序如下 :
/**
* NIO 客戶端
*/
public class NioClientTest {
public static void main(String[] args) throws IOException {
// 打開socket通道
SocketChannel sc = SocketChannel.open();
//設置為非阻塞
sc.configureBlocking(false);
//連接服務器地址和端口
sc.connect(new InetSocketAddress("127.0.0.1", 8080));
while (!sc.finishConnect()) {
// 沒連接上,則一直等待
System.out.println("客戶端正在連接中,請耐心等待");
}
// 發送內容
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put("Hello,我是客戶端".getBytes());
writeBuffer.flip();
sc.write(writeBuffer);
// 讀取響應
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
while (sc.isOpen() && sc.read(readBuffer) != -1) {
// 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認為請求結束了)
if (readBuffer.position() > 0) {
break;
};
}
readBuffer.flip();
String result = new String(readBuffer.array(), 0, readBuffer.limit());
System.out.println("客戶端收到服務端:" + sc.socket().getRemoteSocketAddress() + ",返回的信息:" + result);
// 關閉通道
sc.close();
}
}
最后,依次啟動服務端、客戶端,看看控制臺輸出情況如何。
服務端控制臺結果如下:
接收到新的客戶端連接,地址:/127.0.0.1:57644
收到客戶端發送的信息,內容:Hello,我是客戶端
客戶端控制臺結果如下:
客戶端收到服務端:/127.0.0.1:8080,返回的信息:server send
從編程上可以看到,NIO 的操作比傳統的 IO 操作要復雜的多 !
Selector 被稱為選擇器 ,當然你也可以翻譯為多路復用器 。它是Java NIO 核心組件中的一個,用于檢查一個或多個 Channel (通道)的狀態是否處于 連接就緒 、 接受就緒 、 可讀就緒 、 可寫就緒 。
如此可以實現單線程管理多個 channels 的目的,也就是可以管理多個網絡連接。
使用 Selector 的好處在于 :相比傳統方式使用多個線程來管理 IO,Selector 使用了更少的線程就可以處理通道了,并且實現網絡高效傳輸!
雖然 Java 中的 nio 傳輸比較快,為什么大家都不愿意用 JDK 原生 NIO 進行開發呢?
從上面的代碼中大家都可以看出來,除了編程復雜之外,還有幾個讓人詬病的問題:
- JDK 的 NIO 底層由 epoll 實現,該實現飽受詬病的空輪詢 bug 會導致 cpu 飆升 100%!
- 項目龐大之后,自行實現的 NIO 很容易出現各類 bug,維護成本較高!
但是,Netty 框架的出現,很大程度上改善了 JDK 原生 NIO 所存在的一些讓人難以忍受的問題 ,關于 Netty 框架應用,會在后期的文章里進行介紹。
五、AIO
最后就是 AIO 了,全稱 Asynchronous I/O,可以理解為異步 IO,也被稱為 NIO 2,在 Java 7 中引入,它是異步非阻塞的 IO 模型。
異步 IO 是基于事件回調機制實現的,也就是應用操作之后會直接返回,不會堵塞在那里,當后臺處理完成,操作系統會通知相應的線程進行后續的操作。
具體的實例如下!
服務端操作,樣例程序如下 :
/**
* aio 服務端
*/
public class AioServer {
public AsynchronousServerSocketChannel serverChannel;
/**
* 監聽客戶端請求
* @throws Exception
*/
public void listen() throws Exception {
//打開一個服務端通道
serverChannel = AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));//監聽8080端口
//服務監聽
serverChannel.accept(this, new CompletionHandler< AsynchronousSocketChannel,AioServer >(){
@Override
public void completed(AsynchronousSocketChannel client, AioServer attachment) {
try {
if (client.isOpen()) {
System.out.println("接收到新的客戶端連接,地址:" + client.getRemoteAddress());
final ByteBuffer buffer = ByteBuffer.allocate(1024);
//讀取客戶端發送的信息
client.read(buffer, client, new CompletionHandler< Integer, AsynchronousSocketChannel >(){
@Override
public void completed(Integer result, AsynchronousSocketChannel attachment) {
try {
//讀取請求,處理客戶端發送的數據
buffer.flip();
String content = new String(buffer.array(), 0, buffer.limit());
System.out.println("服務端收到客戶端發送的信息:" + content);
//向客戶端發送數據
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put("server send".getBytes());
writeBuffer.flip();
attachment.write(writeBuffer).get();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
try {
exc.printStackTrace();
attachment.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//當有新客戶端接入的時候,直接調用accept方法,遞歸執行下去,保證多個客戶端都可以阻塞
attachment.serverChannel.accept(attachment, this);
}
}
@Override
public void failed(Throwable exc, AioServer attachment) {
exc.printStackTrace();
}
});
}
public static void main(String[] args) throws Exception {
//啟動服務器,并監聽客戶端
new AioServer().listen();
//因為是異步IO執行,讓主線程睡眠但不關閉
Thread.sleep(Integer.MAX_VALUE);
}
}
客戶端操作,樣例程序如下 :
/**
* aio 客戶端
*/
public class AioClient {
public static void main(String[] args) throws IOException, InterruptedException {
//打開一個客戶端通道
AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
//與服務器建立連接
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
//睡眠1s,等待與服務器建立連接
Thread.sleep(1000);
try {
//向服務器發送數據
channel.write(ByteBuffer.wrap("Hello,我是客戶端".getBytes())).get();
} catch (Exception e) {
e.printStackTrace();
}
try {
//從服務器讀取數據
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
channel.read(byteBuffer).get();//將通道中的數據寫入緩沖buffer
byteBuffer.flip();
String result = new String(byteBuffer.array(), 0, byteBuffer.limit());
System.out.println("客戶端收到服務器返回的內容:" + result);//輸出返回結果
} catch (Exception e) {
e.printStackTrace();
}
}
}
同樣的,依次啟動服務端程序,再啟動客戶端程序,看看運行結果!
服務端控制臺結果如下:
接收到新的客戶端連接,地址:/127.0.0.1:56606
服務端收到客戶端發送的信息:Hello,我是客戶端
客戶端控制臺結果如下:
客戶端收到服務器返回的內容:server send
這種組合方式用起來十分復雜,只有在一些非常復雜的分布式情況下使用,像集群之間的消息同步機制一般用這種 I/O 組合方式。如 Cassandra 的 Gossip 通信機制就是采用異步非阻塞的方式,可以實現非常高的網絡傳輸性能。
Netty 之前也嘗試使用過 AIO,不過又放棄了!
六、小結
本文主要圍繞 BIO、NIO、AIO 等模型,結合一些樣例代碼,做了一次簡單的內容知識總結,希望對大家有所幫助。
-
網絡數據傳輸
+關注
關注
0文章
4瀏覽量
6679 -
模型
+關注
關注
1文章
3305瀏覽量
49221 -
BIO
+關注
關注
0文章
6瀏覽量
9387 -
非阻塞
+關注
關注
0文章
13瀏覽量
2194
發布評論請先 登錄
相關推薦
評論