设计思路
我们在开始动手之前,首先要设计一下我们的软件。我们的服务器要实现的功能是可以接收和处理静态资源请求和数据请求。静态资源请求可以根据路径去classpath下寻找对应的静态文件并返回,数据请求就需要用户挂载在服务器上的自定义Servlet去处理,服务器只负责在启动阶段加载该自定义的类即可。
架构方面,我们要借鉴Tomcat的生命周期和模块化的思想,使我们的软件结构清晰,易于扩展。好了,下面我们就开始吧。
创建服务
我们首先来创建一个简单的Maven工程。引入如下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.rubin</groupId>
<artifactId>minicat</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
服务器配置文件
我们先来编写服务器的配置文件。在配置文件的设计之时也就把我们的模块模型设计好了,我们采用xml格式的文件作为配置文件。配置文件的内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<Server name="defaultServer">
<Service name="defaultService">
<Connector port="8081"/>
<Engine name="defaultEngine">
<Host name="localhost" appBase="webapps"/>
</Engine>
</Service>
</Server>
从配置文件中,我们可以看到:我们的服务器分为Server容器、Service容器、Connector组件、Engine组件和Host组件。因为我们是简易的Tomcat,所以我们的组件不支持多实例。定义成这样结构的原因是因为方便后期扩展来支持多实例。
我们说一下重要组件的属性含义:
- Connector.port:指定了我们服务器监听的HTTP请求端口
- Host.appBase:指定了我们服务器加载自定义Servlet模块的基础目录,该目录可以是相对路径也可以是绝对路径
我们在来定义一下自定义模块的配置文件web.xml文件的文件格式:
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>test1</servlet-name>
<servlet-class>com.rubin.TestServlet1</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>test1</servlet-name>
<url-pattern>/hi</url-pattern>
</servlet-mapping>
</web-app>
这个我们就不解释了,大家都很熟悉。我们服务器启动阶段会读取该文件去加载自定义的Servlet类,后面根据该类配置的映射路径去调用自定义Servlet的方法。
定义顶级接口以及工具类
生命周期接口
我们先定义一个生命周期接口,来保证我们组件的生命周期是一致的。具体定义如下:
/**
* 生命周期组件
* Created by RubinChu on 2021/5/2 11:22
*/
public interface LifeCycle {
/**
* 初始化
*/
void init();
/**
* 启动
*/
void start();
/**
* 销毁
*/
void destroy();
}
定义工具类
我们在来定义一些需要的工具类。首先是静态资源的处理类:
public class StaticResourceUtil {
/**
* 获取静态资源文件的绝对路径
*
* @param path
* @return
*/
public static String getAbsolutePath(String path) {
String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
return absolutePath.replaceAll("\\\\", "/") + path;
}
/**
* 读取静态资源文件输入流,通过输出流输出
*/
public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
int count = 0;
while (count == 0) {
count = inputStream.available();
}
int resourceSize = count;
// 输出http请求头,然后再输出具体内容
outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
// 读取内容输出
long written = 0;// 已经读取的内容长度
int byteSize = 1024; // 计划每次缓冲的长度
byte[] bytes = new byte[byteSize];
while (written < resourceSize) {
if (written + byteSize > resourceSize) { // 说明剩余未读取大小不足一个1024长度,那就按真实长度处理
byteSize = (int) (resourceSize - written); // 剩余的文件内容长度
bytes = new byte[byteSize];
}
inputStream.read(bytes);
outputStream.write(bytes);
outputStream.flush();
written += byteSize;
}
}
}
再定义关于HTTP请求和响应的处理工具类:
package com.rubin.utils;
/**
* http协议工具类,主要是提供响应头信息,这里我们只提供200和404的情况
*/
public class HttpProtocolUtil {
/**
* 为响应码200提供请求头信息
*
* @return
*/
public static String getHttpHeader200(long contentLength) {
return "HTTP/1.1 200 OK \n" +
"Content-Type: text/html \n" +
"Content-Length: " + contentLength + " \n" +
"\r\n";
}
/**
* 为响应码404提供请求头信息(此处也包含了数据内容)
*
* @return
*/
public static String getHttpHeader404() {
String str404 = "<h1>404 not found</h1>";
return "HTTP/1.1 404 NOT Found \n" +
"Content-Type: text/html \n" +
"Content-Length: " + str404.getBytes().length + " \n" +
"\r\n" + str404;
}
}
再定义关于xml文档的读取工具类:
/**
* Created by RubinChu on 2021/5/2 12:58
*/
public class ServerXmlReader {
private Map<String, String> config = new HashMap<>();
public ServerXmlReader() {
InputStream resourceAsStream = ServerXmlReader.class.getClassLoader().getResourceAsStream("server.xml");
SAXReader saxReader = new SAXReader();
// 读取配置信息,封装进缓存中
try {
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
parseElement(rootElement);
} catch (DocumentException e) {
e.printStackTrace();
}
}
/**
* 解析根元素下的所有元素
*
* @param element
*/
private void parseElement(Element element) {
if (element == null) {
return;
}
String elementName = element.getName();
Iterator iterator = element.attributeIterator();
while (iterator.hasNext()) {
DefaultAttribute defaultAttribute = (DefaultAttribute) iterator.next();
String key = elementName + "." + defaultAttribute.getName();
String value = defaultAttribute.getValue();
config.put(key, value);
}
List childrenElements = element.elements();
if (childrenElements != null && childrenElements.size() > 0) {
for (Object childrenElement : childrenElements) {
parseElement((Element) childrenElement);
}
}
}
/**
* 获取配置信息 key=标签名称 + 属性值key
*
* @param key
* @return
*/
public String read(String key) {
return config.get(key);
}
}
/**
* Created by RubinChu on 2021/5/2 12:58
*/
public class WebXmlReader {
private Map<String, String> config = new HashMap<>();
public WebXmlReader(File webXml) {
try {
Document document = new SAXReader().read(new FileInputStream(webXml));
Element rootElement = document.getRootElement();
List<Element> selectNodes = rootElement.selectNodes("//servlet");
for (int i = 0; i < selectNodes.size(); i++) {
Element element = selectNodes.get(i);
Element servletNameElement = (Element) element.selectSingleNode("servlet-name");
String servletName = servletNameElement.getStringValue();
Element servletClassElement = (Element) element.selectSingleNode("servlet-class");
String servletClass = servletClassElement.getStringValue();
Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
config.put(urlPattern, servletClass);
}
} catch (DocumentException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取配置信息
*
* @return
*/
public Set<Map.Entry<String, String>> read() {
return config.entrySet();
}
}
再来定义我们关于Servlet相关的基础类和接口:
/**
* Created by RubinChu on 2021/5/3 18:33
*/
public interface Servlet {
void init() throws Exception;
void destroy() throws Exception;
void service(Request request, Response response) throws Exception;
}
public abstract class HttpServlet implements Servlet{
public abstract void doGet(Request request,Response response);
public abstract void doPost(Request request,Response response);
@Override
public void service(Request request, Response response) throws Exception {
if("GET".equalsIgnoreCase(request.getMethod())) {
doGet(request,response);
}else{
doPost(request,response);
}
}
}
再定义我们基础的请求相应对象,供Connector组件转化流使用:
/**
* 把请求信息封装为Request对象(根据InputSteam输入流封装)
*/
public class Request {
private String method; // 请求方式,比如GET/POST
private String url; // 例如 /,/index.html
private InputStream inputStream; // 输入流,其他属性从输入流中解析出来
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public Request() {
}
// 构造器,输入流传入
public Request(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
// 从输入流中获取请求信息
int count = 0;
while (count == 0) {
count = inputStream.available();
}
byte[] bytes = new byte[count];
inputStream.read(bytes);
String inputStr = new String(bytes);
// 获取第一行请求头信息
String firstLineStr = inputStr.split("\\n")[0]; // GET / HTTP/1.1
String[] strings = firstLineStr.split(" ");
this.method = strings[0];
this.url = strings[1];
System.out.println("=====>>method:" + method);
System.out.println("=====>>url:" + url);
}
}
/**
* 封装Response对象,需要依赖于OutputStream
*
* 该对象需要提供核心方法,输出html
*/
public class Response {
private OutputStream outputStream;
public Response() {
}
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
// 使用输出流输出指定字符串
private void output(String content) throws IOException {
outputStream.write(content.getBytes());
}
public void write(String content) throws IOException {
String httpHeader200 = HttpProtocolUtil.getHttpHeader200(content.length());
output(httpHeader200 + content);
}
/**
*
* @param path url,随后要根据url来获取到静态资源的绝对路径,进一步根据绝对路径读取该静态资源文件,最终通过
* 输出流输出
* /-----> classes
*/
public void outputStaticFile(String path) throws IOException {
// 获取静态资源文件的绝对路径
String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);
// 输入静态资源文件
File file = new File(absoluteResourcePath);
if(file.exists() && file.isFile()) {
// 读取静态资源文件,输出静态资源
StaticResourceUtil.outputStaticResource(new FileInputStream(file),outputStream);
}else{
// 输出404
output(HttpProtocolUtil.getHttpHeader404());
}
}
}
最后,来定义一下我们自定义的类加载器。这里要多说一下,我们平时都学双亲委派机制,这个机制很好的保护了我们的源码并且减少了不必要的类的开销。但是,用户挂载的多个应用之间是要隔离开的。也就是说,相同名称相同包名的类在不同模块下是允许存在的。这就违背了双亲委派机制。所以我们传统的类加载器就不满足我们的需求了,我们就需要自定义一个类加载器来满足我们的需求。
这样的话,我们这个服务器中就包含两种类加载器。一类是传统的加载器,用于加载我们服务器的相关类;一类就是我们自定义的类加载器,来加载用户挂在的所有项目的所有Servlet的实现类,并实现应用隔离。
类加载器定义如下:
/**
* Created by RubinChu on 2021/5/3 18:56
*/
public class ServletClassLoader extends ClassLoader {
private String path;
public ServletClassLoader(String path) {
this.path = path;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] cByte = this.loadClassData(name);
return defineClass(name, cByte, 0, cByte.length);
}
private byte[] loadClassData(String name) {
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(path + File.separator + name.replace(".", File.separator) + ".class"));
out = new ByteArrayOutputStream();
int c;
while ((c = in.read()) != -1) {
out.write(c);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
定义容器
我们容器的定义,就按照server.xml一层一层定义就好。
Server
/**
* 最外层容器 启动一个项目就是启动一个Server
* Created by RubinChu on 2021/5/2 11:20
*/
public class Server implements LifeCycle {
private String name;
private Service service;
/**
* 初始化
*/
@Override
public void init() {
ServerXmlReader serverXmlReader = new ServerXmlReader();
this.name = serverXmlReader.read("Server.name");
this.service = new Service();
service.init();
}
/**
* 启动
*/
@Override
public void start() {
service.start();
}
/**
* 销毁
*/
@Override
public void destroy() {
service.destroy();
}
}
Service
/**
* Created by RubinChu on 2021/5/2 11:25
*/
public class Service implements LifeCycle {
private String name;
private Connector connector;
/**
* 初始化
*/
@Override
public void init() {
ServerXmlReader serverXmlReader = new ServerXmlReader();
this.name = serverXmlReader.read("Service.name");
this.connector = new Connector();
connector.init();
}
/**
* 启动
*/
@Override
public void start() {
connector.start();
}
/**
* 销毁
*/
@Override
public void destroy() {
connector.destroy();
}
}
Connector
/**
* Created by RubinChu on 2021/5/2 11:29
*/
public class Connector implements LifeCycle {
private Engine engine;
private Integer port = 8080;
private ServerSocket serverSocket;
/**
* 初始化
*/
@Override
public void init() {
initConnector();
this.engine = new Engine();
engine.init();
}
/**
* 初始化Connector
*/
private void initConnector() {
this.port = getPort();
try {
this.serverSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取配置的port
*
* @return
*/
private Integer getPort() {
String portKey = "Connector.port";
ServerXmlReader serverXmlReader = new ServerXmlReader();
String portStr = serverXmlReader.read(portKey);
return portStr == null || "".equals(portStr) ? port : Integer.valueOf(portStr);
}
/**
* 启动
*/
@Override
public void start() {
engine.start();
// 定义一个线程池
int corePoolSize = 10;
int maximumPoolSize =50;
long keepAliveTime = 100L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
System.out.println("Server start on port: " + port);
while (true) {
try {
Socket socket = serverSocket.accept();
threadPoolExecutor.execute(new RequestProcessor(socket, engine));
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 销毁
*/
@Override
public void destroy() {
engine.destroy();
}
}
public class RequestProcessor extends Thread {
private Socket socket;
private Engine engine;
public RequestProcessor(Socket socket, Engine engine) {
this.socket = socket;
this.engine = engine;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
// 封装Request对象和Response对象
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
Servlet servlet = engine.getHost().selectServlet(request.getUrl());
if (servlet == null) {
response.outputStaticFile(request.getUrl());
} else {
servlet.service(request, response);
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Engine
/**
* Created by RubinChu on 2021/5/2 13:24
*/
public class Engine implements LifeCycle {
private String name;
private Host host;
public Host getHost() {
return host;
}
/**
* 初始化
*/
@Override
public void init() {
ServerXmlReader serverXmlReader = new ServerXmlReader();
this.name = serverXmlReader.read("Engine.name");
this.host = new Host();
this.host.init();
}
/**
* 启动
*/
@Override
public void start() {
host.start();
}
/**
* 销毁
*/
@Override
public void destroy() {
host.destroy();
}
}
Host
/**
* Created by RubinChu on 2021/5/2 11:38
*/
public class Host implements LifeCycle {
private String name;
private String appBase;
private List<Mapper> mappers;
/**
* 初始化
*/
@Override
public void init() {
ServerXmlReader serverXmlReader = new ServerXmlReader();
this.name = serverXmlReader.read("Host.name");
this.appBase = serverXmlReader.read("Host.appBase");
initMappers();
}
/**
* 初始化映射器
*/
private void initMappers() {
mappers = new ArrayList<>();
File baseFile = new File(appBase);
if (!baseFile.exists()) {
appBase = StaticResourceUtil.getAbsolutePath(appBase);
baseFile = new File(appBase);
}
if (baseFile.exists() && baseFile.isDirectory()) {
Arrays.stream(baseFile.listFiles()).forEach(file -> {
if (file.isDirectory()) {
Mapper mapper = new Mapper(file);
mappers.add(mapper);
}
});
} else {
throw new RuntimeException("the appBase : " + appBase + "is not available");
}
}
/**
* 启动
*/
@Override
public void start() {
}
/**
* 销毁
*/
@Override
public void destroy() {
}
public Servlet selectServlet(String url) {
for (Mapper mapper : mappers) {
Servlet servlet = mapper.selectServlet(url);
if (servlet != null) {
return servlet;
}
}
return null;
}
}
Mapper
/**
* Created by RubinChu on 2021/5/2 13:42
*/
public class Mapper {
private Map<String, MappedContent> mappedContentMap = new HashMap<>();
public Mapper(File baseFolder) {
String contentName = baseFolder.getName();
MappedContent mappedContent = new MappedContent(contentName, baseFolder);
mappedContentMap.put(contentName, mappedContent);
}
public Servlet selectServlet(String url) {
if (url.startsWith("/")) {
url = url.substring(1);
}
if (url.indexOf("/") == -1) {
return null;
}
String contentName = url.substring(0, url.indexOf("/"));
MappedContent mappedContent = mappedContentMap.get(contentName);
if (mappedContent == null) {
return null;
}
String uri = url.substring(contentName.length());
return mappedContent.selectServlet(uri);
}
private static class MappedContent {
private String contentName;
private File baseFolder;
private Map<String, Servlet> servletMap;
public MappedContent(String contentName, File baseFolder) {
this.contentName = contentName;
this.baseFolder = baseFolder;
this.servletMap = new HashMap<>();
initServlets();
}
private void initServlets() {
File webXml = new File(baseFolder.getPath() + File.separator + "web.xml");
if (webXml.exists() && webXml.isFile()) {
WebXmlReader webXmlReader = new WebXmlReader(webXml);
webXmlReader.read().forEach(entry -> {
servletMap.put(entry.getKey(), loadServletInstance(entry.getValue()));
});
}
}
private Servlet loadServletInstance(String value) {
try {
Class clazz = new ServletClassLoader(baseFolder.getPath()).findClass(value);
return (Servlet) clazz.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
throw new RuntimeException("find class " + value + "error");
}
public Servlet selectServlet(String uri) {
return servletMap.get(uri);
}
}
}
启动类
/**
* 启动类
* Created by RubinChu on 2021/5/2 11:20
*/
public class Bootstrap {
public static void main(String[] args) {
Server server = new Server();
server.init();
server.start();
}
}
需要注意的点
我们按照上面的章节定义好之后。我们的服务器就基本写完了。这里提出几点上述需要注意的地方:
- Connector中的初始化就是初始化了一个ServerSocket来监听端口,使用RequestProcessor来处理连接。这是很典型的网络通信编程。这里可以优化成NIO的方式来提升服务器性能。
- Mapper的解析过程以及请求的处理过程可以好好看一下流程,这里也不是很复杂,就不过多讲解了。
测试
我们可以在我们项目下新建一个静态文件index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>static resource</title>
</head>
<body>
Hello Minicat-static resource!
</body>
</html>
新建一个目录webapps,再在目录里面新建两个项目,结构如下:

两个项目的web.xml和TestServlet源码如下:
test1:
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>test1</servlet-name>
<servlet-class>com.rubin.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>test1</servlet-name>
<url-pattern>/hi</url-pattern>
</servlet-mapping>
</web-app>
public class TestServlet extends HttpServlet {
public TestServlet() {
}
public void doGet(Request request, Response response) {
try {
response.write("TestServlet1 GET");
} catch (IOException var4) {
var4.printStackTrace();
}
}
public void doPost(Request request, Response response) {
try {
response.write("TestServlet1 POST");
} catch (IOException var4) {
var4.printStackTrace();
}
}
public void init() throws Exception {
System.out.println("TestServlet1 init...");
}
public void destroy() throws Exception {
}
}
test2:
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>test2</servlet-name>
<servlet-class>com.rubin.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>test2</servlet-name>
<url-pattern>/hi</url-pattern>
</servlet-mapping>
</web-app>
public class TestServlet extends HttpServlet {
public TestServlet() {
}
public void doGet(Request request, Response response) {
try {
response.write("TestServlet2 GET");
} catch (IOException var4) {
var4.printStackTrace();
}
}
public void doPost(Request request, Response response) {
try {
response.write("TestServlet2 POST");
} catch (IOException var4) {
var4.printStackTrace();
}
}
public void init() throws Exception {
System.out.println("TestServlet2 init...");
}
public void destroy() throws Exception {
}
}
至此,我们的测试环境就弄好了。这里需要说明一下,两个自定义Servlet类必须预先编译好,可以在本项目内先写好,在编译,然后移动到项目目录下(包结构一定要对,比如说类中定义package是com.rubin.test,那么该类文件就应该在com/rubin/test文件夹下)。
测试的话,我们也可启动服务。输入以下三个网址来验证:
http://127.0.0.1:8081/index.html

http://127.0.0.1:8081/test1/hi

http://127.0.0.1:8081/test2/hi

文章评论