Netty 实战:如何编写一个麻小俱全的 web 容器
?
逅弈(逅弈逐碼)| 作者?
?本文轉(zhuǎn)載自「逅弈逐碼」,歡迎關(guān)注
學(xué)習(xí) Netty 也有一段時(shí)間了,為了更好的掌握 Netty,我手動(dòng)造了輪子,一個(gè)基于 Netty 的 web 容器:redant,中文叫紅火蟻。創(chuàng)建這個(gè)項(xiàng)目的目的主要是學(xué)習(xí)使用 Netty,俗話說不要輕易的造輪子,但是通過造輪子我們可以學(xué)到很多優(yōu)秀開源框架的設(shè)計(jì)思路,編寫優(yōu)美的代碼,更好的提升自己。
PS:項(xiàng)目地址:https://github.com/all4you/redant
快速啟動(dòng)
Redant 是一個(gè)基于 Netty 的 Web 容器,類似 Tomcat 和 WebLogic 等容器
只需要啟動(dòng)一個(gè) Server,默認(rèn)的實(shí)現(xiàn)類是 NettyHttpServer 就能快速啟動(dòng)一個(gè) web 容器了,如下所示:
public final class ServerBootstrap {public static void main(String[] args) {Server nettyServer = new NettyHttpServer();// 各種初始化工作nettyServer.preStart();// 啟動(dòng)服務(wù)器nettyServer.start();} }我們可以直接啟動(dòng) redant-example 模塊中的 ServerBootstrap 類,因?yàn)?redant-example 中有很多示例的 Controller,我們直接運(yùn)行 example 中的 ServerBootstrap,啟動(dòng)后你會(huì)看到如下的日志信息:
在 redant-example 模塊中,內(nèi)置了以下幾個(gè)默認(rèn)的路由:
啟動(dòng)成功后,可以訪問 http://127.0.0.1:8888/ 查看效果,如下圖所示:
如果你可以看到 "Welcome to redant!" 這樣的消息,那就說明你啟動(dòng)成功了。
自定義路由
框架實(shí)現(xiàn)了自定義路由,通過 @Controller @Mapping 注解就可以唯一確定一個(gè)自定義路由。如下列的 UserController 所示:
和 Spring 的使用方式一樣,訪問 /user/list 來看下效果,如下圖所示:
結(jié)果渲染
目前支持 json、html、xml、text 等類型的結(jié)果渲染,用戶只需要在方法的 @Mapping 注解上通過 renderType 來指定具體的渲染類型即可,如果不指定的話,默認(rèn)以 json 類型返回。
如下圖所示,首頁就是通過指定 renderType 為 html 來返回一個(gè) html 頁面的:
IOC容器
從 UserController 的代碼中,我們看到 userServerce 對(duì)象是通過 @Autowired 注解自動(dòng)注入的,這個(gè)功能是任何一個(gè) IOC 容器基本的能力,下面我們來看看如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 IOC 容器。
首先定義一個(gè) 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); }然后我們需要在系統(tǒng)啟動(dòng)的時(shí)候,掃描出所有被 @Bean 注解修飾的類,然后對(duì)這些類進(jìn)行實(shí)例化,然后把實(shí)例化后的對(duì)象保存在一個(gè) Map 中即可,如下圖所示:
代碼很簡(jiǎn)單,通過在指定路徑下掃描出所有的類之后,把實(shí)例對(duì)象加入map中,但是對(duì)于已經(jīng)加入的 bean 不能繼續(xù)加入了,加入之后要獲取一個(gè) Bean 也很簡(jiǎn)單了,直接通過 name 到 map 中去獲取就可以了。
現(xiàn)在我們已經(jīng)把所有 @Bean 的對(duì)象管理起來了,那對(duì)于依賴到的其他的 bean 該如何注入呢,換句話說就是將我們實(shí)例化好的對(duì)象賦值給 @Autowired 注解修飾的變量。
簡(jiǎn)單點(diǎn)的做法就是遍歷 beanMap,然后對(duì)每個(gè) bean 進(jìn)行檢查,看這個(gè) bean 里面的每個(gè) setter 方法和屬性,如果有 @Autowired 注解,那就找到具體的 bean 實(shí)例之后將值塞進(jìn)去。
setter注入
field注入
通過Aware獲取BeanContext
BeanContext 已經(jīng)實(shí)現(xiàn)了,那怎么獲取 BeanContext 的實(shí)例呢?想到 Spring 中有很多的 Aware 接口,每種接口負(fù)責(zé)一種實(shí)例的回調(diào),比如我們想要獲取一個(gè) BeanFactory 那只要將我們的類實(shí)現(xiàn) BeanFactoryAware 接口就可以了,接口中的 setBeanFactory(BeanFactory factory) 方法參數(shù)中的 BeanFactory 實(shí)例就是我們所需要的,我們只要實(shí)現(xiàn)該方法,然后將參數(shù)中的實(shí)例保存在我們的類中,后續(xù)就可以直接使用了。
那現(xiàn)在我就來實(shí)現(xiàn)這樣的功能,首先定義一個(gè) Aware 接口,所有其他需要回調(diào)塞值的接口都繼承自該接口,如下所示:
public interface Aware {}public interface BeanContextAware extends Aware{/*** 設(shè)置BeanContext* @param beanContext BeanContext對(duì)象*/void setBeanContext(BeanContext beanContext); }接下來需要將 BeanContext 的實(shí)例注入到所有 BeanContextAware 的實(shí)現(xiàn)類中去。BeanContext 的實(shí)例很好得到,BeanContext 的實(shí)現(xiàn)類本身就是一個(gè) BeanContext 的實(shí)例,并且可以將該實(shí)例設(shè)置為單例,這樣的話所有需要獲取 BeanContext 的地方都可以獲取到同一個(gè)實(shí)例。
拿到 BeanContext 的實(shí)例后,我們就需要掃描出所有實(shí)現(xiàn)了 BeanContextAware 接口的類,并實(shí)例化這些類,然后調(diào)用這些類的 setBeanContext 方法,參數(shù)就傳我們拿到的 BeanContext 實(shí)例。
邏輯理清楚之后,實(shí)現(xiàn)起來就很簡(jiǎn)單了,如下圖所示:
Cookie管理
基本上所有的 web 容器都會(huì)有 cookie 管理的能力,那我們的 redant 也不能落后。首先定義一個(gè) 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); }其中我只列舉了幾個(gè)核心的方法,另外有一些不同參數(shù)的重載方法,這里就不詳細(xì)介紹了。最關(guān)鍵的是兩個(gè)方法,一個(gè)是讀 Cookie 一個(gè)是寫 Cookie 。
讀 Cookie
Netty 中是通過 HttpRequest 的 Header 來保存請(qǐng)求中所攜帶的 Cookie的,所以要讀取 Cookie 的話,最關(guān)鍵的是獲取到 HttpRequest。而 HttpRequest 可以在 ChannelHandler 中拿到,通過 HttpServerCodec 編解碼器,Netty 已經(jīng)幫我們把請(qǐng)求的數(shù)據(jù)轉(zhuǎn)換成 HttpRequest 了。但是這個(gè) HttpRequest 只在 ChannelHandler 中才能訪問到,而處理 Cookie 通常是用戶自定義的操作,并且對(duì)用戶來說他是不關(guān)心 HttpRequest 的,他只需要通過 CookieManager 去獲取一個(gè) Cookie 就行了。
這種情況下,最適合的就是將 HttpRequest 對(duì)象保存在一個(gè) ThreadLocal 中,在 CookieManager 中需要獲取的時(shí)候,直接到 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 就是那個(gè)通過 ThreadLocal 保存了 HttpRequest 的類。
寫 Cookie
寫 Cookie 和讀 Cookie 面臨著一樣的問題,就是寫的時(shí)候需要借助于 HttpResponse,將 Cookie 寫入 HttpResponse 的 Header 中去,但是用戶執(zhí)行寫 Cookie 操作的時(shí)候,根本就不關(guān)心 HttpResponse,甚至他在寫的時(shí)候,還沒有 HttpResponse。
這時(shí)的做法也是將需要寫到 HttpResponse 中的 Cookie 保存在 ThreadLocal 中,然后在最后通過 channel 寫響應(yīng)之前,將 Cookie 拿出來塞到 HttpResponse 中去即可,如下列代碼所示:
@Override public void setCookie(Cookie cookie) {TemporaryDataHolder.storeCookie(cookie); }/** * 響應(yīng)消息 */ private void writeResponse(){boolean close = isClose();response.headers().add(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(response.content().readableBytes()));// 從ThreadLocal中取出待寫入的cookieSet<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);} }攔截器
攔截器是一個(gè)框架很重要的功能,通過攔截器可以實(shí)現(xiàn)一些通用的工作,比如登錄鑒權(quán),事務(wù)處理等等。記得在 Servlet 的年代,攔截器是非常重要的一個(gè)功能,基本上每個(gè)系統(tǒng)都會(huì)在 web.xml 中配置很多的攔截器。
攔截器的基本思想是,通過一連串的類去執(zhí)行某個(gè)攔截的操作,一旦某個(gè)類中的攔截操作返回了 false,那就終止后面的所有流程,直接返回。
這種場(chǎng)景非常適合用責(zé)任鏈模式去實(shí)現(xiàn),而 Netty 的 pipeline 本身就是一個(gè)責(zé)任鏈模式的應(yīng)用,所以我們就可以通過 pipeline 來實(shí)現(xiàn)我們的攔截器。這里我定義了兩種類型的攔截器:前置攔截器和后置攔截器。
前置攔截器是在處理用戶的業(yè)務(wù)邏輯之前的一個(gè)攔截操作,如果該操作返回了 false 則直接 return,不會(huì)繼續(xù)執(zhí)行用戶的業(yè)務(wù)邏輯。
后置攔截器就有點(diǎn)不同了,后置攔截器主要就是處理一些后續(xù)的操作,因?yàn)楹笾脭r截器再跟前置攔截器一樣,當(dāng)操作返回了 false 直接 return 的話,已經(jīng)沒有意義了,因?yàn)闃I(yè)務(wù)邏輯已經(jīng)執(zhí)行完了。
理解清楚了具體的邏輯之后,實(shí)現(xiàn)起來就很簡(jiǎn)單了,如下列代碼所示:
前置攔截器
后置攔截器
有了實(shí)現(xiàn)之后,我們需要把他們加到 pipeline 中合適的位置,讓他們?cè)谡麄€(gè)責(zé)任鏈中生效,如下圖所示:
指定攔截器的執(zhí)行順序
目前攔截器還沒有實(shí)現(xiàn)指定順序執(zhí)行的功能,其實(shí)也很簡(jiǎn)單,可以定義一個(gè) @InterceptorOrder 的注解應(yīng)用在所有的攔截器的實(shí)現(xiàn)類上,掃描到攔截器的結(jié)果之后,根據(jù)該注解進(jìn)行排序,然后把拍完序之后的結(jié)果添加到 pipeline 中即可。
集群模式
到目前為止,我描述的都是單節(jié)點(diǎn)模式,如果哪一天單節(jié)點(diǎn)的性能無法滿足了,那就需要使用集群了,所以我也實(shí)現(xiàn)了集群模式。
集群模式是由一個(gè)主節(jié)點(diǎn)和若干個(gè)從節(jié)點(diǎn)構(gòu)成的。主節(jié)點(diǎn)接收到請(qǐng)求后,將請(qǐng)求轉(zhuǎn)發(fā)給從節(jié)點(diǎn)來處理,從節(jié)點(diǎn)把處理好的結(jié)果返回給主節(jié)點(diǎn),由主節(jié)點(diǎn)把結(jié)果響應(yīng)給請(qǐng)求。
要想實(shí)現(xiàn)集群模式需要有一個(gè)服務(wù)注冊(cè)和發(fā)現(xiàn)的功能,目前是借助于 Zk 來做的服務(wù)注冊(cè)與發(fā)現(xiàn)。
準(zhǔn)備一個(gè) Zk 服務(wù)端
因?yàn)橹鞴?jié)點(diǎn)需要把請(qǐng)求轉(zhuǎn)發(fā)給從節(jié)點(diǎn),所以主節(jié)點(diǎn)需要知道目前有哪些從節(jié)點(diǎn),我通過 ZooKeeper 來實(shí)現(xiàn)服務(wù)注冊(cè)與發(fā)現(xiàn)。
如果你沒有可用的 Zk 服務(wù)端的話,那你可以通過運(yùn)行下面的 Main 方法來啟動(dòng)一個(gè) ZooKeeper 服務(wù)端:
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);}} }這樣你就可以在后面啟動(dòng)主從節(jié)點(diǎn)的時(shí)候使用這個(gè) Zk 了。但是這并不是必須的,如果你已經(jīng)有一個(gè)正在運(yùn)行的 Zk 的服務(wù)端,那么你可以在啟動(dòng)主從節(jié)點(diǎn)的時(shí)候直接使用它,通過在 main 方法的參數(shù)中指定 Zk 的地址即可。
啟動(dòng)主節(jié)點(diǎn)
只需要運(yùn)行下面的代碼,就可以啟動(dòng)一個(gè)主節(jié)點(diǎn)了:
public class MasterServerBootstrap {public static void main(String[] args) {String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT);// 啟動(dòng)MasterServerServer masterServer = new MasterServer(zkAddress);masterServer.preStart();masterServer.start();} }如果在 main 方法的參數(shù)中指定了 Zk 的地址,就通過該地址去進(jìn)行服務(wù)發(fā)現(xiàn),否則會(huì)使用默認(rèn)的 Zk 地址。
啟動(dòng)從節(jié)點(diǎn)
只需要運(yùn)行下面的代碼,就可以啟動(dòng)一個(gè)從節(jié)點(diǎn)了:
public class SlaveServerBootstrap {public static void main(String[] args) {String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT);Node node = Node.getNodeWithArgs(args);// 啟動(dòng)SlaveServerServer slaveServer = new SlaveServer(zkAddress,node);slaveServer.preStart();slaveServer.start();} }如果在 main 方法的參數(shù)中指定了 Zk 的地址,就通過該地址去進(jìn)行服務(wù)注冊(cè),否則會(huì)使用默認(rèn)的 Zk 地址。
實(shí)際上多節(jié)點(diǎn)模式具體的處理邏輯還是復(fù)用了單節(jié)點(diǎn)模式的核心功能,只是把原本一臺(tái)實(shí)例擴(kuò)展到多臺(tái)實(shí)例而已。
總結(jié)
本文通過介紹一個(gè)基于 Netty 的 web 容器,讓我們了解了一個(gè) http 服務(wù)端的大概的構(gòu)成,當(dāng)然實(shí)現(xiàn)中可能有更加好的方法。但是主要的還是要了解內(nèi)在的思想,包括 Netty 的一些基本的使用方法。
我會(huì)繼續(xù)優(yōu)化該項(xiàng)目,加入更多的特性,例如服務(wù)發(fā)現(xiàn)與注冊(cè)當(dāng)前是通過 Zk 來實(shí)現(xiàn)的,未來可能會(huì)引入其他的組件去實(shí)現(xiàn)服務(wù)注冊(cè)與發(fā)現(xiàn)。
除此之外,Session 的管理還未完全實(shí)現(xiàn),后續(xù)也需要對(duì)這一塊進(jìn)行完善。
總結(jié)
以上是生活随笔為你收集整理的Netty 实战:如何编写一个麻小俱全的 web 容器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis 分布式缓存 Java 框架
- 下一篇: 如何向5岁小孩解释什么是支持向量机(SV