上次說到AIO,並做了開篇的簡短介紹,今天接着聊
AIO,一種異步非阻塞IO
在這裏回憶下前面所說的BIO和NIO,分別是同步阻塞IO和同步非阻塞IO,NIO在BIO的基礎上實現了對於自身的一個非阻塞操作,然而對於程序來說,無論是是BIO或者是NIO,其過程依然是一個同步的過程,例如讀寫文件,程序進程在讀寫文件時,總是一個同步操作,需要讀取/寫入完畢或者發生異常時,才能做其他的事,類似下面過程:
前面聊天小程序也是一樣,當客戶端嘗試連接服務端的時候,必須要等到服務端給出響應,那麼客戶端才結束方法的執行,即:
簡言之,類似網頁前端向服務端發起的AJAX請求,這是個客戶端向服務端發送的一個 “同步的Ajax請求”。
那麼AIO則提供了一個“異步的Ajax請求”,怎麼理解這句話?讀寫文件或者TCP連接總是立馬返回,不會存在同步等待。
即,讀寫文件變成這樣:
聊天小程序,TCP連接成這樣:
下面,以文件複製爲例,代碼說明下:
package com.lgli.aio.api;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
/**
* AioApi
* @author lgli
*/
public class AioApi {
public static void main(String[] args) throws Exception{
// copyFileAsyncChannelByFuture();
copyFileAsyncChannelByCompletionHandler();
}
/**
* 異步複製文件二
* @throws Exception
*/
private static void copyFileAsyncChannelByCompletionHandler() throws Exception{
//打開異步文件讀取通道
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝.mkv"),
StandardOpenOption.READ);
//打開異步文件寫入通道
AsynchronousFileChannel writeChannel = AsynchronousFileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝-AIO-HAND.mkv"),
StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
ByteBuffer byteBuffer = ByteBuffer.allocate(1048576);
long position = 0l;
while(true){
CountDownLatch downLatch = new CountDownLatch(1);
channel.read(byteBuffer, position, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
downLatch.countDown();
System.out.println("讀取完成");
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("讀取失敗");
}
});
downLatch.await();
byteBuffer.flip();
CountDownLatch downLatchs = new CountDownLatch(1);
writeChannel.write(byteBuffer, position, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("寫入完成");
downLatchs.countDown();
byteBuffer.clear();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("寫入失敗");
}
});
position += byteBuffer.limit();
downLatchs.await();
System.out.println(writeChannel.size()+"-------"+channel.size());
System.out.println(position);
if(writeChannel.size() >= channel.size()){
break;
}
}
System.out.println("複製完成");
}
/**
* 異步複製文件方式一
* @throws Exception
*/
private static void copyFileAsyncChannelByFuture() throws Exception{
//打開異步文件讀取通道
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝.mkv"),
StandardOpenOption.READ);
//打開異步文件寫入通道
AsynchronousFileChannel writeChannel = AsynchronousFileChannel.open(Paths.get("F:\\entertainment\\movie\\馬達加斯加的企鵝-AIO.mkv"),
StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//分配一個指定大小的緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
//讀取通道開始讀取數據
//java.nio.channels.AsynchronousFileChannel.read(java.nio.ByteBuffer, long)
//方法傳入2個參數,第一個是緩衝區,第二個是開始讀取的位置
//方法執行立即返回結果,即不會等待是否讀取完成,就返回了
long begin = 0l;
while(true){
Future<Integer> read = channel.read(byteBuffer, begin);
//可以通過方法java.util.concurrent.Future.isDone判斷是否讀取完成
while(!read.isDone()){
//這裏是爲了防止沒有讀取完數據,就執行後面的程序,所以這裏屬於瞎等待
//實際可以根據場景在異步的同時做其他的事
System.out.println("可以做其他事");
}
//讀取完成
//切換緩衝區讀寫方式
long limit = byteBuffer.limit();
byteBuffer.flip();
//將數據寫入
Future<Integer> write = writeChannel.write(byteBuffer, begin);
while(!write.isDone()){
//這裏是爲了防止沒有讀取完數據,就執行後面的程序,所以這裏屬於瞎等待
//實際可以根據場景在異步的同時做其他的事
System.out.println("可以做其他事");
}
begin += limit;
byteBuffer.clear();
if(writeChannel.size() == channel.size()){
//文件複製完成,退出
break;
}
}
System.out.println("複製完成");
}
}
這裏列舉了AIO異步讀寫數據的兩種方式,下面對代碼做些簡單的解釋,有需要詳細瞭解的,可查看:
https://mp.weixin.qq.com/s/5lOq0K24g17mklvCev3BQw
或者關注公衆號查看更多內容:
AIO異步讀寫文件方式一<java.util.concurrent.Future>
copyFileAsyncChannelByFuture方法:
89-93行:打開讀寫異步文件通道AsynchronousFileChannel,一個是讀,一個是寫
95行:分配讀寫緩衝區
由於這裏用較大文件複製作爲示例,故沒有分配足夠大的緩衝區來一次讀寫完,所以選擇循環讀寫,循環結尾123-126行,表示當文件複製後大小相等則複製完成,退出複製循環。
102行:異步讀取文件數據,這個時候,程序立馬返回結果,不管是否讀取完成。返回對象爲java.util.concurrent.Future
104-108行:可以通過java.util.concurrent.Future#isDone判斷讀取是否完成,如果沒有完成,這裏僅僅實例打印了一行文字–可以做其他的事,即在實際應用場景中,程序可以在讀取文件的同時做其他的任何事,而不是同步阻塞在這裏
112行:切換讀寫模式,變成寫
114行-119行:和讀取的意義一樣,不做過多解釋
121行:由於循環讀取,這裏需要一個參數來分批讀取和寫入數據
這裏對API做點描述:
java.nio.channels.AsynchronousFileChannel#read(java.nio.ByteBuffer, long)
java.nio.channels.AsynchronousFileChannel#write(java.nio.ByteBuffer, long)
上述2個方法,需要傳入2個參數,
第二個參數爲讀/寫的位置,所以循環體中的參數begin就是來記錄這個位置的
122行:清空緩衝區
最後文件複製完成。
AIO異步讀寫文件方式二<java.nio.channels.CompletionHandler>,回調方法處理,對應上述方法copyFileAsyncChannelByCompletionHandler
32-37行:打開讀寫異步文件通道AsynchronousFileChannel,一個是讀,一個是寫
38行:分配讀寫緩衝區
41行:定義CountDownLatch,這個是用來讓線程等待,讓其他線程執行完之後在執行的工具類,這裏由於AIO讀寫異步操作,爲了展示完整複製文件功能,所以用了這個工具,實際的項目中,除非特定情況,一般來說是不會讓線程掛起的,所以這裏僅僅爲了這裏的功能而使用
42-54行:讀取文件,由於是異步的,所以在55行執行線程掛起,讓讀取文件操作完成後,再執行之後的代碼。這裏的讀取文件方式,採用回調方法執行,即
java.nio.channels.CompletionHandler接口
這個接口有2個方法
java.nio.channels.CompletionHandler#completed
表示成功後執行的代碼
java.nio.channels.CompletionHandler#failed
表示失敗後執行的代碼
讀取文件變成了方法
java.nio.channels.AsynchronousFileChannel#read(
java.nio.ByteBuffer,
long,
A,
java.nio.channels.CompletionHandler<java.lang.Integer,? super A>)
需要傳入4個參數:緩衝區,讀取起始位置,附加IO操作對象《可以爲空》,回調接口
45行代碼,調用java.util.concurrent.CountDownLatch#countDown方法,告訴程序其他線程執行完畢<由於前面初始化CountDownLatch時,指定線程數爲1,然後調用countDown減去1,則其他線程爲0。即CountDownLatch只要其他線程爲0時,當前線程就不再掛起了,繼續執行後續程序>
56行:切換讀寫模式,變成寫
58-70行:寫入操作同讀取,這裏不再贅述
71行:記錄讀寫文件位置
75-78行:複製完成,退出循環
下面,基於AIO的異步非阻塞操作,對前面的聊天小程序改造
服務端:
package com.lgli.aio.chart;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* AioChartService
* @author lgli
*/
public class AioChartServer {
public AioChartServer(int port) {
try{
//打開異步服務socket通道
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
//綁定端口
serverSocketChannel.bind(new InetSocketAddress(port));
//保存連接的客戶端
List<AsynchronousSocketChannel> clients = new ArrayList<>();
while(true){
//監聽連接
CountDownLatch downLatch = new CountDownLatch(1);
//獲取到一個連接,則異步處理
serverSocketChannel.accept(serverSocketChannel, new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {
@Override
public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
try{
//保存連接的客戶端
clients.add(result);
//連接成功後,釋放當前線程,持續等待下一個連接
downLatch.countDown();
//接收到連接
System.out.println("接收到"+result.getRemoteAddress()+"的連接");
//發送歡迎頁到客戶端
result.write(ByteBuffer.wrap(("歡迎"+result.getRemoteAddress()+"來到聊天室").getBytes()));
//讀取和轉發客戶端發送的數據
//這是個持續的過程
while(true){
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
CountDownLatch c = new CountDownLatch(1);
result.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer intResult, ByteBuffer attachment) {
if(intResult == null || intResult == 0){
//沒有數據讀取
c.countDown();
return;
}
try{
//打印服務端收到的數據
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
String receive = new String(byteBuffer.get(bytes,0,byteBuffer.limit()).array());
System.out.println("服務端收到來自"+result.getRemoteAddress()+"的消息:"+receive);
String sendMsg = result.getRemoteAddress()+":"+receive;
//讀取完數據後發送數據到其他客戶端
for(AsynchronousSocketChannel channel : clients){
if(channel.getRemoteAddress().toString().equals(result.getRemoteAddress().toString())){
continue;
}
channel.write(StandardCharsets.UTF_8.encode(sendMsg));
}
byteBuffer.clear();
c.countDown();
}catch (Exception e){
c.countDown();
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("數據讀取異常");
exc.printStackTrace();
c.countDown();
}
});
c.await();
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
System.out.println("接收失敗");
}
});
downLatch.await();
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
new AioChartServer(8080);
}
}
客戶端:
package com.lgli.aio.chart;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
/**
* AioChartClient
* @author lgli
*/
public class AioChartClient {
public AioChartClient(int port) {
try{
//打開異步通道
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
CountDownLatch downLatch = new CountDownLatch(1);
//連接服務端
socketChannel.connect(new InetSocketAddress("localhost", port),
socketChannel, new CompletionHandler<Void, AsynchronousSocketChannel>() {
@Override
public void completed(Void result, AsynchronousSocketChannel attachment) {
System.out.println("連接到服務器");
//連接完成後,執行接收服務端的消息和向服務端發送消息
//以下讀取服務端數據和發送數據到服務端,是一個持續過程,這裏用個循環
//由於需要持續讀取服務端數據,這裏又需要接收鍵盤數據的數據發送到服務端,這裏另起一個線程去發送數據到客戶端
Runnable runnable = ()->{
//向服務端發送數據
Scanner scan = new Scanner(System.in);
while(scan.hasNextLine()){
String sendMsg = scan.nextLine();
if("".equals(sendMsg)){
continue;
}
//發送數據到服務端
attachment.write(StandardCharsets.UTF_8.encode(sendMsg));
}
};
Thread thread = new Thread(runnable);
thread.start();
//持續讀取服務端數據
while(true){
//讀取服務端發送的數據
CountDownLatch downLatchs = new CountDownLatch(1);
ByteBuffer receive = ByteBuffer.allocate(2048);
attachment.read(receive, receive, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
if(result == null || result == 0 || result == 17){
//沒有獲取到服務端發送的數據
//清空緩衝區
downLatchs.countDown();
receive.clear();
return;
}
//切換讀寫模式
receive.flip();
byte[] bytes = new byte[receive.limit()];
//讀取到服務端發送的數據
System.out.println(new String(receive.get(bytes,0,receive.limit()).array()));
//clear緩衝區
receive.clear();
downLatchs.countDown();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("讀取客戶端發送的數據異常");
downLatchs.countDown();
exc.printStackTrace();
}
});
try {
//每次接受數據完成後再接收下一次的數據
downLatchs.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
//打印連接失敗
System.out.println("連接服務器失敗");
exc.printStackTrace();
downLatch.countDown();
}
});
downLatch.await();
System.out.println("程序結束");
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
new AioChartClient(8080);
}
}
對於上述代碼,這裏不做過多解釋,每一步操作,基於前面的解釋,都是可以理解的。
運行上述代碼,可以達到一個簡易AIO聊天室效果
對於Java IO的歷史演變過程:BIO–>NIO–>NIO2(AIO),這裏基本就告一段落了
可是主題貌似還沒有進入
下期,將結合前面所有的東西,逐步過渡到主題上來,同時也開始Netty框架的正式學習篇
本次由於匆忙,所以文章可能有些地方未說得清楚,有需要清楚瞭解的,請查看公衆號文章內容。那裏比較詳細