對於同步和非同步,阻塞和非阻塞,BIO,NIO的概念的回顧

同步和異步

同步和異步其實是指CPU時間片的利用,主要看請求發起方對消息結果的獲取是主動發起的,還是被動通知的,如下圖所示。如果是請求方主動發起的,一直在等待應答結果(同步阻塞),或者可以先去處理其他事情,但要不斷輪詢查看發起的請求是否有應答結果(同步非阻塞),因爲不管如何都要發起方主動獲取消息結果,所以形式上還是同步操作。如果是由服務方通知的,也就是請求方發出請求後,要麼一直等待通知(異步阻塞),要麼先去幹自己的事(異步非阻塞)。當事情處理完成後,服務方會主動通知請求方,它的請求已經完成,這就是異步。異步通知的方式一般通過狀態改變、消息通知或者回調函數來完成,大多數時候採用的都是回調函數。

 

阻塞和非阻塞

阻塞和非阻塞在計算機的世界裏,通常指針對I/O的操作,如網絡I/O和磁盤I/O等。那麼什麼是阻塞和非阻塞呢?簡單地說,就是我們調用了一個函數後,在等待這個函數返回結果之前,當前的線程是處於掛起狀態還是運行狀態。如果是掛起狀態,就意味着當前線程什麼都不能幹,就等着獲取結果,這就是同步阻塞;如果仍然是運行狀態,就意味着當前線程是可以繼續處理其他任務的,但要時不時地看一下是否有結果了,這就是同步非阻塞。具體如下圖所示

 

阻塞I/O模型

 

非阻塞I/O模型

多路複用I/O模型

從BIO到NIO的演進

面向流與面向緩衝Java NIO和BIO之間第一個最大的區別是,BIO是面向流的,NIO是面向緩衝區的。Java BIO面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,不能前後移動流中的數據。如果需要前後移動從流中讀取的數據,需要先將它緩存到一個緩衝區。Java NIO的緩衝導向方法略有不同。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程的靈活性。但是,還需要檢查該緩衝區是否包含所有需要處理的數據。而且,要確保當更多的數據讀入緩衝區時,不能覆蓋緩衝區裏尚未處理的數據。

例子分析

阻塞I/O在調用InputStream.read()方法時是阻塞的,它會一直等到數據到來(或超時)時纔會返回;同樣,在調用ServerSocket.accept()方法時,也會一直阻塞到有客戶端連接纔會返回,每個客戶端連接成功後,服務端都會啓動一個線程去處理該客戶端的請求。阻塞I/O的通信模型示意如下圖所示。

缺點。(1)當客戶端多時,會創建大量的處理線程。且每個線程都要佔用棧空間和一些CPU時間。(2)阻塞可能帶來頻繁的上下文切換,且大部分上下文切換可能是無意義的。在這種情況下非阻塞I/O就有了它的應用前景。

BIO代碼分析:

package com.io.demo;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Objects;

public class BioServer {

    private String ip;

    private int port;

    public BioServer(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }

    public void startListen() throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(ip, port));

        while (true) {
            // 阻塞
            Socket socket = serverSocket.accept();
            new RequestHandler(socket).start();
        }
    }

    private class RequestHandler extends Thread {
        private final Socket socket;


        public RequestHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            InputStream in = null;
            InputStreamReader reader = null;
            BufferedReader br = null;
            OutputStream out = null;
            OutputStreamWriter writer = null;
            BufferedWriter bw = null;
            try {
                //輸入流用於接收客戶端傳輸過來的數據
                in = socket.getInputStream();
                reader = new InputStreamReader(in);
                br = new BufferedReader(reader);
                String line;
                while ((line = br.readLine()) != null) {
                    System.out.println(line);
                    //按照HTTP協議,請求頭和請求體之間爲一空行分隔
                    if ("".equals(line)) {
                        break;
                    }
                }
                //輸出流用於向客戶端發送響應消息,需遵從HTTP協議格式
                out = socket.getOutputStream();
                writer = new OutputStreamWriter(out);
                bw = new BufferedWriter(writer);
                bw.write(getResponseText());
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //必須在請求讀取和響應寫入都處理完畢之後纔可以調用close方法,將輸入流關閉也會導致輸出流不可用
                try {
                    Objects.requireNonNull(bw).close();
                    writer.close();
                    out.close();
                    br.close();
                    reader.close();
                    in.close();
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    private String getResponseText() {
        StringBuffer sb = new StringBuffer();
        sb.append("HTTP/1.1 200 OK\n");
        sb.append("Content-Type: text/html; charset=UTF-8\n");
        sb.append("\n");
        sb.append("<html>");
        sb.append("  <head>");
        sb.append("    <title>");
        sb.append("      NIO Http Server");
        sb.append("    </title>");
        sb.append("  </head>");
        sb.append("  <body>");
        sb.append("    <h1>Hello World!</h1>");
        sb.append("  </body>");
        sb.append("</html>");

        return sb.toString();
    }

    public static void main(String[] args) {
        BioServer server = new BioServer("127.0.0.1", 8081);
        try {
            server.startListen();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

Java NIO的工作原理。(1)由一個專門的線程來處理所有的I/O事件,並負責分發。(2)事件驅動機制:事件到的時候觸發,而不是同步地去監視事件。(3)線程通信:線程之間通過wait、notify等方式通信。保證每次上下文切換都是有意義的,減少無謂的線程切換。下面是筆者理解的Java NIO反應堆的工作原理圖。

(注:每個線程的處理流程大概都是讀取數據、解碼、計算處理、編碼和發送響應。)

NIO代碼分析

package com.io.demo;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;

public class NioServer {
    private String ip;

    private int port;

    private Selector selector;

    public NioServer(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }

    public void startListen() throws IOException {
        selector = Selector.open();
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        serverChannel.bind(new InetSocketAddress(ip, port));

        while (true) {
            //不能使用select方法,該方法會阻塞,如果在阻塞過程中channel狀態就緒,會因此處阻塞而無法執行。
            //所以,如果調用阻塞方法,下面對channel狀態的處理得另起一個常駐線程
            int result = selector.selectNow();
            if (result == 0) {
                continue;
            }
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                if (key.isAcceptable()) {
                    accept(key);
                } else if (key.isReadable()) {
                    read(key);
                } else if (key.isWritable()) {
                    write(key);
                } else {
                    System.out.println("Unknow selector type");
                }

                //一定要調用remove方法將已經處理過的SelectionKey清除掉,否則會造成後面的請求無法接受
                it.remove();
            }
        }
    }

    private void accept(SelectionKey key) throws IOException {
        System.out.println("Receive connection");
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        SocketChannel channel = serverSocketChannel.accept();

        if (channel != null) {
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);
        }
        System.out.println("Connection end");
    }

    private void read(SelectionKey key) throws IOException {
        System.out.println("Start read");
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(64);
        boolean hasContent = false;

        //這裏的判斷條件不能是不等於-1,因爲channel一直都在,只是在數據被讀完后里面爲空,返回的長度是0.用-1判斷會無限循環無法退出
        while (channel.read(buffer) > 0) {
            buffer.flip(); //切換爲讀模式
            CharBuffer cb = Charset.forName("UTF-8").decode(buffer);
            System.out.print(cb.toString());
            buffer.clear();
            hasContent = true;
        }

        if (hasContent) {
            //設置interestOps,用於寫響應
            key.interestOps(SelectionKey.OP_WRITE);
        } else {
            channel.close();
        }
        System.out.println("Read end");
    }

    private void write(SelectionKey key) throws IOException {
        System.out.println("Start write");
        SocketChannel channel = (SocketChannel) key.channel();

        String resText = getResponseText();
        ByteBuffer buffer = ByteBuffer.wrap(resText.getBytes());

        //此處不可使用channel.write(buffer) != -1來判斷,因爲在兩端都不關閉的情況下,會一直返回0,導致該循環無法退出
        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }
        channel.close();
        System.out.println("End write");
    }

    private String getResponseText() {
        StringBuffer sb = new StringBuffer();
        sb.append("HTTP/1.1 200 OK\n");
        sb.append("Content-Type: text/html; charset=UTF-8\n");
        sb.append("\n");
        sb.append("<html>");
        sb.append("  <head>");
        sb.append("    <title>");
        sb.append("      NIO Http Server");
        sb.append("    </title>");
        sb.append("  </head>");
        sb.append("  <body>");
        sb.append("    <h1>Hello World!</h1>");
        sb.append("  </body>");
        sb.append("</html>");

        return sb.toString();
    }

    public static void main(String[] args) {
        NioServer server = new NioServer("127.0.0.1", 8080);
        try {
            server.startListen();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

 

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