「Netty 01」從 BIO、NIO 聊到 Netty,最後還要實現個 RPC 框架!

你們好,我是 「後端技術進階」 做者,一個熱愛技術的少年。html

@[toc]java

以爲不錯的話,歡迎 star!ღ( ´・ᴗ・` )比心git

老套路,學習某一門技術或者框架的時候,第一步固然是要了解下面這幾樣東西。github

  1. 是什麼?
  2. 有哪些特色?
  3. 有哪些應用場景?
  4. 有哪些成功使用的案例?
  5. .....

爲了讓你更好地瞭解 Netty 以及它誕生的緣由,先從傳統的網絡編程提及吧!編程

仍是要從 BIO 提及

傳統的阻塞式通訊流程

早期的 Java 網絡相關的 API(java.net包) 使用 Socket(套接字)進行網絡通訊,不過只支持阻塞函數使用。後端

要經過互聯網進行通訊,至少須要一對套接字:安全

  1. 運行於服務器端的 Server Socket。
  2. 運行於客戶機端的 Client Socket

Socket 網絡通訊過程以下圖所示:服務器

img

https://www.javatpoint.com/so...微信

Socket 網絡通訊過程簡單來講分爲下面 4 步:網絡

  1. 創建服務端而且監聽客戶端請求
  2. 客戶端請求,服務端和客戶端創建鏈接
  3. 兩端之間能夠傳遞數據
  4. 關閉資源

對應到服務端和客戶端的話,是下面這樣的。

服務器端:

  1. 建立 ServerSocket 對象而且綁定地址(ip)和端口號(port): server.bind(new InetSocketAddress(host, port))
  2. 經過 accept()方法監聽客戶端請求
  3. 鏈接創建後,經過輸入流讀取客戶端發送的請求信息
  4. 經過輸出流向客戶端發送響應信息
  5. 關閉相關資源

客戶端:

  1. 建立Socket 對象而且鏈接指定的服務器的地址(ip)和端口號(port):socket.connect(inetSocketAddress)
  2. 鏈接創建後,經過輸出流向服務器端發送請求信息
  3. 經過輸入流獲取服務器響應的信息
  4. 關閉相關資源

一個簡單的 demo

爲了便於理解,我寫了一個簡單的代碼幫助各位小夥伴理解。

服務端:

public class HelloServer {
    private static final Logger logger = LoggerFactory.getLogger(HelloServer.class);

    public void start(int port) {
        //1.建立 ServerSocket 對象而且綁定一個端口
        try (ServerSocket server = new ServerSocket(port);) {
            Socket socket;
            //2.經過 accept()方法監聽客戶端請求, 這個方法會一直阻塞到有一個鏈接創建
            while ((socket = server.accept()) != null) {
                logger.info("client connected");
                try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                     ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
                   //3.經過輸入流讀取客戶端發送的請求信息
                    Message message = (Message) objectInputStream.readObject();
                    logger.info("server receive message:" + message.getContent());
                    message.setContent("new content");
                    //4.經過輸出流向客戶端發送響應信息
                    objectOutputStream.writeObject(message);
                    objectOutputStream.flush();
                } catch (IOException | ClassNotFoundException e) {
                    logger.error("occur exception:", e);
                }
            }
        } catch (IOException e) {
            logger.error("occur IOException:", e);
        }
    }

    public static void main(String[] args) {
        HelloServer helloServer = new HelloServer();
        helloServer.start(6666);
    }
}

ServerSocketaccept() 方法是阻塞方法,也就是說 ServerSocket 在調用 accept()等待客戶端的鏈接請求時會阻塞,直到收到客戶端發送的鏈接請求才會繼續往下執行代碼,所以咱們須要要爲每一個 Socket 鏈接開啓一個線程(能夠經過線程池來作)。

上述服務端的代碼只是爲了演示,並無考慮多個客戶端鏈接併發的狀況。

客戶端:

/**
 * @author shuang.kou
 * @createTime 2020年05月11日 16:56:00
 */
public class HelloClient {

    private static final Logger logger = LoggerFactory.getLogger(HelloClient.class);

    public Object send(Message message, String host, int port) {
        //1. 建立Socket對象而且指定服務器的地址和端口號
        try (Socket socket = new Socket(host, port)) {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            //2.經過輸出流向服務器端發送請求信息
            objectOutputStream.writeObject(message);
            //3.經過輸入流獲取服務器響應的信息
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            return objectInputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            logger.error("occur exception:", e);
        }
        return null;
    }

    public static void main(String[] args) {
        HelloClient helloClient = new HelloClient();
        helloClient.send(new Message("content from client"), "127.0.0.1", 6666);
        System.out.println("client receive message:" + message.getContent());
    }
}

發送的消息實體類

/**
 * @author shuang.kou
 * @createTime 2020年05月11日 17:02:00
 */
@Data
@AllArgsConstructor
public class Message implements Serializable {

    private String content;
}

首先運行服務端,而後再運行客戶端,控制檯輸出以下:

服務端:

[main] INFO github.javaguide.socket.HelloServer - client connected
[main] INFO github.javaguide.socket.HelloServer - server receive message:content from client

客戶端:

client receive message:new content

資源消耗嚴重的問題

很明顯,我上面演示的代碼片斷有一個很嚴重的問題:只能同時處理一個客戶端的鏈接,若是須要管理多個客戶端的話,就須要爲咱們請求的客戶端單首創建一個線程。 以下圖所示:

img

對應的 Java 代碼多是下面這樣的:

new Thread(() -> {
   // 建立 socket 鏈接
}).start();

可是,這樣會致使一個很嚴重的問題:資源浪費

咱們知道線程是很寶貴的資源,若是咱們爲每一次鏈接都用一個線程處理的話,就會致使線程愈來愈好,最好達到了極限以後,就沒法再建立線程處理請求了。處理的很差的話,甚至可能直接就宕機掉了。

不少人就會問了:那有沒有改進的方法呢?

線程池雖能夠改善,但終究未從根本解決問題

固然有! 比較簡單而且實際的改進方法就是使用線程池。線程池還可讓線程的建立和回收成本相對較低,而且咱們能夠指定線程池的可建立線程的最大數量,這樣就不會致使線程建立過多,機器資源被不合理消耗。

ThreadFactory threadFactory = Executors.defaultThreadFactory();
ExecutorService threadPool = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(100), threadFactory);
threadPool.execute(() -> {
     // 建立 socket 鏈接
 });

可是,即便你再怎麼優化和改變。也改變不了它的底層仍然是同步阻塞的 BIO 模型的事實,所以沒法從根本上解決問題。

爲了解決上述的問題,Java 1.4 中引入了 NIO ,一種同步非阻塞的 I/O 模型。

再看 NIO

Netty 實際上就基於 Java NIO 技術封裝完善以後獲得一個高性能框架,熟悉 NIO 的基本概念對於學習和更好地理解 Netty 仍是頗有必要的!

初識 NIO

NIO 是一種同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,對應 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。

NIO 中的 N 能夠理解爲 Non-blocking,已經不在是 New 了(已經出來很長時間了)。

NIO 支持面向緩衝(Buffer)的,基於通道(Channel)的 I/O 操做方法。

NIO 提供了與傳統 BIO 模型中的 SocketServerSocket 相對應的 SocketChannelServerSocketChannel 兩種不一樣的套接字通道實現,兩種通道都支持阻塞和非阻塞兩種模式:

  1. 阻塞模式 : 基本不會被使用到。使用起來就像傳統的網絡編程同樣,比較簡單,可是性能和可靠性都很差。對於低負載、低併發的應用程序,勉強能夠用一下以提高開發速率和更好的維護性
  2. 非阻塞模式 : 與阻塞模式正好相反,非阻塞模式對於高負載、高併發的(網絡)應用來講很是友好,可是編程麻煩,這個是大部分人詬病的地方。因此, 也就致使了 Netty 的誕生。

NIO 核心組件解讀

NIO 包含下面幾個核心的組件:

  • Channel
  • Buffer
  • Selector
  • Selection Key

這些組件之間的關係是怎麼的呢?

img

  1. NIO 使用 Channel(通道)和 Buffer(緩衝區)傳輸數據,數據老是從緩衝區寫入通道,並從通道讀取到緩衝區。在面向流的 I/O 中,能夠將數據直接寫入或者將數據直接讀到 Stream 對象中。在 NIO 庫中,全部數據都是經過 Buffer(緩衝區)處理的。 Channel 能夠看做是 Netty 的網絡操做抽象類,對應於 JDK 底層的 Socket
  2. NIO 利用 Selector (選擇器)來監視多個通道的對象,如數據到達,鏈接打開等。所以,單線程能夠監視多個通道中的數據。
  3. 當咱們將 Channel 註冊到 Selector 中的時候, 會返回一個 Selection Key 對象, Selection Key 則表示了一個特定的通道對象和一個特定的選擇器對象之間的註冊關係。經過 Selection Key 咱們能夠獲取哪些 IO 事件已經就緒了,而且能夠經過其獲取 Channel 並對其進行操做。

Selector(選擇器,也能夠理解爲多路複用器)是 NIO(非阻塞 IO)實現的關鍵。它使用了事件通知相關的 API 來實現選擇已經就緒也就是可以進行 I/O 相關的操做的任務的能力。

簡單來講,整個過程是這樣的:

  1. 將 Channel 註冊到 Selector 中。
  2. 調用 Selector 的 select() 方法,這個方法會阻塞;
  3. 到註冊在 Selector 中的某個 Channel 有新的 TCP 鏈接或者可讀寫事件的話,這個 Channel 就會處於就緒狀態,會被 Selector 輪詢出來。
  4. 而後經過 SelectionKey 能夠獲取就緒 Channel 的集合,進行後續的 I/O 操做。

NIO 爲啥更好?

相比於傳統的 BIO 模型來講, NIO 模型的最大改進是:

  1. 使用比較少的線程即可以管理多個客戶端的鏈接,提升了併發量而且減小的資源消耗(減小了線程的上下文切換的開銷)
  2. 在沒有 I/O 操做相關的事情的時候,線程能夠被安排在其餘任務上面,以讓線程資源獲得充分利用。

使用 NIO 編寫代碼太難了

一個使用 NIO 編寫的 Server 端以下,能夠看出仍是總體仍是比較複雜的,而且代碼讀起來不是很直觀,而且還可能因爲 NIO 自己會存在 Bug。

不多使用 NIO,很大狀況下也是由於使用 NIO 來建立正確而且安全的應用程序的開發成本和維護成本都比較大。因此,通常狀況下咱們都會使用 Netty 這個比較成熟的高性能框架來作(Apace Mina 與之相似,可是 Netty 使用的更多一點)。

在這裏插入圖片描述

重要角色 Netty 登場

簡單用 3 點歸納一下 Netty 吧!

  1. Netty 是一個基於 NIO 的 client-server(客戶端服務器)框架,使用它能夠快速簡單地開發網絡應用程序。
  2. 它極大地簡化並簡化了 TCP 和 UDP 套接字服務器等網絡編程,而且性能以及安全性等不少方面甚至都要更好。
  3. 支持多種協議如 FTP,SMTP,HTTP 以及各類二進制和基於文本的傳統協議。

用官方的總結就是:Netty 成功地找到了一種在不妥協可維護性和性能的狀況下實現易於開發,性能,穩定性和靈活性的方法。

Netty 特色

根據官網的描述,咱們能夠總結出下面一些特色:

  • 統一的 API,支持多種傳輸類型,阻塞和非阻塞的。
  • 簡單而強大的線程模型。
  • 自帶編解碼器解決 TCP 粘包/拆包問題。
  • 自帶各類協議棧。
  • 真正的無鏈接數據包套接字支持。
  • 比直接使用 Java 核心 API 有更高的吞吐量、更低的延遲、更低的資源消耗和更少的內存複製。
  • 安全性不錯,有完整的 SSL/TLS 以及 StartTLS 支持。
  • 社區活躍
  • 成熟穩定,經歷了大型項目的使用和考驗,並且不少開源項目都使用到了 Netty 好比咱們常常接觸的 Dubbo、RocketMQ 等等。
  • ......

使用 Netty 能作什麼?

這個應該是老鐵們最關心的一個問題了,憑藉本身的瞭解,簡單說一下,理論上 NIO 能夠作的事情 ,使用 Netty 均可以作而且更好。Netty 主要用來作網絡通訊 :

  1. 做爲 RPC 框架的網絡通訊工具 : 咱們在分佈式系統中,不一樣服務節點之間常常須要相互調用,這個時候就須要 RPC 框架了。不一樣服務指點的通訊是如何作的呢?可使用 Netty 來作。好比我調用另一個節點的方法的話,至少是要讓對方知道我調用的是哪一個類中的哪一個方法以及相關參數吧!
  2. 實現一個本身的 HTTP 服務器 :經過 Netty 咱們能夠本身實現一個簡單的 HTTP 服務器,這個你們應該不陌生。說到 HTTP 服務器的話,做爲 Java 後端開發,咱們通常使用 Tomcat 比較多。一個最基本的 HTTP 服務器可要以處理常見的 HTTP Method 的請求,好比 POST 請求、GET 請求等等。
  3. 實現一個即時通信系統 : 使用 Netty 咱們能夠實現一個能夠聊天相似微信的即時通信系統,這方面的開源項目還蠻多的,能夠自行去 Github 找一找。
  4. 消息推送系統 :市面上有不少消息推送系統都是基於 Netty 來作的。
  5. ......

哪些開源項目用到了 Netty?

咱們日常常常接觸的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。

能夠說大量的開源項目都用到了 Netty,因此掌握 Netty 有助於你更好的使用這些開源項目而且讓你有能力對其進行二次開發。

實際上還有不少不少優秀的項目用到了 Netty,Netty 官方也作了統計,統計結果在這裏:https://netty.io/wiki/related...

img

後記

RPC 框架源碼已經開源了,地址:https://github.com/Snailclimb...

相關文章
相關標籤/搜索