推薦閱讀
1. Java 性能優化:教你提高代碼運行的效率
2. Java問題排查工具清單
3. 記住:永遠不要在MySQL中使用UTF-8
4. Springboot啟動原理解析
本文主要介紹如何通過netty來手寫一套簡單版的HTTP伺服器,同時將關於netty的許多細小知識點進行了串聯,用於鞏固和提升對於netty框架的掌握程度。
伺服器運行效果
伺服器支持對靜態文件css,js,html,圖片資源的訪問。通過網路的形式對這些文件可以進行訪問,相應截圖如下所示:
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.6.Final</version> </dependency>
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.13</version> </dependency>
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.6</version> </dependency>
導入依賴之後,新建一個包itree.demo(包名可以自己隨便定義)
定義一個啟動類WebApplication.java(有點類似於springboot的那種思路)
package itree.demo;
import com.sise.itree.ITreeApplication;
/** * @author idea * @data 2019/4/30 */ public class WebApplication {
public static void main(String[] args) throws IllegalAccessException, InstantiationException { ITreeApplication.start(WebApplication.class); } }
在和這個啟動類同級別的包底下,建立itree.demo.controller和itree.demo.filter包,主要是用於做測試:
建立一個測試使用的Controller:
package itree.demo.controller;
import com.sise.itree.common.BaseController; import com.sise.itree.common.annotation.ControllerMapping; import com.sise.itree.core.handle.response.BaseResponse; import com.sise.itree.model.ControllerRequest;
/** * @author idea * @data 2019/4/30 */ @ControllerMapping(url = "/myController") public class MyController implements BaseController {
@Override public BaseResponse doGet(ControllerRequest controllerRequest) { String username= (String) controllerRequest.getParameter("username"); System.out.println(username); return new BaseResponse(1,username); }
@Override public BaseResponse doPost(ControllerRequest controllerRequest) { return null; } }
這裡面的BaseController是我自己在Itree包裡面編寫的介面,這裡面的格式有點類似於javaee的servlet,之前我在編寫代碼的時候有點參考了servlet的設計。(註解裡面的url正是匹配了客戶端訪問時候所映射的url鏈接)
編寫相應的過濾器:
package itree.demo.filter;
import com.sise.itree.common.BaseFilter; import com.sise.itree.common.annotation.Filter; import com.sise.itree.model.ControllerRequest;
/** * @author idea * @data 2019/4/30 */ @Filter(order = 1) public class MyFilter implements BaseFilter {
@Override public void beforeFilter(ControllerRequest controllerRequest) { System.out.println("before"); }
@Override public void afterFilter(ControllerRequest controllerRequest) { System.out.println("after"); } }
通過代碼的表面意思,可以很好的理解這裡大致的含義。當然,如果過濾器有優先順序的話,可以通過@Filter註解裡面的order屬性進行排序。搭建起多個controller和filter之後,整體項目的結構如下所示:
暫時可提供的配置有以下幾個:
server.port=9090 index.page=html/home.html not.found.page=html/404.html
結合相應的靜態文件放入之後,整體的項目結構圖如下所示:
這個時候可以啟動之前編寫的WebApplication啟動類
啟動的時候控制台會列印出相應的信息:
啟動類會掃描同級目錄底下所有帶有@Filter註解和@ControllerMapping註解的類,然後加入指定的容器當中。(這裡借鑒了Spring裡面的ioc容器的思想)
啟動之後,進行對於上述controller介面的訪問測試,便可以查看到以下信息的內容:
除了常規的介面類型數據響應之外,還提供有靜態文件的訪問功能:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> this is home </body> </html>
我們在之前說的properties文件裡面提及了相應的初始化頁面配置是:
index.page=html/home.html
因此,訪問的時候默認的http://localhost:9090/就會跳轉到該指定頁面:
首先從整體設計方面,核心內容是分為了netty的server和serverHandler處理器:
首先是接受數據的server端:
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.stream.ChunkedWriteHandler;
/** * @author idea * @data 2019/4/26 */ public class NettyHttpServer {
private int inetPort;
public NettyHttpServer(int inetPort) { this.inetPort = inetPort; }
public int getInetPort() { return inetPort; }
public void init() throws Exception {
EventLoopGroup parentGroup = new NioEventLoopGroup(); EventLoopGroup childGroup = new NioEventLoopGroup();
try { ServerBootstrap server = new ServerBootstrap(); // 1. 綁定兩個線程組分別用來處理客戶端通道的accept和讀寫時間 server.group(parentGroup, childGroup) // 2. 綁定服務端通道NioServerSocketChannel .channel(NioServerSocketChannel.class) // 3. 給讀寫事件的線程通道綁定handler去真正處理讀寫 // ChannelInitializer初始化通道SocketChannel .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { // 請求解碼器 socketChannel.pipeline().addLast("http-decoder", new HttpRequestDecoder()); // 將HTTP消息的多個部分合成一條完整的HTTP消息 socketChannel.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65535)); // 響應轉碼器 socketChannel.pipeline().addLast("http-encoder", new HttpResponseEncoder()); // 解決大碼流的問題,ChunkedWriteHandler:向客戶端發送HTML5文件 socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler()); // 自定義處理handler socketChannel.pipeline().addLast("http-server", new NettyHttpServerHandler()); } });
// 4. 監聽埠(伺服器host和port埠),同步返回 ChannelFuture future = server.bind(this.inetPort).sync(); System.out.println("[server] opening in "+this.inetPort); // 當通道關閉時繼續向後執行,這是一個阻塞方法 future.channel().closeFuture().sync(); } finally { childGroup.shutdownGracefully(); parentGroup.shutdownGracefully(); } }
}
Netty接收數據的處理器NettyHttpServerHandler 代碼如下:
import com.alibaba.fastjson.JSON; import com.sise.itree.common.BaseController; import com.sise.itree.model.ControllerRequest; import com.sise.itree.model.PicModel; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.util.CharsetUtil; import com.sise.itree.core.handle.StaticFileHandler; import com.sise.itree.core.handle.response.BaseResponse; import com.sise.itree.core.handle.response.ResponCoreHandle; import com.sise.itree.core.invoke.ControllerCglib; import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map;
import static io.netty.buffer.Unpooled.copiedBuffer; import static com.sise.itree.core.ParameterHandler.getHeaderData; import static com.sise.itree.core.handle.ControllerReactor.getClazzFromList; import static com.sise.itree.core.handle.FilterReactor.aftHandler; import static com.sise.itree.core.handle.FilterReactor.preHandler; import static com.sise.itree.util.CommonUtil.*;
/** * @author idea * @data 2019/4/26 */ @Slf4j public class NettyHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws Exception { String uri = getUri(fullHttpRequest.getUri()); Object object = getClazzFromList(uri); String result = "recive msg"; Object response = null;
//靜態文件處理 response = StaticFileHandler.responseHandle(object, ctx, fullHttpRequest);
if (!(response instanceof FullHttpResponse) && !(response instanceof PicModel)) {
//介面處理 if (isContaionInterFace(object, BaseController.class)) { ControllerCglib cc = new ControllerCglib(); Object proxyObj = cc.getTarget(object); Method[] methodArr = null; Method aimMethod = null;
if (fullHttpRequest.method().equals(HttpMethod.GET)) { methodArr = proxyObj.getClass().getMethods(); aimMethod = getMethodByName(methodArr, "doGet"); } else if (fullHttpRequest.method().equals(HttpMethod.POST)) { methodArr = proxyObj.getClass().getMethods(); aimMethod = getMethodByName(methodArr, "doPost"); }
//代理執行method if (aimMethod != null) { ControllerRequest controllerRequest=paramterHandler(fullHttpRequest); preHandler(controllerRequest); BaseResponse baseResponse = (BaseResponse) aimMethod.invoke(proxyObj, controllerRequest); aftHandler(controllerRequest); result = JSON.toJSONString(baseResponse); } } response = ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result); } ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); }
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); }
/** * 處理請求的參數內容 * * @param fullHttpRequest * @return */ private ControllerRequest paramterHandler(FullHttpRequest fullHttpRequest) { //參數處理部分內容 Map<String, Object> paramMap = new HashMap<>(60); if (fullHttpRequest.method() == HttpMethod.GET) { paramMap = ParameterHandler.getGetParamsFromChannel(fullHttpRequest); } else if (fullHttpRequest.getMethod() == HttpMethod.POST) { paramMap = ParameterHandler.getPostParamsFromChannel(fullHttpRequest); } Map<String, String> headers = getHeaderData(fullHttpRequest);
ControllerRequest ctr = new ControllerRequest(); ctr.setParams(paramMap); ctr.setHeader(headers); return ctr; }
這裡面的核心模塊我大致分成了:
url匹配處理:
我們的客戶端發送的url請求進入server端之後,需要快速的進行url路徑的格式處理。例如將http://localhost:8080/xxx-1/xxx-2?username=test轉換為/xxx-1/xxx-2的格式,這樣方便和controller頂部設計的註解的url信息進行關鍵字匹配。
/** * 截取url裡面的路徑欄位信息 * * @param uri * @return */ public static String getUri(String uri) { int pathIndex = uri.indexOf("/"); int requestIndex = uri.indexOf("?"); String result; if (requestIndex < 0) { result = uri.trim().substring(pathIndex); } else { result = uri.trim().substring(pathIndex, requestIndex); } return result; }
從容器獲取匹配響應數據:
經過了前一段的url格式處理之後,我們需要根據url的後綴來預先判斷是否是數據靜態文件的請求:
對於不同後綴格式來返回不同的model對象(每個model對象都是共同的屬性url),之所以設計成不同的對象是因為針對不同格式的數據,response的header裡面需要設置不同的屬性值。
/** * 匹配響應信息 * * @param uri * @return */ public static Object getClazzFromList(String uri) { if (uri.equals("/") || uri.equalsIgnoreCase("/index")) { PageModel pageModel; if(ITreeConfig.INDEX_CHANGE){ pageModel= new PageModel(); pageModel.setPagePath(ITreeConfig.INDEX_PAGE); } return new PageModel(); } if (uri.endsWith(RequestConstants.HTML_TYPE)) { return new PageModel(uri); } if (uri.endsWith(RequestConstants.JS_TYPE)) { return new JsModel(uri); } if (uri.endsWith(RequestConstants.CSS_TYPE)) { return new CssModel(uri); } if (isPicTypeMatch(uri)) { return new PicModel(uri); }
//查看是否是匹配json格式 Optional<ControllerMapping> cmOpt = CONTROLLER_LIST.stream().filter((p) -> p.getUrl().equals(uri)).findFirst(); if (cmOpt.isPresent()) { String className = cmOpt.get().getClazz(); try { Class clazz = Class.forName(className); Object object = clazz.newInstance(); return object; } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { LOGGER.error("[MockController] 類載入異常,{}", e); } }
//沒有匹配到html,js,css,圖片資源或者介面路徑 return null; }
/** * 靜態文件處理器 * * @param object * @return * @throws IOException */ public static Object responseHandle(Object object, ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException { String result; FullHttpResponse response = null; //介面的404處理模塊 if (object == null) { result = CommonUtil.read404Html(); return ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);
} else if (object instanceof JsModel) {
JsModel jsModel = (JsModel) object; result = CommonUtil.readFileFromResource(jsModel.getUrl()); response = notFoundHandler(result); return (response == null) ? ResponCoreHandle.responseJs(HttpResponseStatus.OK, result) : response;
} else if (object instanceof CssModel) {
CssModel cssModel = (CssModel) object; result = CommonUtil.readFileFromResource(cssModel.getUrl()); response = notFoundHandler(result); return (response == null) ? ResponCoreHandle.responseCss(HttpResponseStatus.OK, result) : response;
}//初始化頁面 else if (object instanceof PageModel) {
PageModel pageModel = (PageModel) object; if (pageModel.getCode() == RequestConstants.INDEX_CODE) { result = CommonUtil.readIndexHtml(pageModel.getPagePath()); } else { result = CommonUtil.readFileFromResource(pageModel.getPagePath()); }
return ResponCoreHandle.responseHtml(HttpResponseStatus.OK, result);
} else if (object instanceof PicModel) { PicModel picModel = (PicModel) object; ResponCoreHandle.writePic(picModel.getUrl(), ctx, fullHttpRequest); return picModel; } return null;
對於介面類型的數據請求,主要是在handler裡面完成
這裡面主要是借用了cglib來進行一些相關的代理編寫,通過url找到匹配的controller,然後根據請求的類型來執行doget或者dopost功能。而preHandler和afterHandler主要是用於進行相關過濾器的執行操作。這裡面用到了責任鏈的模式來進行編寫。
過濾鏈在程序初始化的時候便有進行相應的掃描和排序操作,核心代碼思路如下所示:
HTTP /** * 掃描過濾器 * * @param path * @return */ public static List<FilterModel> scanFilter(String path) throws IllegalAccessException, InstantiationException { Map<String, Object> result = new HashMap<>(60); Set<Class<?>> clazz = ClassUtil.getClzFromPkg(path); List<FilterModel> filterModelList = new ArrayList<>(); for (Class<?> aClass : clazz) { if (aClass.isAnnotationPresent(Filter.class)) { Filter filter = aClass.getAnnotation(Filter.class); FilterModel filterModel = new FilterModel(filter.order(), filter.name(), aClass.newInstance()); filterModelList.add(filterModel); } } FilterModel[] tempArr = new FilterModel[filterModelList.size()]; int index = 0; for (FilterModel filterModel : filterModelList) { tempArr[index] = filterModel; System.out.println("[Filter] " + filterModel.toString()); index++; } return sortFilterModel(tempArr); }
/** * 對載入的filter進行優先順序排序 * * @return */ private static List<FilterModel> sortFilterModel(FilterModel[] filterModels) { for (int i = 0; i < filterModels.length; i++) { int minOrder = filterModels[i].getOrder(); int minIndex = i; for (int j = i; j < filterModels.length; j++) { if (minOrder > filterModels[j].getOrder()) { minOrder = filterModels[j].getOrder(); minIndex = j; } } FilterModel temp = filterModels[minIndex]; filterModels[minIndex] = filterModels[i]; filterModels[i] = temp; } return Arrays.asList(filterModels); }
最後附上本框架的碼雲地址:
https://gitee.com/IdeaHome_admin/ITree
內附對應的源代碼,jar包,以及可以讓人理解思路的代碼注釋,喜歡的朋友可以給個star。