天天看點

Netty 實戰:如何編寫一個麻小俱全的 web 容器

學習 Netty 也有一段時間了,為了更好的掌握 Netty,我手動造了輪子,一個基于 Netty 的 web 容器:redant,中文叫紅火蟻。建立這個項目的目的主要是學習使用 Netty,俗話說不要輕易的造輪子,但是通過造輪子我們可以學到很多優秀開源架構的設計思路,編寫優美的代碼,更好的提升自己。

PS:項目位址:https://github.com/all4you/redant

Netty 實戰:如何編寫一個麻小俱全的 web 容器

快速啟動

Redant 是一個基于 Netty 的 Web 容器,類似 Tomcat 和 WebLogic 等容器

隻需要啟動一個 Server,預設的實作類是 NettyHttpServer 就能快速啟動一個 web 容器了,如下所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

我們可以直接啟動 redant-example 子產品中的 ServerBootstrap 類,因為 redant-example 中有很多示例的 Controller,我們直接運作 example 中的 ServerBootstrap,啟動後你會看到如下的日志資訊:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

在 redant-example 子產品中,内置了以下幾個預設的路由:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

啟動成功後,可以通路 http://127.0.0.1:8888/ 檢視效果,如下圖所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

如果你可以看到 “Welcome to redant!” 這樣的消息,那就說明你啟動成功了。

自定義路由

架構實作了自定義路由,通過 @Controller @Mapping 注解就可以唯一确定一個自定義路由。如下列的 UserController 所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

和 Spring 的使用方式一樣,通路 /user/list 來看下效果,如下圖所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

結果渲染

目前支援 json、html、xml、text 等類型的結果渲染,使用者隻需要在 方法的 @Mapping 注解上通過 renderType 來指定具體的渲染類型即可,如果不指定的話,預設以 json 類型範圍。

如下圖所示,首頁就是通過指定 renderType 為 html 來傳回一個 html 頁面的:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

IOC容器

從 UserController 的代碼中,我們看到 userServerce 對象是通過 @Autowired 注解自動注入的,這個功能是任何一個 IOC 容器基本的能力,下面我們來看看如何實作一個簡單的 IOC 容器。

首先定義一個 BeanContext 接口,如下所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

然後我們需要在系統啟動的時候,掃描出所有被 @Bean 注解修飾的類,然後對這些類進行執行個體化,然後把執行個體化後的對象儲存在一個 Map 中即可,如下圖所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

代碼很簡單,通過在指定路徑下掃描出所有的類之後,把執行個體對象加入map中,但是對于已經加入的 bean 不能繼續加入了,加入之後要擷取一個 Bean 也很簡單了,直接通過 name 到 map 中去擷取就可以了。

現在我們已經把所有 @Bean 的對象管理起來了,那對于依賴到的其他的 bean 該如何注入呢,換句話說就是将我們執行個體化好的對象指派給 @Autowired 注解修飾的變量。

簡單點的做法就是周遊 beanMap,然後對每個 bean 進行檢查,看這個 bean 裡面的每個 setter 方法和屬性,如果有 @Autowired 注解,那就找到具體的 bean 執行個體之後将值塞進去。

setter注入

Netty 實戰:如何編寫一個麻小俱全的 web 容器

field注入

Netty 實戰:如何編寫一個麻小俱全的 web 容器

通過Aware擷取BeanContext

BeanContext 已經實作了,那怎麼擷取 BeanContext 的執行個體呢?想到 Spring 中有很多的 Aware 接口,每種接口負責一種執行個體的回調,比如我們想要擷取一個 BeanFactory 那隻要将我們的類實作 BeanFactoryAware 接口就可以了,接口中的 setBeanFactory(BeanFactory factory) 方法參數中的 BeanFactory 執行個體就是我們所需要的,我們隻要實作該方法,然後将參數中的執行個體儲存在我們的類中,後續就可以直接使用了。

那現在我就來實作這樣的功能,首先定義一個 Aware 接口,所有其他需要回調塞值的接口都繼承自該接口,如下所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

接下來需要将 BeanContext 的執行個體注入到所有 BeanContextAware 的實作類中去。BeanContext 的執行個體很好得到,BeanContext 的實作類本身就是一個 BeanContext 的執行個體,并且可以将該執行個體設定為單例,這樣的話所有需要擷取 BeanContext 的地方都可以擷取到同一個執行個體。

拿到 BeanContext 的執行個體後,我們就需要掃描出所有實作了 BeanContextAware 接口的類,并執行個體化這些類,然後調用這些類的 setBeanContext 方法,參數就傳我們拿到的 BeanContext 執行個體。

邏輯理清楚之後,實作起來就很簡單了,如下圖所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

Cookie管理基本上所有的 web 容器都會有 cookie 管理的能力,那我們的 redant 也不能落後。首先定義一個 CookieManager 的接口,核心的操作 cookie 的方法如下:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

其中我隻列舉了幾個核心的方法,另外有一些不同參數的重載方法,這裡就不詳細介紹了。最關鍵的是兩個方法,一個是讀 Cookie 一個是寫 Cookie 。

讀 Cookie

Netty 中是通過 HttpRequest 的 Header 來儲存請求中所攜帶的 Cookie的,是以要讀取 Cookie 的話,最關鍵的是擷取到 HttpRequest。而 HttpRequest 可以在 ChannelHandler 中拿到,通過 HttpServerCodec 編解碼器,Netty 已經幫我們把請求的資料轉換成 HttpRequest 了。但是這個 HttpRequest 隻在 ChannelHandler 中才能通路到,而處理 Cookie 通常是使用者自定義的操作,并且對使用者來說他是不關心 HttpRequest 的,他隻需要通過 CookieManager 去擷取一個 Cookie 就行了。

這種情況下,最适合的就是将 HttpRequest 對象儲存在一個 ThreadLocal 中,在 CookieManager 中需要擷取的時候,直接到 ThreadLocal 中去取出來就可以了,如下列代碼所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

TemporaryDataHolder 就是那個通過 ThreadLocal 儲存了 HttpRequest 的類。

寫 Cookie

寫 Cookie 和讀 Cookie 面臨着一樣的問題,就是寫的時候需要借助于 HttpResponse,将 Cookie 寫入 HttpResponse 的 Header 中去,但是使用者執行寫 Cookie 操作的時候,根本就不關心 HttpResponse,甚至他在寫的時候,還沒有 HttpResponse。

這時的做法也是将需要寫到 HttpResponse 中的 Cookie 儲存在 ThreadLocal 中,然後在最後通過 channel 寫響應之前,将 Cookie 拿出來塞到 HttpResponse 中去即可,如下列代碼所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

攔截器

攔截器是一個架構很重要的功能,通過攔截器可以實作一些通用的工作,比如登入鑒權,事務處理等等。記得在 Servlet 的年代,攔截器是非常重要的一個功能,基本上每個系統都會在 web.xml 中配置很多的攔截器。

攔截器的基本思想是,通過一連串的類去執行某個攔截的操作,一旦某個類中的攔截操作傳回了 false,那就終止後面的所有流程,直接傳回。

這種場景非常适合用責任鍊模式去實作,而 Netty 的 pipeline 本身就是一個責任鍊模式的應用,是以我們就可以通過 pipeline 來實作我們的攔截器。這裡我定義了兩種類型的攔截器:前置攔截器和後置攔截器。

前置攔截器是在處理使用者的業務邏輯之前的一個攔截操作,如果該操作傳回了 false 則直接 return,不會繼續執行使用者的業務邏輯。

後置攔截器就有點不同了,後置攔截器主要就是處理一些後續的操作,因為後置攔截器再跟前置攔截器一樣,當操作傳回了 false 直接 return 的話,已經沒有意義了,因為業務邏輯已經執行完了。

了解清楚了具體的邏輯之後,實作起來就很簡單了,如下列代碼所示:

前置攔截器

Netty 實戰:如何編寫一個麻小俱全的 web 容器

後置攔截器

Netty 實戰:如何編寫一個麻小俱全的 web 容器

有了實作之後,我們需要把他們加到 pipeline 中合适的位置,讓他們在整個責任鍊中生效,如下圖所示:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

指定攔截器的執行順序

目前攔截器還沒有實作指定順序執行的功能,其實也很簡單,可以定義一個 @InterceptorOrder 的注解應用在所有的攔截器的實作類上,掃描到攔截器的結果之後,根據該注解進行排序,然後把拍完序之後的結果添加到 pipeline 中即可。

叢集模式

到目前為止,我描述的都是單節點模式,如果哪一天單節點的性能無法滿足了,那就需要使用叢集了,是以我也實作了叢集模式。

叢集模式是由一個主節點和若幹個從節點構成的。主節點接收到請求後,将請求轉發給從節點來處理,從節點把處理好的結果傳回給主節點,由主節點把結果響應給請求。

要想實作叢集模式需要有一個服務注冊和發現的功能,目前是借助于 Zk 來做的服務注冊與發現。

準備一個 Zk 服務端

因為主節點需要把請求轉發給從節點,是以主節點需要知道目前有哪些從節點,我通過 ZooKeeper 來實作服務注冊與發現。

如果你沒有可用的 Zk 服務端的話,那你可以通過運作下面的 Main 方法來啟動一個 ZooKeeper 服務端:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

這樣你就可以在後面啟動主從節點的時候使用這個 Zk 了。但是這并不是必須的,如果你已經有一個正在運作的 Zk 的服務端,那麼你可以在啟動主從節點的時候直接使用它,通過在 main 方法的參數中指定 Zk 的位址即可。

啟動主節點

隻需要運作下面的代碼,就可以啟動一個主節點了:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

如果在 main 方法的參數中指定了 Zk 的位址,就通過該位址去進行服務發現,否則會使用預設的 Zk 位址。

啟動從節點

隻需要運作下面的代碼,就可以啟動一個從節點了:

Netty 實戰:如何編寫一個麻小俱全的 web 容器

如果在 main 方法的參數中指定了 Zk 的位址,就通過該位址去進行服務注冊,否則會使用預設的 Zk 位址。

實際上多節點模式具體的處理邏輯還是複用了單節點模式的核心功能,隻是把原本一台執行個體擴充到多台執行個體而已。

總結

本文通過介紹一個基于 Netty 的 web 容器,讓我們了解了一個 http 服務端的大概的構成,當然實作中可能有更加好的方法。但是主要的還是要了解内在的思想,包括 Netty 的一些基本的使用方法。