前言
因为本人是才学java不久,所以学一些基础就直接上手框架,没有用过jsp或者单独的Tomcat进行开发,所以本容器的用法更像是在框架中使用其他组件的用法。如有不足之处请指出。
项目背景以及意义
或许学框架、背面经或多或少的都会知道一些容器处理的请求响应的过程,但是相信更多的人跟博主一样,懂了,但是没完全懂。所以自己简单的写一下,算是走一遍处理流程,让自己更加理解容器或者框架请求响应部分的底层原理。本项目除了学习几乎没有其他价值,看完本项目你将学到容器处理请求响应的原理、http请求响应消息头中常见字段的作用、较为规范化的代码、java的IO流等
项目分析
前面也说过我没有直接使用过tomcat,所以我的设计更多的是参照框架。无论怎么设计,有一点很明确,那就是使用起来不能太复杂,对新手友好。所以我设计的容器只需要这几个步骤:
一、引入依赖
这一步肯定是不可少的,用别人的东西你肯定得引入jar包。
二、实例化一个启动类Server
这里框架中的注解enableXXXX,不过写注解不是本项目的重点,所以都用了其他的处理方式,启动类的构造函数需要接收端口号和ip地址。
三、编写接口
这里跟springboot类似,你可以使用Controller、Service将接口和服务分离。不一样的是没有自动注入,框架的接口只需要@Controller注解标识,一个类中所有的方法都可以处理一个或多个路径。而我的是继承Servlet类,实现doGet和doPost方法,一个类只能处理一个路径。哪个类处理哪个路径需要在web.xml中编写。
四、编写配置文件web.xml
如果不写接口,只用来做静态web服务器,配置类可以不用写,在maven项目中静态资源默认访问路径为resources下的所有文件。如果在配置类中用
com.xu.Controller.HelloController
这两个标签处理映射关系即可。
更加具体的使用看完本篇文章后相信你就都知道了
项目实现
分析完项目后我们再来设计一下容器大体的架构。
架构设计
一、启动类Server
Server用来绑定服务器ip和端口、监听客户端,启动线程处理客户端。
package com.xu.server;
import com.xu.Utils.CloseIoUtil;
import com.xu.servlet.DispatchServlet;
import com.xu.servlet.Request;
import com.xu.servlet.Response;
import com.xu.servlet.servletImp.HttpRequest;
import com.xu.servlet.servletImp.HttpResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* web容器启动类
*/
@Slf4j
public class Server {
public static void main(String[] args) {
new Server(8080, "127.0.0.1");
}
/**
* 容器的构造函数
*
* @param port 服务器监听端口
* @param ipAddress 服务器监听地址
*/
public Server(int port, String ipAddress) {
try (ServerSocket server = new ServerSocket(port, 3, InetAddress.getByName(ipAddress))) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
/*
* 不断监听客户端的请求
* 有请求进来提交给线程池做相应的处理
*/
while (true) {
try {
Socket s = server.accept();
log.info("有客户端连接");
threadPool.execute(new Client(s));
} catch (Throwable e) {
log.error(e.getMessage());
}
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
/**
* 处理请求的线程类
*/
private static class Client implements Runnable {
private Socket s;
private Response response;
private Request request;
public Client(Socket s) {
this.s = s;
}
@Override
public void run() {
try {
request = new HttpRequest(s.getInputStream());
response = new HttpResponse(s.getOutputStream());
DispatchServlet.dispatch(request, response);
} catch (Throwable e) {
log.error(e.getMessage());
} finally {
CloseIoUtil.close(s);
}
}
}
}
二、Request、Response接口
用于解析请求数据和用于返回响应数据。篇幅过长,我就只贴接口的代码。
package com.xu.servlet;
import com.alibaba.fastjson.JSONObject;
import java.util.Map;
public interface Request {
/**
* 获取请求的消息头
*/
Map getHeaders();
/**
* 获取请求的方法
*/
String getMethod();
/**
* 获取请求的路径,不包含路径中的参数
*/
String getPath();
/**
* 获取原生请求体的数据
*/
String getBody();
/**
* 获取请求路径中的参数
*/
Map<String, String> getPathVariables();
/**
* 如果请求类型是application/json
* 获取请求体的json对象
*/
JSONObject getJsonBody();
/**
* 如果请求类型是application/x-www-form-urlencoded
* 获取请求体的Map对象
*/
Map<String, String> getUrlEncodeBody();
}
package com.xu.servlet;
import java.io.IOException;
public interface Response {
/**
* 将数据返回给前端
*
* @param responseContent 返回的数据,数据必须为对象
* @throws IOException
*/
void send(Object responseContent) throws IOException;
/**
* 设置响应头
*
* @param key 响应头的键
* @param value 响应头的值
*/
void setHeader(String key, String value);
/**
* 设置响应编码
*/
void setCharset(String charset);
/**
* 获取响应编码
*/
String getCharset();
/**
* 设置状态码
*/
void setStatus(int status);
}
三、DispatchServlet类
用于寻找能处理该请求的类或者资源
package com.xu.servlet;
import com.xu.Utils.ReadConfigFile;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
/**
* 请求处理中心,决定该请求由谁处理
*/
@Slf4j
public class DispatchServlet {
private static HashMap<String, String> map;
//静态代码块加载配置文件中路径与处理方法的映射关系
static {
map = ReadConfigFile.getHashmap("path", "controller");
}
public static void dispatch(Request request, Response response) throws Exception {
//如果有处理方法则调用对应的方法处理,否则找静态资源,
if (map.containsKey(request.getPath())) {
String className = map.get(request.getPath());
Class<Servlet> servletClass = (Class<Servlet>) Class.forName(className);
Servlet servlet = servletClass.getDeclaredConstructor().newInstance();
servlet.service(request, response);
} else {
StaticResourceController.resourceDispatch(request, response);
}
}
}
四、Servlet类
只要实现了该类就把实现类当做后端接口,根据请求的方法调用doGet或者doPost方法。
package com.xu.servlet;
import com.xu.exception.MethodException;
import java.io.IOException;
public abstract class Servlet {
void service(Request request, Response response) throws IOException {
if (request.getMethod().equalsIgnoreCase("get")) {
response.send(doGet(request, response));
} else if (request.getMethod().equalsIgnoreCase("post")) {
response.send(doPost(request, response));
} else {
throw new MethodException("不支持该方法");
}
}
public abstract Object doGet(Request request, Response response);
public abstract Object doPost(Request request, Response response);
}
为了文章的篇幅,只贴出了核心代码,完整的目录结构如图
实现思路
架构就是大体的思路了,有了思路才能设计大体的架构,但是为了更便于读者理解我就先写架构在写思路。
首先用socket监听客户端的请求,请求进来后调用一个线程去处理。这个线程应该是实例化一个request用来解析请求数据,然后根据解析出来的路径、方法等通过dispatchServlet寻找能够处理的类,不存在就找静态资源,还是不存在就返回404。找到Servlet实现类后得到返回数据,根据返回类型设置相应的请求头、编码等,最后将数据通过response类将数据返回给客户端。
不足
该项目还存在很多地方尚未优化,比如不能长连接,连接又断开耗费资源。连接采用的是BIO不是NIO等。如果面试过对这些名词和他的概念应该很熟悉,时间比较充裕的大学生可以看懂这个项目并且再往这方面优化,指不定可以惊艳到面试官。毕竟做过和知道是两回事
总结
写了这个项目后发现以前在学框架时不懂的知识都弄懂了不少,果然不能光听光看,听懂看懂再做了一遍之后才是自己的。
最后完整的源码我放在我的资源里面,有需要自行下载,其中包含了一个应用该容器实现返回helloWorld的示例代码。看不懂或者发现有不好的地方欢迎评论区或者私信探讨。