本文主要介紹如何通過netty來手寫一套簡單版的HTTP服務器,同時將關於netty的許多細小知識點進行了串聯,用於鞏固和提升對於netty框架的掌握程度。
服務器運行效果
服務器支持對靜態文件css,js,html,圖片資源的訪問。通過網絡的形式對這些文件可以進行訪問,相應截圖如下所示:
支持對於js,css,html等文件的訪問:
然后引用相應的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之后,整體項目的結構如下所示:
基礎的java程序寫好之后,便是相應的resources文件了:
這里提供了可適配性的配置文件,默認配置文件命名為resources的config/itree-config.properties文件:
暫時可提供的配置有以下幾個:
server.port=9090
index.page=html/home.html
not.found.page=html/404.html
結合相應的靜態文件放入之后,整體的項目結構圖如下所示:
這個時候可以啟動之前編寫的WebApplication啟動類
啟動的時候控制台會打印出相應的信息:
啟動類會掃描同級目錄底下所有帶有@Filter注解和@ControllerMapping注解的類,然后加入指定的容器當中。(這里借鑒了Spring里面的ioc容器的思想)
啟動之后,進行對於上述controller接口的訪問測試,便可以查看到以下信息的內容:
同樣,我們查看控制台的信息打印:
controller接收數據之前,通過了三層的filter進行過濾,而且過濾的順序也是和我們之前預期所想的那樣一直,按照order從小到大的順序執行(同樣我們可以接受post類型的請求)
除了常規的接口類型數據響應之外,還提供有靜態文件的訪問功能:
對於靜態文件里面的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/就會跳轉到該指定頁面:
假設不配置properties文件的話,則會采用默認的頁面跳轉,默認的端口號8080
默認的404頁面為
基本的使用步驟大致如上述所示。
那么又該怎么來進行這樣的一套框架設計和編寫呢?
首先從整體設計方面,核心內容是分為了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 }
針對靜態文件的處理模塊,這里面主要是由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里面完成
代碼為:
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
推薦閱讀
2. Java問題排查工具清單