本文主要介绍如何通过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 {@Overridepublic BaseResponse doGet(ControllerRequest controllerRequest) {String username= (String) controllerRequest.getParameter("username");System.out.println(username);return new BaseResponse(1,username);}@Overridepublic 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 {@Overridepublic void beforeFilter(ControllerRequest controllerRequest) {System.out.println("before");}@Overridepublic 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>() {@Overrideprotected 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());// 自定义处理handlersocketChannel.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> {@Overrideprotected 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");}//代理执行methodif (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);}@Overridepublic 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");}//代理执行methodif (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主要是用于进行相关过滤器的执行操作。这里面用到了责任链的模式来进行编写。

过滤链在程序初始化的时候便有进行相应的扫描和排序操作,核心代码思路如下所示:

    /*** 扫描过滤器** @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。

手写一套迷你版HTTP服务器相关推荐

  1. 迷你服务器开机无显示,迷你版云服务器未启动

    迷你版云服务器未启动 内容精选 换一换 企业主机安全(Host Security Service,HSS)是提升服务器整体安全性的服务,通过主机管理.风险防御.入侵检测.安全运营.网页防篡改功能,可全 ...

  2. 手写数字识别增强版项目实践规划

    项目人员: 1160300925 王卓 1160300630 范航明 1160300316 张雅舒 1160300920 陈浩楠 项目选择: 项目一,手写数字增强版 Github 仓库地址: http ...

  3. 金蝶迷你版云服务器没有响应,金蝶迷你版连接金蝶云服务器异常

    金蝶迷你版连接金蝶云服务器异常 内容精选 换一换 华为云帮助中心,为用户提供产品简介.价格说明.购买指南.用户指南.API参考.最佳实践.常见问题.视频帮助等技术文档,帮助您快速上手使用华为云服务. ...

  4. 金蝶迷你版云服务器没有响应,连接云服务器异常金蝶迷你版

    连接云服务器异常金蝶迷你版 内容精选 换一换 云服务器列表页面显示了所有已创建的GPU加速型云服务器信息.您可以参考如下操作查看云服务器详情.云服务器详情中展示了如下信息:云服务器名称.ID.状态等. ...

  5. asp手写签名代码2021版提供源码控件

    今天接了一个小事,一个朋友想实现货物在线签收,要收货人在线签名,并保存入库,让我帮忙写一个asp的手写签名功能,忙活一下午实现了,估计会有很多人有这种需求,放网上吧 function lineCanv ...

  6. 【uni-app】使用写字板,实现手写签名----直接使用版

    需求 用户需在APP或小程序使用手写的方式签字或签名 效果图片 实现方式 封装成组件调用(推荐),如需了解使用可点击前往-[封装组件版] 使用的页面内直接使用,目前的教程就是直接使用的,未封装成组件 ...

  7. 微信小程序---手写签名(签字版)

    公司近期有个需要用户签名的功能,就用小程序canvas写了个, wxml <view class="sign"><view class="paper&q ...

  8. 用Maven手写SpringIOC(简易版)

    这里我是实现了如何获取配置文件中的bean对象 一.文件结构 二.需要的pom.xml依赖 <dependencies><dependency><groupId>d ...

  9. Djkastra堆(手写堆)优化版

    直接上代码 代码中的注释掉的部分为C++优先队列实现 #include<cstdio> #include<cstdlib> #include<iostream> # ...

最新文章

  1. 运维自动化之Cobbler安装配置
  2. 用JS的正则表达式如何判断输入框内为中文或者是英文
  3. python拆分列表元素_Python将列表拆分为一个元素 - python
  4. Java虚拟机(JVM)以及跨平台原理
  5. jQuery 3.0 的 setter/getter 模式
  6. 算法-排序-选择排序
  7. oracle+tquery,Oracle测试题及答案
  8. 中国酒精拭子市场趋势报告、技术动态创新及市场预测
  9. aspnet_regsql.exe 工具参数解析
  10. 14-CompletableFuture异步编排
  11. 软件著作权申请注意事项——常见问题[详细版,不断补充中]
  12. python psutil模块 硬盘厂家芯片型号_python第三方模块—psutil模块
  13. java仿QQ聊天软件OIM艰辛之路
  14. MySQL启动提示The server quit without updating PID file (/[失败]sqld/mysqld.pid).
  15. 地铁时光机第一阶段冲刺四
  16. 基于Modis数据的地表温度反演
  17. 微软常用运行库合集,电脑必备组件
  18. 企业宣传片如何做?上海宣传片公司拍摄与制作看这里
  19. python小游戏 炸弹人小游戏设计与实现
  20. 【2020蓝桥杯省赛】【填空题】排序(详解!)

热门文章

  1. 量子纠缠的机制是什么?一定要理解整体性概念
  2. 从开始到验收:我做外包项目测试的经验总结
  3. 用node.js 搭建的博客程序心得(node.js实战读书笔记1)
  4. 什么是线程循环发包 ?
  5. PaddleOCR数字仪表识别——1.字体背景删选
  6. 自我鉴定300字大专计算机应用,业余大专自我鉴定300字(精选8篇)
  7. arena of valor服务器未响应,传说对决 -Arena of Valor-启动后一直黑屏无法进入什么原因...
  8. vim工具——常用插件
  9. td自动换行时不切断英文单词
  10. ubuntu WPS 报错系统缺失字体symbol、wingdings、wingdings 2、wingdings 3