如何用Netty实现一个轻量级的HTTP代理服务器
為什么會想通過Netty構建一個HTTP代理服務器?這也是筆者發表這篇文章的目的所在。
其主要還是源于解決在日常開發測試過程中,一直困擾測試同學很久的一個問題,現在我就來具體闡述一下這個問題。
在日常開發測試過程中,為了確保上線項目的萬無一失,集成測試通常有部署,測試環境和回歸環境這兩套環境。開發人員根據需求編寫代碼模塊,自測通過之后,由測試的同學更新到測試環境,進行測試。如果測試通過,確定項目具備上線條件,后續會在回歸環境,進行回歸測試。回歸驗證通過的項目,才具備上線條件。
由于模塊的復雜性和多樣性,我們系統要跟外系統進行一些數據的交互,這通常是通過HTTP協議方式完成的。現在由于某些條件的限制,通常只有測試環境的網絡和端口是和外系統是打通的,回歸環境的這塊網絡鏈路是關閉的。這樣就產生了一個很尷尬的問題:如果一個模塊有跟外系統進行交互,回歸環境是不具備回歸條件的,這樣就要測試的同學,額外把模塊更新到測試環境來驗證,這樣不僅耗時耗力。并且由于測試環境和回歸環境系統數據的差異,往往可能導致項目的潛在風險沒有被及時地發現。
現在迫切希望有一個HTTP代理服務器,能夠路由回歸環境的請求到測試環境。更進一步地,如果能根據請求報文的某些關鍵字來過濾,決定最終路由的地址,這個當然是最好了。
基于這些因素,考慮到HTTP代理服務器的主要用途是轉發URL請求,可選的方案有很多種。比如Apache、Nginx等等。但是最終都沒有被采用,主要基于以下幾點考慮:
有沒有通過簡單的幾個配置,就可以達到目的的可行方案呢?
我首先想到了使用Netty這個NIO框架,來實現一個輕量級的HTTP代理轉發服務器,同時只要簡單地配置過濾規則,就可以實現請求的規則路由。
本文要求你熟悉Netty網絡框架的工作流程,基本原理。有興趣的朋友,可以認真研讀一下《Netty in Action》這本書,對提高Netty的功力有很大幫助。
言歸正傳,下面是這個HTTP代理轉發服務器的工作流程圖:
這里我簡單描述一下:
- 首先是Netty的服務端連接器(Acceptor)線程接收到HTTP請求,然后會把這個請求放入后端Netty專門負責處理I/O操作的線程池中。這個也是Netty經典的主從Reactor多線程模型的應用。
- I/O處理線程先對HTTP請求,調用HttpRequestDecoder解碼器進行解碼。
- HttpRequestDecoder把解碼的結果,通知給路由規則計算的核心模塊(GatewayServerHandler),核心模塊根據配置加上請求報文中的關鍵字,計算出要轉發的URL地址。
- 通過HTTP POST方式把請求,轉發給計算出來的URL地址。
- 獲取HTTP POST的獲得到的應答結果。
- 然后通過HttpResponseEncoder編碼器,把應答結果進行HTTP編碼,最后透傳給調用方。
流程描述很簡單,現在關鍵是,如何設計關鍵字路由規則配置模塊。
我是通過屬性配置文件(.properties)方式來實現的,主要有兩個配置文件。
- netty-gateway.properties配置文件,主要是用來描述URL中的路徑、以及其沒有和請求URL路徑匹配成功時,默認轉發的URL地址。
配置文件的配置參考說明:
#配置說明參考: #netty-gateway.config1.serverPath ==> URL路徑關鍵字。 #netty-gateway.config1.defaultAddr ==> 請求報文中的關鍵字沒有匹配成功時,默認轉發的URL地址。 #config的數字后綴順序遞增即可。netty-gateway.config1.serverPath=fcgi-bin/UIG_SFC_186 netty-gateway.config1.defaultAddr=http://10.46.158.10:8088/fcgi-bin/UIG_SFC_186netty-gateway.config2.serverPath=fcgi-bin/BSSP_SFC netty-gateway.config2.defaultAddr=http://10.46.158.10:8089/fcgi-bin/BSSP_SFC- netty-route.properties配置文件,則是主要配置URL中的路徑、請求報文關鍵字集合、以及請求的URL路徑、請求報文關鍵字和配置的匹配成功時,轉發的URL地址。
配置文件的配置參考說明:
#配置說明參考: #netty-gateway.config1.serverPath ==> URL路徑關鍵字。 #netty-gateway.config1.keyWord ==> 請求報文匹配關鍵字。支持1~N個關鍵字,多個關鍵字用逗號分割,關鍵字之間是邏輯與的關系。 #netty-gateway.config1.matchAddr ==> 請求報文匹配關鍵字匹配成功時,轉發的ULR地址。 #config的數字后綴順序遞增即可。netty-gateway.config1.serverPath=fcgi-bin/UIG_SFC_186 netty-gateway.config1.keyWord=1,2,3 netty-gateway.config1.matchAddr=http://10.46.158.20:8088/fcgi-bin/UIG_SFC_186netty-gateway.config2.serverPath=fcgi-bin/UIG_SFC_186 netty-gateway.config2.keyWord=1,2,3,4 netty-gateway.config2.matchAddr=http://10.46.158.20:8088/fcgi-bin/UIG_SFC_186netty-gateway.config3.serverPath=fcgi-bin/BSSP_SFC netty-gateway.config3.keyWord=HelloWorldNettyGateway netty-gateway.config3.matchAddr=http://10.46.158.20:8089/fcgi-bin/BSSP_SFC有了上述兩個基礎的配置信息之后,就可以實現基于Netty的關鍵字HTTP路由轉發服務器了。
這里主要說明關鍵代碼模塊的設計思路:
首先是GatewayAttribute類,它主要對應netty-gateway.properties配置文件的數據結構。
package com.newlandframework.gateway.commons;/*** @author tangjie<https://github.com/tang-jie>* @filename:GatewayAttribute.java* @description:GatewayAttribute功能模塊* @blogs http://www.cnblogs.com/jietang/* @since 2018/4/18*/ public class GatewayAttribute {private String serverPath;private String defaultAddr;public String getDefaultAddr() {return defaultAddr;}public void setDefaultAddr(String defaultAddr) {this.defaultAddr = defaultAddr;}public String getServerPath() {return serverPath;}public void setServerPath(String serverPath) {this.serverPath = serverPath;} }其次是RouteAttribute類,它主要對應netty-route.properties配置文件的數據結構。
package com.newlandframework.gateway.commons;/*** @author tangjie<https://github.com/tang-jie>* @filename:RouteAttribute.java* @description:RouteAttribute功能模塊* @blogs http://www.cnblogs.com/jietang/* @since 2018/4/18*/ public class RouteAttribute {private String serverPath;private String keyWord;private String matchAddr;public String getMatchAddr() {return matchAddr;}public void setMatchAddr(String matchAddr) {this.matchAddr = matchAddr;}public String getServerPath() {return serverPath;}public void setServerPath(String serverPath) {this.serverPath = serverPath;}public String getKeyWord() {return keyWord;}public void setKeyWord(String keyWord) {this.keyWord = keyWord;} }然后通過實現spring框架的BeanDefinitionRegistryPostProcessor接口,來實現配置文件的自動加載注入。對應代碼如下:
package com.newlandframework.gateway.commons;import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource;import java.io.IOException; import java.util.*;import static com.newlandframework.gateway.commons.GatewayOptions.*;/*** @author tangjie<https://github.com/tang-jie>* @filename:RoutingLoader.java* @description:RoutingLoader功能模塊* @blogs http://www.cnblogs.com/jietang/* @since 2018/4/18*/ public class RoutingLoader implements BeanDefinitionRegistryPostProcessor {public static final List<RouteAttribute> ROUTERS = new ArrayList<RouteAttribute>();public static final List<GatewayAttribute> GATEWAYS = new ArrayList<GatewayAttribute>();private static final List<String> KEY_ROUTERS = new ArrayList<String>();private static final List<String> KEY_GATEWAYS = new ArrayList<String>();@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {initGatewayRule(registry);initRouteRule(registry);}@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {GATEWAYS.clear();ROUTERS.clear();for (String beanName : KEY_GATEWAYS) {GATEWAYS.add(beanFactory.getBean(beanName, GatewayAttribute.class));}for (String beanName : KEY_ROUTERS) {ROUTERS.add(beanFactory.getBean(beanName, RouteAttribute.class));}}//加載netty-gateway.properties配置文件private void initGatewayRule(BeanDefinitionRegistry registry) {GenericBeanDefinition beanDefinition = new GenericBeanDefinition();Resource resource = new ClassPathResource(GATEWAY_OPTION_GATEWAY_CONFIG_FILE);Properties p = new Properties();try {p.load(resource.getInputStream());String key = null;String keyPrefix = null;String defaultAddr = null;String serverPath = null;Map<String, String> valuesMap = null;MutablePropertyValues mpv = null;for (Object obj : p.keySet()) {key = obj.toString();if (key.endsWith(GATEWAY_PROPERTIES_PREFIX_SERVER_PATH)) {keyPrefix = key.substring(0, key.indexOf(GATEWAY_PROPERTIES_PREFIX_SERVER_PATH));serverPath = p.getProperty(keyPrefix + GATEWAY_PROPERTIES_PREFIX_SERVER_PATH).trim();defaultAddr = p.getProperty(keyPrefix + GATEWAY_PROPERTIES_PREFIX_DEFAULT_ADDR).trim();valuesMap = new LinkedHashMap<String, String>();valuesMap.put(GATEWAY_PROPERTIES_DEFAULT_ADDR, defaultAddr);valuesMap.put(GATEWAY_PROPERTIES_SERVER_PATH, serverPath);mpv = new MutablePropertyValues(valuesMap);beanDefinition = new GenericBeanDefinition();beanDefinition.setBeanClass(GatewayAttribute.class);beanDefinition.setPropertyValues(mpv);registry.registerBeanDefinition(serverPath, beanDefinition);KEY_GATEWAYS.add(serverPath);}}} catch (IOException e) {e.printStackTrace();}}//加載netty-route.properties配置文件private void initRouteRule(BeanDefinitionRegistry registry) {GenericBeanDefinition beanDefinition = new GenericBeanDefinition();Resource resource = new ClassPathResource(GATEWAY_OPTION_ROUTE_CONFIG_FILE);Properties p = new Properties();try {p.load(resource.getInputStream());String key = null;String keyPrefix = null;String keyWord = null;String matchAddr = null;String serverPath = null;Map<String, String> valuesMap = null;MutablePropertyValues mpv = null;for (Object obj : p.keySet()) {key = obj.toString();if (key.endsWith(GATEWAY_PROPERTIES_PREFIX_KEY_WORD)) {keyPrefix = key.substring(0, key.indexOf(GATEWAY_PROPERTIES_PREFIX_KEY_WORD));keyWord = p.getProperty(keyPrefix + GATEWAY_PROPERTIES_PREFIX_KEY_WORD).trim();if (keyWord.isEmpty()) continue;matchAddr = p.getProperty(keyPrefix + GATEWAY_PROPERTIES_PREFIX_MATCH_ADDR).trim();serverPath = p.getProperty(keyPrefix + GATEWAY_PROPERTIES_PREFIX_SERVER_PATH).trim();valuesMap = new LinkedHashMap<String, String>();valuesMap.put(GATEWAY_PROPERTIES_KEY_WORD, keyWord);valuesMap.put(GATEWAY_PROPERTIES_MATCH_ADDR, matchAddr);valuesMap.put(GATEWAY_PROPERTIES_SERVER_PATH, serverPath);mpv = new MutablePropertyValues(valuesMap);beanDefinition = new GenericBeanDefinition();beanDefinition.setBeanClass(RouteAttribute.class);beanDefinition.setPropertyValues(mpv);String beanName = serverPath + GATEWAY_OPTION_SERVER_SPLIT + keyWord;registry.registerBeanDefinition(beanName, beanDefinition);KEY_ROUTERS.add(beanName);}}} catch (IOException e) {e.printStackTrace();}} }最后是重點的關鍵字過濾轉發代碼模塊,主要完成路由轉發地址的匹配計算、路由轉發、以及應答轉發結果給請求客戶端的工作。
import com.newlandframework.gateway.commons.GatewayAttribute; import com.newlandframework.gateway.commons.HttpClientUtils; import com.newlandframework.gateway.commons.RouteAttribute; import com.newlandframework.gateway.commons.RoutingLoader; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.*; import io.netty.util.Signal; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.GlobalEventExecutor; import org.springframework.util.StringUtils;import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit;import static com.newlandframework.gateway.commons.GatewayOptions.*; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;/*** @author tangjie<https://github.com/tang-jie>* @filename:GatewayServerHandler.java* @description:GatewayServerHandler功能模塊* @blogs http://www.cnblogs.com/jietang/* @since 2018/4/18*/ public class GatewayServerHandler extends SimpleChannelInboundHandler<Object> {private HttpRequest request;private StringBuilder buffer = new StringBuilder();private String url = "";private String uri = "";private StringBuilder respone;private GlobalEventExecutor executor = GlobalEventExecutor.INSTANCE;private CountDownLatch latch = new CountDownLatch(1);@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) {ctx.flush();}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, Object msg) {if (msg instanceof HttpRequest) {HttpRequest request = this.request = (HttpRequest) msg;//收到客戶端的100-Continue協議請求,說明客戶端要post數據給服務器if (HttpUtil.is100ContinueExpected(request)) {notify100Continue(ctx);}buffer.setLength(0);uri = request.uri().substring(1);}if (msg instanceof HttpContent) {HttpContent httpContent = (HttpContent) msg;ByteBuf content = httpContent.content();if (content.isReadable()) {buffer.append(content.toString(GATEWAY_OPTION_CHARSET));}//獲取post數據完畢if (msg instanceof LastHttpContent) {LastHttpContent trace = (LastHttpContent) msg;System.out.println("[NETTY-GATEWAY] REQUEST : " + buffer.toString());//根據netty-gateway.properties、netty-route.properties匹配出最終轉發的URL地址url = matchUrl();System.out.println("[NETTY-GATEWAY] URL : " + url);//http請求異步轉發處理,不要阻塞當前的Netty Handler的I/O線程,提高服務器的吞吐量。Future<StringBuilder> future = executor.submit(new Callable<StringBuilder>() {@Overridepublic StringBuilder call() {return HttpClientUtils.post(url, buffer.toString(), GATEWAY_OPTION_HTTP_POST);}});future.addListener(new FutureListener<StringBuilder>() {@Overridepublic void operationComplete(Future<StringBuilder> future) throws Exception {if (future.isSuccess()) {respone = ((StringBuilder) future.get(GATEWAY_OPTION_HTTP_POST, TimeUnit.MILLISECONDS));} else {respone = new StringBuilder(((Signal) future.cause()).name());}latch.countDown();}});try {latch.await();writeResponse(respone, future.isSuccess() ? trace : null, ctx);ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);} catch (InterruptedException e) {e.printStackTrace();}}}}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {cause.printStackTrace();ctx.close();}//根據netty-gateway.properties、netty-route.properties匹配出最終轉發的URL地址private String matchUrl() {for (GatewayAttribute gateway : RoutingLoader.GATEWAYS) {if (gateway.getServerPath().equals(uri)) {for (RouteAttribute route : RoutingLoader.ROUTERS) {if (route.getServerPath().equals(uri)) {String[] keys = StringUtils.delimitedListToStringArray(route.getKeyWord(), GATEWAY_OPTION_KEY_WORD_SPLIT);boolean match = true;for (String key : keys) {if (key.isEmpty()) continue;if (buffer.toString().indexOf(key.trim()) == -1) {match = false;break;}}if (match) {return route.getMatchAddr();}}}return gateway.getDefaultAddr();}}return GATEWAY_OPTION_LOCALHOST;}//把路由轉發的結果應答給http客戶端private void writeResponse(StringBuilder respone, HttpObject current, ChannelHandlerContext ctx) {if (respone != null) {boolean keepAlive = HttpUtil.isKeepAlive(request);FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, current == null ? OK : current.decoderResult().isSuccess() ? OK : BAD_REQUEST,Unpooled.copiedBuffer(respone.toString(), GATEWAY_OPTION_CHARSET));response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=GBK");if (keepAlive) {response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);}ctx.write(response);}}private static void notify100Continue(ChannelHandlerContext ctx) {FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE);ctx.write(response);} }這樣把整個工程maven打包部署運行,服務器默認啟動端口8999,你可以通過netty-gateway.xml的gatewayPort屬性進行配置調整。
控制臺打印出如下的信息,則說明服務器啟動成功。
下面繼續以一個實際的案例來說明一下,如何配置使用這個HTTP服務器。
NettyGateway代理轉發場景描述
- NettyGateway部署在10.1.1.76主機,URL中的路徑為:fcgi-bin/BSSP_SFC
- 如果請求報文中出現HelloWorldNettyGateway關鍵字的時候,轉發到http://10.46.158.20:8089/fcgi-bin/BSSP_SFC
- 否則轉發到http://10.46.158.10:8089/fcgi-bin/BSSP_SFC
NettyGateway代理轉發場景配置說明
- 配置文件netty-gateway.properties新增如下屬性:
- 配置文件netty-route.properties新增如下屬性:
NettyGateway代理轉發測試
- 發送HelloWorldNettyGateway到NettyGateway,關鍵字匹配成功,路由到http://10.46.158.20:8089/fcgi-bin/BSSP_SFC
- 發送Tangjie到NettyGateway,關鍵字匹配不成功,路由到默認的http://10.46.158.10:8089/fcgi-bin/BSSP_SFC
?
到此,整個基于Netty實現的,一個輕量級HTTP代理服務器的主要設計思路已經介紹完了。整個服務器實現代碼非常的少,而且通過簡單地配置,就能很好的滿足實際要求。相比通過“重量級”的服務器:Apache、Nginx,進行HTTP代理轉發而言,提供了另外一種解決問題的思路。在部門的實際部署運行中,這個Netty寫的小而精的服務器,運行良好,很好地幫助測試部門的同學,解決了一個困擾他們很久的問題。
俗話說得好,黑貓、白貓,抓到老鼠就是好貓。我把這個基于Netty的HTTP代理服務器取名“NettyGateway”,目前把代碼托管在github上面:https://github.com/tang-jie/NettyGateway。
有興趣的朋友,可以關注一下。由于技術能力所限,文中難免有紕漏和不足,大家如果有疑問,歡迎在下方的博客園評論區留言,或者通過github提issue給我。
最后,感謝您耐心閱讀。如果喜歡本文的話,可以點擊推薦,算是給我一個小小的鼓勵!謝謝大家。
?
附上本人曾經在博客園發表的,基于Netty框架實際應用的原創文章,有興趣的朋友,可以關聯閱讀。
基于Netty構建的RPC
談談如何使用Netty開發實現高性能的RPC服務器
Netty實現高性能RPC服務器優化篇之消息序列化
基于Netty打造RPC服務器設計經驗談
基于Netty構建的簡易消息隊列
Netty構建分布式消息隊列(AvatarMQ)設計指南之架構篇
Netty構建分布式消息隊列實現原理淺析
?
轉載于:https://www.cnblogs.com/jietang/p/8926325.html
總結
以上是生活随笔為你收集整理的如何用Netty实现一个轻量级的HTTP代理服务器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MPC5744p时钟模块
- 下一篇: js 获取 当天凌晨时间