當前位置:
首頁 > 知識 > Netty 實戰:如何編寫一個麻小俱全的 web 框架

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

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

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

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 框架

啟動成功後,可以訪問 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 介面,如下所示:

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 介面,所有其他需要回調塞值的介面都繼承自該介面,如下所示:

public interface Aware {
}
public interface BeanContextAware extends Aware{
/**
* 設置BeanContext
* @param beanContext BeanContext對象
*/
void setBeanContext(BeanContext beanContext);
}

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

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

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

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

Cookie管理

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

public interface CookieManager {
Set<Cookie> getCookies();
Cookie getCookie(String name);

void addCookie(String name,String value);
void setCookie(Cookie cookie);
boolean deleteCookie(String name);
}

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

讀 Cookie

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

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

@Override
public 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 中去即可,如下列代碼所示:

@Override
public void setCookie(Cookie cookie) {
TemporaryDataHolder.storeCookie(cookie);
}
/**
* 響應消息
*/
private void writeResponse(){
boolean close = isClose();
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(response.content().readableBytes()));
// 從ThreadLocal中取出待寫入的cookie
Set<Cookie> cookies = TemporaryDataHolder.loadCookies();
if(!CollectionUtil.isEmpty(cookies)){
for(Cookie cookie : cookies){
// 將cookie寫入response中
response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
}
}
ChannelFuture future = channel.write(response);
if(close){
future.addListener(ChannelFutureListener.CLOSE);
}
}

攔截器

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

public final class ZkBootstrap {
private static final Logger LOGGER = LoggerFactory.getLogger(ZkBootstrap.class);
public static void main(String[] args) {
try {
ZkServer zkServer = new ZkServer();
zkServer.startStandalone(ZkConfig.DEFAULT);
}catch (Exception e){
LOGGER.error("ZkBootstrap start failed,cause:",e);
System.exit(1);
}
}
}

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

啟動主節點

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

public class MasterServerBootstrap {
public static void main(String[] args) {
String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT);
// 啟動MasterServer
Server masterServer = new MasterServer(zkAddress);
masterServer.preStart();
masterServer.start();
}
}

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

啟動從節點

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

public class SlaveServerBootstrap {
public static void main(String[] args) {
String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT);
Node node = Node.getNodeWithArgs(args);
// 啟動SlaveServer
Server slaveServer = new SlaveServer(zkAddress,node);
slaveServer.preStart();
slaveServer.start();
}
}

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

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

總結

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

我會繼續優化該項目,加入更多的特性,例如服務發現與註冊當前是通過 Zk 來實現的,未來可能會引入其他的組件去實現服務註冊與發現。

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

作者: 逅弈逐碼

原文:https://my.oschina.net/u/3216837/blog/3009802

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

EF Core的三種主要關係類型
大數據技術之Azkaban學習_(阿茲卡班)介紹 + 安裝部署 + 實戰

TAG:程序員小新人學習 |