手写一套迷你版HTTP服务器
本文主要介绍如何通过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问题排查工具清单

《《--扫描二维码关注他!
【Java知音】公众号,每天早上8:30为您准时推送一篇技术文章
在Java知音公众号内回复“面试题聚合”,送你一份Java面试题宝典。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?