推薦閱讀

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

2. Java問題排查工具清單

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

4. Springboot啟動原理解析

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

伺服器運行效果

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

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

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

<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之後,整體項目的結構如下所示:

基礎的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文件內容如下所示:

<!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/就會跳轉到該指定頁面:

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

默認的404頁面為

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

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

首先從整體設計方面,核心內容是分為了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匹配處理:

我們的客戶端發送的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;
}

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

代碼如下:

/**
* 靜態文件處理器
*
* @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裡面完成

代碼為:

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);
}

這裡面主要是借用了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);
}

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

gitee.com/IdeaHome_admi

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


推薦閱讀:
相关文章