JavaIO詳解--儘可能將BIO、NIO、AIO講得通俗易懂

一、BIO概述

BIO即同步阻塞IO,實現模型爲一個連接就需要一個線程去處理。這種方式簡單來說就是當有客戶端來請求服務器時,服務器就會開啓一個線程去處理這個請求,即使這個請求不幹任何事情,這個線程都一直處於阻塞狀態。

BIO模型有很多缺點,最大的缺點就是資源的浪費。想象一下如果QQ使用BIO模型,當有一個人上線時就需要一個線程,即使這個人不聊天,這個線程也一直被佔用,那再多的服務器資源都不管用

二、BIO代碼實踐

我們通過socket模擬BIO的實現邏輯

首先建立Server,建立一個ServerSocket對象,綁定端口,然後等待連接,如果連接成功就新建一個線程去處理連接。

public class server {
    private static Socket socket=null;
    public static void main(String[] args) {
        try {
            //綁定端口
            ServerSocket serverSocket=new ServerSocket();
            serverSocket.bind(new InetSocketAddress(8080));
            while (true){
                //等待連接  阻塞
                System.out.println("等待連接");
                socket = serverSocket.accept();
                System.out.println("連接成功");
                //連接成功後新開一個線程去處理這個連接
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        byte[] bytes=new byte[1024];
                        try {
                            System.out.println("等待讀取數據");
                            //等待讀取數據    阻塞
                            int length=socket.getInputStream().read(bytes);
                            System.out.println(new String(bytes,0,length));
                            System.out.println("數據讀取成功");
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

接着建立Client代碼

public class Client {
    public static void main(String[] args) {
        Socket socket= null;
        try {
            socket = new Socket("127.0.0.1",8080);
            socket.getOutputStream().write("一條數據".getBytes());
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客戶端的代碼就連接一個服務器,然後發出一條數據即可。

這樣就實現了一個BIO,但是BIO的缺點實在太明顯了,因此在JDK1.4的時候,NIO出現了。

三、NIO概述

BIO是阻塞的,如果沒有多線程,BIO就需要一直佔用CPU,而NIO則是非阻塞IO,NIO在獲取連接或者請求時,即使沒有取得連接和數據,也不會阻塞程序。NIO有幾個知識點需要掌握,Channel(通道),Buffer(緩衝區), Selector(選擇器(多路複用))

Channel既可以用來進行讀操作,又可以用來進行寫操作。NIO中常用的Channel有FileChannel

、SocketChannel、ServerSocketChannel、DatagramChannel。

Buffer緩衝區用來發送和接受數據。

Selector 一般稱爲選擇器或者多路複用器 。它是Java NIO核心組件中的一個,用於檢查一個或多個NIO Channel(通道)的狀態是否處於可讀、可寫。在javaNIO中使用Selector往往是將Channel註冊到Selector中。

下面我通過代碼的方式模擬javaNIO的運行流程

四、NIO代碼實踐

在BIO的代碼中,有兩處位置發生了阻塞:1.等待請求時,2.等待數據時

socket = serverSocket.accept();
int length=socket.getInputStream().read(bytes);

而NIO的代碼就是要讓這兩處的阻塞變爲非阻塞,Socket本身並不支持NIO,因爲java創建了支持NIO的socket,SocketChannelServerSocketChannel。通過configureBlocking方法設置爲非阻塞。

ServerSocketChannel是NIO中的通道,ByteBuffer是獲取數據的緩衝區,多路複用機制我在這裏使用for循環代替,而多路複用機制實際上是通過select/epoll/poll實現的,這需要涉及到底層的知識,當一個socket連接成功時,就將他放入集合中,並不斷遍歷這個集合觀察是否有哪個socket發送了數據。

服務端代碼:

public class Server {
    //list用來存放已經連接的socket
    static List<SocketChannel> list=new ArrayList<>();
    //緩衝區,用來獲取數據
    static ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
    public static void main(String[] args) {
        try {
            //創建一個socketchannel,並綁定端口
            ServerSocketChannel server=ServerSocketChannel.open();
            server.bind(new InetSocketAddress(8080));
            //設置這個通道爲非阻塞,對應於BIO的第一處阻塞
            server.configureBlocking(false);
            while (true){
                //模擬多路複用器,不斷查詢是否有數據發過來
                for (SocketChannel socketChannel1:list){
                    byteBuffer.clear();
                    int length = socketChannel1.read(byteBuffer);
                    if (length>0){
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array()));
                    }
                }
                //非阻塞的獲取連接
                SocketChannel socketChannel = server.accept();
                //如果沒有,則等待
                if (socketChannel==null){
                    System.out.println("等待連接");
                    Thread.sleep(1000);
                }else {
                    //如果獲取到了一個連接,則把這個連接也設置爲非阻塞,對應於BIO的第二處阻塞
                    socketChannel.configureBlocking(false);
                    //將設置連接放進list中,不斷輪詢是否有消息發出
                    list.add(socketChannel);
                    System.out.println("連接成功");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

客戶端代碼:

我們不必要求客戶端也實現NIO,因此客戶端使用BIO實現

public class client {
    public static void main(String[] args) {
        try {
            Socket socket=new Socket("127.0.0.1",8080);
            Scanner scanner=new Scanner(System.in);
            String str=scanner.next();
            socket.getOutputStream().write(str.getBytes());
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

當啓動服務器端時,不斷輸出等待連接,當啓動客戶端後,輸出了連接成功,但此時並未阻塞程序,而是繼續等待。

當有數據發送給服務器端時,則接收到了ByteBuffer的數據,程序的所有執行過程都處於非阻塞狀態。

五、AIO概述

AIO是在JDK1.7中推出的新的IO方式--異步非阻塞IO,也被稱爲NIO2.0,AIO在進行讀寫操作時,直接調用API的read和write方法即可,這兩種均是異步的方法,且完成後會主動調用回調函數。簡單來講,當有流可該取時,操作系統會將可讀的流傳入read方法的緩衝區,並通知應用程序;對於寫操作而言,當保作系統將write方法傳遞的流寫入完畢時,操作系統主動通知應用程序。

Java提供了四個異步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。

六、AIO代碼實踐

服務器端代碼:AIO的創建方式和NIO類似,先創建通道,再綁定,再監聽。只不過這裏的監聽方式用了類似遞歸的監聽方式。在讀取數據的操作中,read方法也和NIO有區別。

public class AIOServer {
    public static void main(String[] args) {
        try {
            //創建異步通道
            AsynchronousServerSocketChannel serverSocketChannel=AsynchronousServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            System.out.println("等待連接中");
            //在AIO中,accept有兩個參數,
            // 第一個參數是一個泛型,可以用來控制想傳遞的對象
            // 第二個參數CompletionHandler,用來處理監聽成功和失敗的邏輯
            //  如此設置監聽的原因是因爲這裏的監聽是一個類似於遞歸的操作,每次監聽成功後要開啓下一個監聽
            serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
                //請求成功處理邏輯
                @Override
                public void completed(AsynchronousSocketChannel result, Object attachment) {
                    System.out.println("連接成功,處理數據中");
                    //開啓新的監聽
                    serverSocketChannel.accept(null,this);
                    handledata(result);
                }
                @Override
                public void failed(Throwable exc, Object attachment) {
                    System.out.println("失敗");
                }
            });
            try {
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handledata(AsynchronousSocketChannel result) {
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        //通道的read方法也帶有三個參數
        //1.目的地:處理客戶端傳遞數據的中轉緩存,可以不使用
        //2.處理客戶端傳遞數據的對象
        //3.處理邏輯,也有成功和不成功的兩個寫法
        result.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                if (result>0){
                    attachment.flip();
                    byte[] array = attachment.array();
                    System.out.println(new String(array));
                }
            }
            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                System.out.println("失敗");
            }
        });
    }
}

客戶端代碼基本上沒有太多差別,可以使用同步的方式也可以使用異步方式,這裏使用異步處理的方式:

public class AIOClient {
    public static void main(String[] args) {
        try {
            AsynchronousSocketChannel socketChannel=AsynchronousSocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
            Scanner scanner=new Scanner(System.in);
            String next = scanner.next();
            ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
            byteBuffer.put(next.getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

觀察結果:

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