基於Netty實現Web容器-Netty版Tomcat(二)

上次說到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框架的正式學習篇

本次由於匆忙,所以文章可能有些地方未說得清楚,有需要清楚瞭解的,請查看公衆號文章內容。那裏比較詳細

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章