手写一套迷你版HTTP服务器
本文主要介绍如何通过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主要是用于进行相关过滤器的执行操作。这里面用到了责任链的模式来进行编写。
过滤链在程序初始化的时候便有进行相应的扫描和排序操作,核心代码思路如下所示:
/**
* 扫描过滤器
*
* @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。
作者:idea
推荐阅读
2. Java问题排查工具清单
手写一套迷你版HTTP服务器的更多相关文章
- 手写一个最迷你的Web服务器
今天我们就仿照Tomcat服务器来手写一个最简单最迷你版的web服务器,仅供学习交流. 1. 在你windows系统盘的F盘下,创建一个文件夹webroot,用来存放前端代码. 2. 代码介绍: ( ...
- Mybatis(一):手写一套持久层框架
作者 : 潘潘 未来半年,有幸与导师们一起学习交流,趁这个机会,把所学所感记录下来. 「封面图」 自毕业以后,自己先创业后上班,浮沉了近8年,内心着实焦躁,虽一直是走科班路线,但在技术道路上却始终没静 ...
- MNIST手写数字分类simple版(03-2)
simple版本nn模型 训练手写数字处理 MNIST_data数据 百度网盘链接:https://pan.baidu.com/s/19lhmrts-vz0-w5wv2A97gg 提取码:cgnx ...
- 手写tomcat——编写一个echo http服务器
核心代码如下: public class DiyTomcat1 { public void run() throws IOException { ServerSocket serverSocket = ...
- 一个用 C 语言写的迷你版 2048 游戏,仅仅有 500个字符
Jay Chan 用 C 语言写的一个迷你版 2048 游戏,仅仅有 487 个字符. 来围观吧 M[16],X=16,W,k;main(){T(system("stty cbreak&qu ...
- 手写Koa.js源码
用Node.js写一个web服务器,我前面已经写过两篇文章了: 第一篇是不使用任何框架也能搭建一个web服务器,主要是熟悉Node.js原生API的使用:使用Node.js原生API写一个web服务器 ...
- JDK动态代理深入理解分析并手写简易JDK动态代理(上)
原文同步发表至个人博客[夜月归途] 原文链接:http://www.guitu18.com/se/java/2019-01-03/27.html 作者:夜月归途 出处:http://www.guitu ...
- 手写HashMap,快手面试官直呼内行!
手写HashMap?这么狠,面试都卷到这种程度了? 第一次见到这个面试题,是在某个不方便透露姓名的Offer收割机大佬的文章: 这--我当时就麻了,我们都知道HashMap的数据结构是数组+链表+红黑 ...
- 手写迷你SpringMVC框架
前言 学习如何使用Spring,SpringMVC是很快的,但是在往后使用的过程中难免会想探究一下框架背后的原理是什么,本文将通过讲解如何手写一个简单版的springMVC框架,直接从代码上看框架中请 ...
随机推荐
- 关于mybatis的 insert into select 命令未结束问题
关于mybatis的 insert into select 命令未结束问题,最后以为是sql写错了,可是,在plsql运行又没问题.最后还是解决这个问题. 是设置问题. ### Cause: java ...
- [IT学习]从网上获取pdf制作vce文件
考过IT证书的朋友,都知道什么是vce文件.如果仅仅找到了pdf版本的文件,该如何转为vce文件呢? 具体的步骤如下: 1.到如下网址下载examformatter,http://www.examco ...
- spring的PROPAGATION_REQUIRES_NEW事务,下列说法正确的是(D)
A:内部事务回滚会导致外部事务回滚 B:内部事务回滚了,外部事务仍可以提交 C:外部事务回滚了,内部事务也跟着回滚 D:外部事务回滚了,内部事务仍可以提交 PROPAGATION_REQUIRES_N ...
- ps -ef | grep
ps -ef | grep java 查看所有关于java的进程 root 17540 1 0 2009 ? 01:42:27 /usr/java/jdk1.5. ...
- js数组清空和去重
1.splice var ary = [1,2,3,4]; ary.splice(0,ary.length); console.log(ary); // 输出 Array[0],空数组,即被清空了 2 ...
- I.MX6 ifconfig: SIOCSIFHWADDR: Cannot assign requested address
/************************************************************************** * I.MX6 ifconfig: SIOCSI ...
- BZOJ_2081_[Poi2010]Beads_哈希
BZOJ_2081_[Poi2010]Beads_哈希 Description Zxl有一次决定制造一条项链,她以非常便宜的价格买了一长条鲜艳的珊瑚珠子,她现在也有一个机器,能把这条珠子切成很多块(子 ...
- 【POJ 1947】 Rebuilding Roads
[题目链接] 点击打开链接 [算法] f[i][j]表示以i为根的子树中,最少删多少条边可以组成j个节点的子树 树上背包,即可 [代码] #include <algorithm> #inc ...
- 【SCOI 2005】 扫雷
[题目链接] 点击打开链接 [算法] 只要第一行第一个数确定了,后面的数也都确定了 递推两遍即可 [代码] #include<bits/stdc++.h> using namespace ...
- 【141】Adobe Acrobat技巧
目录: 去除PDF的水印 待定 待定 待定 待定 待定 待定 待定 1. 批量去除PDF文件的水印 用Adobe Acrobat打开PDF文件之后,右侧选择工具>页面>水印>删除,可 ...