本文主要介紹如何經過netty來手寫一套簡單版的HTTP服務器,同時將關於netty的許多細小知識點進行了串聯,用於鞏固和提高對於netty框架的掌握程度。css
服務器運行效果html
服務器支持對靜態文件css,js,html,圖片資源的訪問。經過網絡的形式對這些文件能夠進行訪問,相應截圖以下所示:java
支持對於js,css,html等文件的訪問:git
而後引用相應的pom依賴文件信息:spring
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(包名能夠本身隨便定義)json
定義一個啓動類WebApplication.java(有點相似於springboot的那種思路)bootstrap
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包,主要是用於作測試:api
創建一個測試使用的Controller:springboot
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問題排查工具清單