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

逅弈逐碼發表於2019-02-01

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

PS:專案地址:

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

快速啟動

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

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

public final class ServerBootstrap {    public static void main(String[] args) {        Server nettyServer = new NettyHttpServer();        // 各種初始化工作        nettyServer.preStart();        // 啟動伺服器        nettyServer.start();    }}

我們可以直接啟動 redant-example 模組中的 ServerBootstrap 類,因為 redant-example 中有很多示例的 Controller,我們直接執行 example 中的 ServerBootstrap,啟動後你會看到如下的日誌資訊:

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

在 redant-example 模組中,內建了以下幾個預設的路由:

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

啟動成功後,可以訪問 檢視效果,如下圖所示:

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 介面,如下所示:

public interface BeanContext {    /**     * 獲得Bean     * @param name Bean的名稱     * @return Bean     */    Object getBean(String name);    /**     * 獲得Bean     * @param name Bean的名稱     * @param clazz Bean的類     * @param <T> 泛型     * @return Bean     */    <T> T getBean(String name,Class<T> clazz);}

然後我們需要在系統啟動的時候,掃描出所有被 @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 介面,所有其他需要回撥塞值的介面都繼承自該介面,如下所示:

  1. public interface Aware {


  2. }


  3. public interface BeanContextAware extends Aware{


  4.    /**

  5.     * 設定BeanContext

  6.     * @param beanContext BeanContext物件

  7.     */

  8.    void setBeanContext(BeanContext beanContext);

  9. }

接下來需要將 BeanContext 的例項注入到所有 BeanContextAware 的實現類中去。BeanContext 的例項很好得到,BeanContext 的實現類本身就是一個 BeanContext 的例項,並且可以將該例項設定為單例,這樣的話所有需要獲取 BeanContext 的地方都可以獲取到同一個例項。

拿到 BeanContext 的例項後,我們就需要掃描出所有實現了 BeanContextAware 介面的類,並例項化這些類,然後呼叫這些類的 setBeanContext 方法,引數就傳我們拿到的 BeanContext 例項。

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

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

Cookie管理

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

  1. public interface CookieManager {


  2.    Set<Cookie> getCookies();


  3.    Cookie getCookie(String name);


  4.    void addCookie(String name,String value);


  5.    void setCookie(Cookie cookie);


  6.    boolean deleteCookie(String name);


  7. }

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

讀 Cookie

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

這種情況下,最適合的就是將 HttpRequest 物件儲存在一個 ThreadLocal 中,在 CookieManager 中需要獲取的時候,直接到 ThreadLocal 中去取出來就可以了,如下列程式碼所示:

@Overridepublic Set<Cookie> getCookies() {    HttpRequest request = TemporaryDataHolder.loadHttpRequest();    Set<Cookie> cookies = new HashSet<>();    if(request != null) {        String value = request.headers().get(HttpHeaderNames.COOKIE);        if (value != null) {            cookies = ServerCookieDecoder.STRICT.decode(value);        }    }    return cookies;}

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

寫 Cookie

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

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

  1. @Override

  2. public void setCookie(Cookie cookie) {

  3.    TemporaryDataHolder.storeCookie(cookie);

  4. }


  5. /**

  6. * 響應訊息

  7. */

  8. private void writeResponse(){

  9.    boolean close = isClose();

  10.    response.headers().add(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(response.content().readableBytes()));

  11.    // 從ThreadLocal中取出待寫入的cookie

  12.    Set<Cookie> cookies = TemporaryDataHolder.loadCookies();

  13.    if(!CollectionUtil.isEmpty(cookies)){

  14.        for(Cookie cookie : cookies){

  15.            // 將cookie寫入response中

  16.            response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));

  17.        }

  18.    }

  19.    ChannelFuture future = channel.write(response);

  20.    if(close){

  21.        future.addListener(ChannelFutureListener.CLOSE);

  22.    }

  23. }

攔截器

攔截器是一個框架很重要的功能,透過攔截器可以實現一些通用的工作,比如登入鑑權,事務處理等等。記得在 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 服務端:

  1. public final class ZkBootstrap {

  2.    private static final Logger LOGGER = LoggerFactory.getLogger(ZkBootstrap.class);


  3.    public static void main(String[] args) {

  4.        try {

  5.            ZkServer zkServer = new ZkServer();

  6.            zkServer.startStandalone(ZkConfig.DEFAULT);

  7.        }catch (Exception e){

  8.            LOGGER.error("ZkBootstrap start failed,cause:",e);

  9.            System.exit(1);

  10.        }

  11.    }

  12. }

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

啟動主節點

只需要執行下面的程式碼,就可以啟動一個主節點了:

  1. public class MasterServerBootstrap {

  2.    public static void main(String[] args) {

  3.        String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT);


  4.        // 啟動MasterServer

  5.        Server masterServer = new MasterServer(zkAddress);

  6.        masterServer.preStart();

  7.        masterServer.start();

  8.    }

  9. }

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

啟動從節點

只需要執行下面的程式碼,就可以啟動一個從節點了:

  1. public class SlaveServerBootstrap {


  2.    public static void main(String[] args) {

  3.        String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT);

  4.        Node node = Node.getNodeWithArgs(args);


  5.        // 啟動SlaveServer

  6.        Server slaveServer = new SlaveServer(zkAddress,node);

  7.        slaveServer.preStart();

  8.        slaveServer.start();

  9.    }


  10. }

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

實際上多節點模式具體的處理邏輯還是複用了單節點模式的核心功能,只是把原本一臺例項擴充套件到多臺例項而已。

總結

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

我會繼續最佳化該專案,加入更多的特性,例如服務發現與註冊當前是透過 Zk 來實現的,未來可能會引入其他的元件去實現服務註冊與發現。

除此之外,Session 的管理還未完全實現,後續也需要對這一塊進行完善。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69908605/viewspace-2565385/,如需轉載,請註明出處,否則將追究法律責任。

相關文章