學習 Netty 也有一段時間了,爲了更好的掌握 Netty,我手動造了個輪子,一個基於 Netty 的 web 框架:redant,中文叫紅火蟻。建立這個項目的目的主要是學習使用 Netty,俗話說不要輕易的造輪子,可是經過造輪子咱們能夠學到不少優秀開源框架的設計思路,編寫優美的代碼,更好的提高本身。html
PS:項目地址:https://github.com/all4you/redant java
Redant 是一個基於 Netty 的 Web 容器,相似 Tomcat 和 WebLogic 等容器node
只須要啓動一個 Server,默認的實現類是 NettyHttpServer 就能快速啓動一個 web 容器了,以下所示:git
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,啓動後你會看到以下的日誌信息:github
在 redant-example 模塊中,內置瞭如下幾個默認的路由:web
啓動成功後,能夠訪問 http://127.0.0.1:8888/ 查看效果,以下圖所示:json
若是你能夠看到 "Welcome to redant!" 這樣的消息,那就說明你啓動成功了。服務器
框架實現了自定義路由,經過 @Controller @Mapping 註解就能夠惟一肯定一個自定義路由。以下列的 UserController 所示:cookie
和 Spring 的使用方式同樣,訪問 /user/list 來看下效果,以下圖所示:app
目前支持 json、html、xml、text 等類型的結果渲染,用戶只須要在 方法的 @Mapping 註解上經過 renderType 來指定具體的渲染類型便可,若是不指定的話,默認以 json 類型範圍。
以下圖所示,首頁就是經過指定 renderType 爲 html 來返回一個 html 頁面的:
從 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 中便可,以下圖所示:
代碼很簡單,經過在指定路徑下掃描出全部的類以後,把實例對象加入map中,可是對於已經加入的 bean 不能繼續加入了,加入以後要獲取一個 Bean 也很簡單了,直接經過 name 到 map 中去獲取就能夠了。
如今咱們已經把全部 @Bean 的對象管理起來了,那對於依賴到的其餘的 bean 該如何注入呢,換句話說就是將咱們實例化好的對象賦值給 @Autowired 註解修飾的變量。
簡單點的作法就是遍歷 beanMap,而後對每一個 bean 進行檢查,看這個 bean 裏面的每一個 setter 方法和屬性,若是有 @Autowired 註解,那就找到具體的 bean 實例以後將值塞進去。
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 實例。
邏輯理清楚以後,實現起來就很簡單了,以下圖所示:
基本上全部的 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 。
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 面臨着同樣的問題,就是寫的時候須要藉助於 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 的話,已經沒有意義了,由於業務邏輯已經執行完了。
理解清楚了具體的邏輯以後,實現起來就很簡單了,以下列代碼所示:
有了實現以後,咱們須要把他們加到 pipeline 中合適的位置,讓他們在整個責任鏈中生效,以下圖所示:
目前攔截器尚未實現指定順序執行的功能,其實也很簡單,能夠定義一個 @InterceptorOrder 的註解應用在全部的攔截器的實現類上,掃描到攔截器的結果以後,根據該註解進行排序,而後把拍完序以後的結果添加到 pipeline 中便可。
到目前爲止,我描述的都是單節點模式,若是哪一天單節點的性能沒法知足了,那就須要使用集羣了,因此我也實現了集羣模式。
集羣模式是由一個主節點和若干個從節點構成的。主節點接收到請求後,將請求轉發給從節點來處理,從節點把處理好的結果返回給主節點,由主節點把結果響應給請求。
要想實現集羣模式須要有一個服務註冊和發現的功能,目前是藉助於 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 的管理還未徹底實現,後續也須要對這一塊進行完善。