架構底層的NIO入門,如何保證微服務高效通信的

Java BIO

  1. Java BIO 就是傳統的 java io編程,其相關的類和接口在 java.io包下
  2. BIO(blocking I/O) : 同步阻塞,服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過線程池機制改善
  3. BIO 方式適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,程序簡單易理解。

BIO 代碼示例

package nio;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {
    public static void main(String[] args) throws Exception {
        //線程池機制
        //思路
        //1. 創建一個線程池
        //2. 如果有客戶端連接,就創建一個線程,與之通訊(單獨寫一個方法)
        //ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        ExecutorService newCachedThreadPool = Executors.newSingleThreadExecutor();
        //創建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服務器啓動了");
        while (true) {
            System.out.println("線程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName()+"等待連接....");
            //監聽,等待客戶端連接,阻塞
            final Socket socket = serverSocket.accept();
            System.out.println("連接到一個客戶端");

            //就創建一個線程,與之通訊(單獨寫一個方法)
            newCachedThreadPool.execute(new Runnable() {
                public void run() { //我們重寫
                    //可以和客戶端通訊
                    handler(socket);
                }
            });
        }
    }

    //編寫一個handler方法,和客戶端通訊
    public static void handler(Socket socket) {
        try {
            byte[] bytes = new byte[1024];
            //通過socket 獲取輸入流
            InputStream inputStream = socket.getInputStream();
            //循環的讀取客戶端發送的數據
            while (true) {
                //read也是阻塞的
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println("線程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName()+"讀到:"+
                    new String(bytes, 0, read)); //輸出客戶端發送的數據
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("關閉和client的連接");
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

java BIO的缺點

  1. 每個請求都需要創建獨立的線程,與對應的客戶端進行數據 Read,業務處理,數據 Write 。當併發數較大時,需要創建大量線程來處理連接,系統資源佔用較大。
    file
  2. 連接建立後,如果當前線程暫時沒有數據可讀,則線程就阻塞在 Read 操作上,造成線程資源浪費。其他請求不能使用當前被掛起的線程。
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IIFgERKi-1587207564828)(http://www.txjava.cn/wordpress/wp-content/uploads/2020/04/image-1586499972233.png)]

我們可以通過telnet來測試

我們能看到,每一個請求都需要一個一個線程來處理
file
此時我們如果採用單例線程池來做多個請求,我們發現程序無法同時處理多個請求,只有關閉正在阻塞的請求才能去處理其他的請求。顯然不合理。

此種方式符合 同步阻塞IO模型
file

NIO

  1. Java NIO 全稱 java non-blocking IO,是指 JDK提供的新 API。從JDK1.4 開始,Java 提供了一系列改進的輸入/輸出的新特性,被統稱爲NIO(即New IO),是同步非阻塞的
  2. NIO 相關類都被放在 java.nio 包及子包下,並且對原 java.io 包中的很多類進行改寫。
  3. NIO有三大核心部分:Channel(通道),Buffer(緩衝區), Selector(選擇器)
  4. NIO 是面向緩衝區編程的。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞式的高伸縮性網絡
  5. Java NIO 的非阻塞模式,使一個線程從某通道發送請求或者讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。
  6. 通俗理解:NIO 是可以做到用一個線程來處理多個操作的。假設有 10000 個請求過來,根據實際情況,可以分配50 或者 100 個線程來處理。不像之前的阻塞 IO 那樣,非得分配 10000 個。
  7. HTTP2.0 使用了多路複用的技術,做到同一個連接併發處理多個請求,而且併發請求的數量比 HTTP1.1 大了好幾個數量級
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Ef2wNLD3-1587207564837)(http://www.txjava.cn/wordpress/wp-content/uploads/2020/04/image-1586502329862.png)]

簡單NIO示例

服務端代碼:

package nio.demo1;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NIOServer {

    /**
     * 創建一個緩存區
     * @param args
     * @throws Exception
     */

    static ByteBuffer buffer = ByteBuffer.allocate(1024);

    public static void main(String[] args) throws Exception {
        //獲得服務器斷點的服務管道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //綁定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //設置成異步
        serverSocketChannel.configureBlocking(false);

        while (true){
            //獲得客戶端連接過來的管道
            SocketChannel socketChannel = serverSocketChannel.accept();
            if(socketChannel != null){
                socketChannel.configureBlocking(false);
                int length = socketChannel.read(buffer);
                if(length != -1){
                    System.out.println("接收到的數據是:"+new String(buffer.array(), 0, length));
                }
            }
        }
    }
}

客戶端代碼

public class NIOClient {

    public static void main(String[] args) throws Exception {
        InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 6666);
        Socket socket = new Socket();
        socket.connect(socketAddress);
        socket.getOutputStream().write("hello".getBytes());
    }
}

在服務端由於是非阻塞IO,那麼我們無法監控到客戶端的連接,所以我們採用不斷輪詢的方式來監控(顯然這是一種性能的巨大消耗),此種方法是同步非阻塞IO模型的一種方式
file

NIO多路複用解決方案

我們可以看到,我們剛剛手動的方式來做了一個很不合理的方案。那麼到底什麼樣的方案是完美的方案呢?

選擇器

  1. Java 的 NIO,用非阻塞的 IO 方式。可以用一個線程,處理多個的客戶端連接,就會使用到 Selector(選擇器)
  2. Selector 能夠檢測多個註冊的通道上是否有事件發生(注意:多個 Channel 以事件的方式可以註冊到同一個Selector),如果有事件發生,便獲取事件然後針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。
  3. 只有在連接/通道 真正有讀寫事件發生時,纔會進行讀寫,就大大地減少了系統開銷,並且不必爲每個連接都創建一個線程,不用去維護多個線程, 避免了多線程之間的上下文切換導致的開銷
    file

NIO多路複用實現代碼

服務端:

package nio.demo2;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NIOServer1 {

    public static void main(String[] args) throws Exception {
        //獲得服務器斷點的服務管道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //綁定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //設置成異步
        serverSocketChannel.configureBlocking(false);
        //創建Selector
        Selector selector = Selector.open();
        //註冊
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            while(selector.select(2000) == 0){
                System.out.println("等待連接");
                continue;
            }
            //獲得準備就緒的key
            Set<SelectionKey> keys = selector.selectedKeys();
            //遍歷keys
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()){
                //獲得key
                SelectionKey key = iterator.next();
                if(key.isAcceptable()){
                    //獲得通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    if(socketChannel != null){
                        System.out.println(Thread.currentThread().getName()+" 接收到一個連接:"+socketChannel.getRemoteAddress());
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    }
                }
                if(key.isReadable()){
                    //獲得通道
                    SocketChannel channel = (SocketChannel) key.channel();
                    //獲得緩衝區
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    int length = channel.read(buffer);
                    if(length != -1){
                        System.out.println(Thread.currentThread().getName()+"  發送的數據是:"+new String(buffer.array(), 0, length)+"   客戶端的地址:"+channel.getRemoteAddress());
                    }
                    buffer.clear();
                }
                iterator.remove();
            }
        }
    }
}

程序流程:

file

  1. 服務端啓動時候獲得Selector,註冊給ServerSocketChannel
  2. Selector 調用select方法, 監聽請求和讀寫事件,返回有事件發生的通道的個數.
  3. 通過selector獲得就緒狀態的鍵集合,然後遍歷這個集合判斷這個鍵的類型
  4. 當客戶端連接時key類型是OP_ACCEPT,會通過 ServerSocketChannel 得到 SocketChannel, 把該通道連同buffer註冊到selector選擇器上。一個 selector 上可以註冊多個 SocketChannel
  5. 註冊後返回一個 SelectionKey, 會和該 Selector 關聯(集合)
  6. 進一步得到各個 SelectionKey (有事件發生)
  7. 如果key的類型是OP_READ或者OP_WRITE(讀寫),通過 SelectionKey反向獲取 SocketChannel , 方法 channel()
  8. 可以通過得到的channel, 完成業務處理
    此刻我會發現我們的NIO使用的就是IO多路複用的模型。
    file
    更詳細的Spring源碼解析請關注:java架構師免費課程
    每晚20:00直播分享高級java架構技術
    掃描加入QQ交流羣264572737
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章