手寫一套迷你版HTTP服務器


本文主要介紹如何通過netty來手寫一套簡單版的HTTP服務器,同時將關於netty的許多細小知識點進行了串聯,用於鞏固和提升對於netty框架的掌握程度。

服務器運行效果

服務器支持對靜態文件css,js,html,圖片資源的訪問。通過網絡的形式對這些文件可以進行訪問,相應截圖如下所示:

手寫一套迷你版HTTP服務器


支持對於js,css,html等文件的訪問:

 

手寫一套迷你版HTTP服務器

 

手寫一套迷你版HTTP服務器


然后引用相應的pom依賴文件信息:

 1         <dependency>
 2            <groupId>com.alibaba</groupId>
 3             <artifactId>fastjson</artifactId>
 4             <version>1.2.47</version>
 5         </dependency>
 6 
 7         <dependency>
 8             <groupId>org.projectlombok</groupId>
 9             <artifactId>lombok</artifactId>
10             <optional>true</optional>
11         </dependency>
12 
13         <dependency>
14             <groupId>io.netty</groupId>
15             <artifactId>netty-all</artifactId>
16             <version>4.1.6.Final</version>
17         </dependency>
18 
19         <dependency>
20             <groupId>org.slf4j</groupId>
21             <artifactId>slf4j-api</artifactId>
22             <version>1.7.13</version>
23         </dependency>
24 
25         <dependency>
26             <groupId>cglib</groupId>
27             <artifactId>cglib</artifactId>
28             <version>3.2.6</version>
29         </dependency>

 

導入依賴之后,新建一個包itree.demo(包名可以自己隨便定義)

定義一個啟動類WebApplication.java(有點類似於springboot的那種思路)

 1 package itree.demo;
 2 
 3 import com.sise.itree.ITreeApplication;
 4 
 5 /**
 6  * @author idea
 7  * @data 2019/4/30
 8  */
 9 public class WebApplication {
10 
11     public static void main(String[] args) throws IllegalAccessException, InstantiationException {
12         ITreeApplication.start(WebApplication.class);
13     }
14 }

 

在和這個啟動類同級別的包底下,建立itree.demo.controller和itree.demo.filter包,主要是用於做測試:

建立一個測試使用的Controller:

 1 package itree.demo.controller;
 2 
 3 import com.sise.itree.common.BaseController;
 4 import com.sise.itree.common.annotation.ControllerMapping;
 5 import com.sise.itree.core.handle.response.BaseResponse;
 6 import com.sise.itree.model.ControllerRequest;
 7 
 8 /**
 9  * @author idea
10  * @data 2019/4/30
11  */
12 @ControllerMapping(url = "/myController")
13 public class MyController implements BaseController {
14 
15     @Override
16     public BaseResponse doGet(ControllerRequest controllerRequest) {
17         String username= (String) controllerRequest.getParameter("username");
18         System.out.println(username);
19         return new BaseResponse(1,username);
20     }
21 
22     @Override
23     public BaseResponse doPost(ControllerRequest controllerRequest) {
24         return null;
25     }
26 }

 

這里面的BaseController是我自己在Itree包里面編寫的接口,這里面的格式有點類似於javaee的servlet,之前我在編寫代碼的時候有點參考了servlet的設計。(注解里面的url正是匹配了客戶端訪問時候所映射的url鏈接)

編寫相應的過濾器:

 1 package itree.demo.filter;
 2 
 3 import com.sise.itree.common.BaseFilter;
 4 import com.sise.itree.common.annotation.Filter;
 5 import com.sise.itree.model.ControllerRequest;
 6 
 7 /**
 8  * @author idea
 9  * @data 2019/4/30
10  */
11 @Filter(order = 1)
12 public class MyFilter implements BaseFilter {
13 
14     @Override
15     public void beforeFilter(ControllerRequest controllerRequest) {
16         System.out.println("before");
17     }
18 
19     @Override
20     public void afterFilter(ControllerRequest controllerRequest) {
21         System.out.println("after");
22     }
23 }

 

通過代碼的表面意思,可以很好的理解這里大致的含義。當然,如果過濾器有優先順序的話,可以通過@Filter注解里面的order屬性進行排序。搭建起多個controller和filter之后,整體項目的結構如下所示:

手寫一套迷你版HTTP服務器


基礎的java程序寫好之后,便是相應的resources文件了:
這里提供了可適配性的配置文件,默認配置文件命名為resources的config/itree-config.properties文件:

 

暫時可提供的配置有以下幾個:

server.port=9090
index.page=html/home.html
not.found.page=html/404.html

結合相應的靜態文件放入之后,整體的項目結構圖如下所示:

手寫一套迷你版HTTP服務器

這個時候可以啟動之前編寫的WebApplication啟動類

 

啟動的時候控制台會打印出相應的信息:

手寫一套迷你版HTTP服務器

啟動類會掃描同級目錄底下所有帶有@Filter注解和@ControllerMapping注解的類,然后加入指定的容器當中。(這里借鑒了Spring里面的ioc容器的思想)

啟動之后,進行對於上述controller接口的訪問測試,便可以查看到以下信息的內容:

手寫一套迷你版HTTP服務器


同樣,我們查看控制台的信息打印:

 

手寫一套迷你版HTTP服務器


controller接收數據之前,通過了三層的filter進行過濾,而且過濾的順序也是和我們之前預期所想的那樣一直,按照order從小到大的順序執行(同樣我們可以接受post類型的請求)

 

除了常規的接口類型數據響應之外,還提供有靜態文件的訪問功能:

手寫一套迷你版HTTP服務器


對於靜態文件里面的html也可以通過網絡url的形式來訪問:


home.html文件內容如下所示:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6 </head>
 7 <body>
 8 this is home
 9 </body>
10 </html>

 

我們在之前說的properties文件里面提及了相應的初始化頁面配置是:

index.page=html/home.html

因此,訪問的時候默認的http://localhost:9090/就會跳轉到該指定頁面:

手寫一套迷你版HTTP服務器


假設不配置properties文件的話,則會采用默認的頁面跳轉,默認的端口號8080

 

手寫一套迷你版HTTP服務器


默認的404頁面為

 

手寫一套迷你版HTTP服務器


基本的使用步驟大致如上述所示。

那么又該怎么來進行這樣的一套框架設計和編寫呢?

首先從整體設計方面,核心內容是分為了netty的server和serverHandler處理器:

首先是接受數據的server端:

 1 import io.netty.bootstrap.ServerBootstrap;
 2 import io.netty.channel.ChannelFuture;
 3 import io.netty.channel.ChannelInitializer;
 4 import io.netty.channel.EventLoopGroup;
 5 import io.netty.channel.nio.NioEventLoopGroup;
 6 import io.netty.channel.socket.SocketChannel;
 7 import io.netty.channel.socket.nio.NioServerSocketChannel;
 8 import io.netty.handler.codec.http.HttpObjectAggregator;
 9 import io.netty.handler.codec.http.HttpRequestDecoder;
10 import io.netty.handler.codec.http.HttpResponseEncoder;
11 import io.netty.handler.stream.ChunkedWriteHandler;
12 
13 /**
14  * @author idea
15  * @data 2019/4/26
16  */
17 public class NettyHttpServer {
18 
19     private int inetPort;
20 
21     public NettyHttpServer(int inetPort) {
22         this.inetPort = inetPort;
23     }
24 
25     public int getInetPort() {
26         return inetPort;
27     }
28 
29 
30     public void init() throws Exception {
31 
32         EventLoopGroup parentGroup = new NioEventLoopGroup();
33         EventLoopGroup childGroup = new NioEventLoopGroup();
34 
35         try {
36             ServerBootstrap server = new ServerBootstrap();
37             // 1. 綁定兩個線程組分別用來處理客戶端通道的accept和讀寫時間
38             server.group(parentGroup, childGroup)
39                     // 2. 綁定服務端通道NioServerSocketChannel
40                     .channel(NioServerSocketChannel.class)
41                     // 3. 給讀寫事件的線程通道綁定handler去真正處理讀寫
42                     // ChannelInitializer初始化通道SocketChannel
43                     .childHandler(new ChannelInitializer<SocketChannel>() {
44                         @Override
45                         protected void initChannel(SocketChannel socketChannel) throws Exception {
46                             // 請求解碼器
47                             socketChannel.pipeline().addLast("http-decoder", new HttpRequestDecoder());
48                             // 將HTTP消息的多個部分合成一條完整的HTTP消息
49                             socketChannel.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65535));
50                             // 響應轉碼器
51                             socketChannel.pipeline().addLast("http-encoder", new HttpResponseEncoder());
52                             // 解決大碼流的問題,ChunkedWriteHandler:向客戶端發送HTML5文件
53                             socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
54                             // 自定義處理handler
55                             socketChannel.pipeline().addLast("http-server", new NettyHttpServerHandler());
56                         }
57                     });
58 
59             // 4. 監聽端口(服務器host和port端口),同步返回
60             ChannelFuture future = server.bind(this.inetPort).sync();
61             System.out.println("[server] opening in "+this.inetPort);
62             // 當通道關閉時繼續向后執行,這是一個阻塞方法
63             future.channel().closeFuture().sync();
64         } finally {
65             childGroup.shutdownGracefully();
66             parentGroup.shutdownGracefully();
67         }
68     }
69 
70 }

 

Netty接收數據的處理器NettyHttpServerHandler 代碼如下:

  1 import com.alibaba.fastjson.JSON;
  2 import com.sise.itree.common.BaseController;
  3 import com.sise.itree.model.ControllerRequest;
  4 import com.sise.itree.model.PicModel;
  5 import io.netty.buffer.ByteBuf;
  6 import io.netty.channel.ChannelFutureListener;
  7 import io.netty.channel.ChannelHandlerContext;
  8 import io.netty.channel.SimpleChannelInboundHandler;
  9 import io.netty.handler.codec.http.FullHttpRequest;
 10 import io.netty.handler.codec.http.FullHttpResponse;
 11 import io.netty.handler.codec.http.HttpMethod;
 12 import io.netty.handler.codec.http.HttpResponseStatus;
 13 import io.netty.util.CharsetUtil;
 14 import com.sise.itree.core.handle.StaticFileHandler;
 15 import com.sise.itree.core.handle.response.BaseResponse;
 16 import com.sise.itree.core.handle.response.ResponCoreHandle;
 17 import com.sise.itree.core.invoke.ControllerCglib;
 18 import lombok.extern.slf4j.Slf4j;
 19 
 20 import java.lang.reflect.Method;
 21 import java.util.HashMap;
 22 import java.util.Map;
 23 
 24 import static io.netty.buffer.Unpooled.copiedBuffer;
 25 import static com.sise.itree.core.ParameterHandler.getHeaderData;
 26 import static com.sise.itree.core.handle.ControllerReactor.getClazzFromList;
 27 import static com.sise.itree.core.handle.FilterReactor.aftHandler;
 28 import static com.sise.itree.core.handle.FilterReactor.preHandler;
 29 import static com.sise.itree.util.CommonUtil.*;
 30 
 31 /**
 32  * @author idea
 33  * @data 2019/4/26
 34  */
 35 @Slf4j
 36 public class NettyHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
 37 
 38     @Override
 39     protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws Exception {
 40         String uri = getUri(fullHttpRequest.getUri());
 41         Object object = getClazzFromList(uri);
 42         String result = "recive msg";
 43         Object response = null;
 44 
 45         //靜態文件處理
 46         response = StaticFileHandler.responseHandle(object, ctx, fullHttpRequest);
 47 
 48         if (!(response instanceof FullHttpResponse) && !(response instanceof PicModel)) {
 49 
 50             //接口處理
 51             if (isContaionInterFace(object, BaseController.class)) {
 52                 ControllerCglib cc = new ControllerCglib();
 53                 Object proxyObj = cc.getTarget(object);
 54                 Method[] methodArr = null;
 55                 Method aimMethod = null;
 56 
 57 
 58                 if (fullHttpRequest.method().equals(HttpMethod.GET)) {
 59                     methodArr = proxyObj.getClass().getMethods();
 60                     aimMethod = getMethodByName(methodArr, "doGet");
 61                 } else if (fullHttpRequest.method().equals(HttpMethod.POST)) {
 62                     methodArr = proxyObj.getClass().getMethods();
 63                     aimMethod = getMethodByName(methodArr, "doPost");
 64                 }
 65 
 66                 //代理執行method
 67                 if (aimMethod != null) {
 68                     ControllerRequest controllerRequest=paramterHandler(fullHttpRequest);
 69                     preHandler(controllerRequest);
 70                     BaseResponse baseResponse = (BaseResponse) aimMethod.invoke(proxyObj, controllerRequest);
 71                     aftHandler(controllerRequest);
 72                     result = JSON.toJSONString(baseResponse);
 73                 }
 74             }
 75             response = ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);
 76         }
 77         ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
 78     }
 79 
 80 
 81     @Override
 82     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
 83         cause.printStackTrace();
 84     }
 85 
 86 
 87     /**
 88      * 處理請求的參數內容
 89      *
 90      * @param fullHttpRequest
 91      * @return
 92      */
 93     private ControllerRequest paramterHandler(FullHttpRequest fullHttpRequest) {
 94         //參數處理部分內容
 95         Map<String, Object> paramMap = new HashMap<>(60);
 96         if (fullHttpRequest.method() == HttpMethod.GET) {
 97             paramMap = ParameterHandler.getGetParamsFromChannel(fullHttpRequest);
 98         } else if (fullHttpRequest.getMethod() == HttpMethod.POST) {
 99             paramMap = ParameterHandler.getPostParamsFromChannel(fullHttpRequest);
100         }
101         Map<String, String> headers = getHeaderData(fullHttpRequest);
102 
103         ControllerRequest ctr = new ControllerRequest();
104         ctr.setParams(paramMap);
105         ctr.setHeader(headers);
106         return ctr;
107     }
108 
109 
110 }

 

這里面的核心模塊我大致分成了:

  • url匹配

  • 從容器獲取響應數據

  • 靜態文件響應處理

  • 接口請求響應處理四個步驟

url匹配處理:

我們的客戶端發送的url請求進入server端之后,需要快速的進行url路徑的格式處理。例如將http://localhost:8080/xxx-1/xxx-2?username=test轉換為/xxx-1/xxx-2的格式,這樣方便和controller頂部設計的注解的url信息進行關鍵字匹配。

 1 /**
 2      * 截取url里面的路徑字段信息
 3      *
 4      * @param uri
 5      * @return
 6      */
 7     public static String getUri(String uri) {
 8         int pathIndex = uri.indexOf("/");
 9         int requestIndex = uri.indexOf("?");
10         String result;
11         if (requestIndex < 0) {
12             result = uri.trim().substring(pathIndex);
13         } else {
14             result = uri.trim().substring(pathIndex, requestIndex);
15         }
16         return result;
17     }

 

從容器獲取匹配響應數據:

經過了前一段的url格式處理之后,我們需要根據url的后綴來預先判斷是否是數據靜態文件的請求:

對於不同后綴格式來返回不同的model對象(每個model對象都是共同的屬性url),之所以設計成不同的對象是因為針對不同格式的數據,response的header里面需要設置不同的屬性值。

 

 1  /**
 2      * 匹配響應信息
 3      *
 4      * @param uri
 5      * @return
 6      */
 7     public static Object getClazzFromList(String uri) {
 8         if (uri.equals("/") || uri.equalsIgnoreCase("/index")) {
 9             PageModel pageModel;
10             if(ITreeConfig.INDEX_CHANGE){
11                 pageModel= new PageModel();
12                 pageModel.setPagePath(ITreeConfig.INDEX_PAGE);
13             }
14             return new PageModel();
15         }
16         if (uri.endsWith(RequestConstants.HTML_TYPE)) {
17             return new PageModel(uri);
18         }
19         if (uri.endsWith(RequestConstants.JS_TYPE)) {
20             return new JsModel(uri);
21         }
22         if (uri.endsWith(RequestConstants.CSS_TYPE)) {
23             return new CssModel(uri);
24         }
25         if (isPicTypeMatch(uri)) {
26             return new PicModel(uri);
27         }
28 
29         //查看是否是匹配json格式
30         Optional<ControllerMapping> cmOpt = CONTROLLER_LIST.stream().filter((p) -> p.getUrl().equals(uri)).findFirst();
31         if (cmOpt.isPresent()) {
32             String className = cmOpt.get().getClazz();
33             try {
34                 Class clazz = Class.forName(className);
35                 Object object = clazz.newInstance();
36                 return object;
37             } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
38                 LOGGER.error("[MockController] 類加載異常,{}", e);
39             }
40         }
41 
42         //沒有匹配到html,js,css,圖片資源或者接口路徑
43         return null;
44     }

 


手寫一套迷你版HTTP服務器


針對靜態文件的處理模塊,這里面主要是由responseHandle函數處理。


代碼如下:

 1  /**
 2      * 靜態文件處理器
 3      *
 4      * @param object
 5      * @return
 6      * @throws IOException
 7      */
 8     public static Object responseHandle(Object object, ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException {
 9         String result;
10         FullHttpResponse response = null;
11         //接口的404處理模塊
12         if (object == null) {
13             result = CommonUtil.read404Html();
14             return ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);
15 
16         } else if (object instanceof JsModel) {
17 
18             JsModel jsModel = (JsModel) object;
19             result = CommonUtil.readFileFromResource(jsModel.getUrl());
20             response = notFoundHandler(result);
21             return (response == null) ? ResponCoreHandle.responseJs(HttpResponseStatus.OK, result) : response;
22 
23         } else if (object instanceof CssModel) {
24 
25             CssModel cssModel = (CssModel) object;
26             result = CommonUtil.readFileFromResource(cssModel.getUrl());
27             response = notFoundHandler(result);
28             return (response == null) ? ResponCoreHandle.responseCss(HttpResponseStatus.OK, result) : response;
29 
30         }//初始化頁面
31         else if (object instanceof PageModel) {
32 
33             PageModel pageModel = (PageModel) object;
34             if (pageModel.getCode() == RequestConstants.INDEX_CODE) {
35                 result = CommonUtil.readIndexHtml(pageModel.getPagePath());
36             } else {
37                 result = CommonUtil.readFileFromResource(pageModel.getPagePath());
38             }
39 
40             return ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);
41 
42         } else if (object instanceof PicModel) {
43             PicModel picModel = (PicModel) object;
44             ResponCoreHandle.writePic(picModel.getUrl(), ctx, fullHttpRequest);
45             return picModel;
46         }
47         return null;
48 
49     }

 

對於接口類型的數據請求,主要是在handler里面完成

手寫一套迷你版HTTP服務器


代碼為:

 1  if (!(response instanceof FullHttpResponse) && !(response instanceof PicModel)) {
 2 
 3             //接口處理
 4             if (isContaionInterFace(object, BaseController.class)) {
 5                 ControllerCglib cc = new ControllerCglib();
 6                 Object proxyObj = cc.getTarget(object);
 7                 Method[] methodArr = null;
 8                 Method aimMethod = null;
 9 
10 
11                 if (fullHttpRequest.method().equals(HttpMethod.GET)) {
12                     methodArr = proxyObj.getClass().getMethods();
13                     aimMethod = getMethodByName(methodArr, "doGet");
14                 } else if (fullHttpRequest.method().equals(HttpMethod.POST)) {
15                     methodArr = proxyObj.getClass().getMethods();
16                     aimMethod = getMethodByName(methodArr, "doPost");
17                 }
18 
19                 //代理執行method
20                 if (aimMethod != null) {
21                     ControllerRequest controllerRequest=paramterHandler(fullHttpRequest);
22                     preHandler(controllerRequest);
23                     BaseResponse baseResponse = (BaseResponse) aimMethod.invoke(proxyObj, controllerRequest);
24                     aftHandler(controllerRequest);
25                     result = JSON.toJSONString(baseResponse);
26                 }
27             }
28             response = ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);
29         }
30         ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
31     }

 

這里面主要是借用了cglib來進行一些相關的代理編寫,通過url找到匹配的controller,然后根據請求的類型來執行doget或者dopost功能。而preHandler和afterHandler主要是用於進行相關過濾器的執行操作。這里面用到了責任鏈的模式來進行編寫。

過濾鏈在程序初始化的時候便有進行相應的掃描和排序操作,核心代碼思路如下所示:

 1   /**
 2      * 掃描過濾器
 3      *
 4      * @param path
 5      * @return
 6      */
 7     public static List<FilterModel> scanFilter(String path) throws IllegalAccessException, InstantiationException {
 8         Map<String, Object> result = new HashMap<>(60);
 9         Set<Class<?>> clazz = ClassUtil.getClzFromPkg(path);
10         List<FilterModel> filterModelList = new ArrayList<>();
11         for (Class<?> aClass : clazz) {
12             if (aClass.isAnnotationPresent(Filter.class)) {
13                 Filter filter = aClass.getAnnotation(Filter.class);
14                 FilterModel filterModel = new FilterModel(filter.order(), filter.name(), aClass.newInstance());
15                 filterModelList.add(filterModel);
16             }
17         }
18         FilterModel[] tempArr = new FilterModel[filterModelList.size()];
19         int index = 0;
20         for (FilterModel filterModel : filterModelList) {
21             tempArr[index] = filterModel;
22             System.out.println("[Filter] " + filterModel.toString());
23             index++;
24         }
25         return sortFilterModel(tempArr);
26     }
27 
28     /**
29      * 對加載的filter進行優先級排序
30      *
31      * @return
32      */
33     private static List<FilterModel> sortFilterModel(FilterModel[] filterModels) {
34         for (int i = 0; i < filterModels.length; i++) {
35             int minOrder = filterModels[i].getOrder();
36             int minIndex = i;
37             for (int j = i; j < filterModels.length; j++) {
38                 if (minOrder > filterModels[j].getOrder()) {
39                     minOrder = filterModels[j].getOrder();
40                     minIndex = j;
41                 }
42             }
43             FilterModel temp = filterModels[minIndex];
44             filterModels[minIndex] = filterModels[i];
45             filterModels[i] = temp;
46         }
47         return Arrays.asList(filterModels);
48     }

 

最后附上本框架的碼雲地址:

https://gitee.com/IdeaHome_admin/ITree

內附對應的源代碼,jar包,以及可以讓人理解思路的代碼注釋,喜歡的朋友可以給個star。

 

作者:idea

1. Java 性能優化:教你提高代碼運行的效率

2. Java問題排查工具清單

3. 記住:永遠不要在MySQL中使用UTF-8

4. Springboot啟動原理解析

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM