手撸简易版Tomcat(一)

手撸简易版Tomcat(一)

一、设计服务器架构

在开发我们自己的Web服务器Jerrymouse Server之前,我们先看一下Tomcat的架构。Tomcat是一个开源的Web服务器,它的架构是基于组件的设计,可以将多个组件组合起来使用,用一张图表示如下:

image-20231127101826768

一个Tomcat Server内部可以有多个Service(服务),通常是一个Service。Service内部包含两个组件:

  • Connectors:代表一组Connector(连接器),至少定义一个Connector,也允许定义多个Connector,例如HTTP和HTTPS两个Connector;
  • Engine:代表一个引擎,所有HTTP请求经过Connector后传递给Engine。

在一个Engine内部,可以有一个或多个Host(主机),Host可以根据域名区分,在Host内部,又可以有一个或多个Context(上下文),每个Context对应一个Web App。Context是由路径前缀区分的,如/abc/xyz/分别代表3个Web App,/表示的Web App在Tomcat中表示根Web App。

因此,一个HTTP请求:

1
http://www.example.com/abc/hello

首先根据域名www.example.com定位到某个Host,然后,根据路径前缀/abc定位到某个Context,若路径前缀没有匹配到任何Context,则匹配/Context。在Context内部,就是开发者编写的Web App,一个Context仅包含一个Web App。

可见Tomcat Server是一个全功能的Web服务器,它支持HTTP、HTTPS和AJP等多种Connector,又能同时运行多个Host,每个Host内部,还可以挂载一个或多个Context,对应一个或多个Web App。

我们设计Jerrymouse Server就没必要搞这么复杂,可以大幅简化为:

  • 仅一个HTTP Connector,不支持HTTPS;
  • 仅支持挂载到/的一个Context,不支持多个Host与多个Context。

因为只有一个Context,所以也只能有一个Web App。Jerrymouse Server的架构如下:

image-20231127102141915

有的同学会担心,如果要运行多个Web App怎么办?这个问题很容易解决:运行多个Jerrymouse Server就可以运行多个Web App了。

还有的同学会担心,只支持HTTP,如果一定要使用HTTPS怎么办?HTTPS可以部署在网关,通过网关将HTTPS请求转发为HTTP请求给Jerrymouse Server即可。部署一个Nginx就可以充当网关:

image-20231127102223805

此外,Nginx还可以定义多个Host,根据Host转发给不同的Jerrymouse Server,所以,我们专注于实现一个仅支持HTTP、仅支持一个Web App的Web服务器,把HTTPS、HTTP/2、HTTP/3、Host、Cluster(集群)等功能全部扔给Nginx即可。

二、Servlet规范分析

一个Java Web App通常打包为.war文件,并且可以部署到Tomcat、Jetty等多种Web服务器上。为什么一个Java Web App基本上可以无修改地部署到多种Web服务器上呢?原因就在于Servlet规范

Servlet规范是Java Servlet API的规范,用于定义Web服务器如何处理HTTP请求和响应。Servlet规范有一组接口,对于Web App来说,操作的是接口,而真正对应的实现类,则由各个Web Server实现,这样一来,Java Web App实际上编译的时候仅用到了Servlet规范定义的接口,只要每个Web服务器在实现Servlet接口时严格按照规范实现,就可以保证一个Web App可以正常运行在多种Web服务器上:

image-20231127102924754

对于Web应用程序,Servlet规范是非常重要的。Servlet规范有好几个版本,每个版本都有一些新的功能。以下是一些常见版本的新功能:

  • Servlet 1.0:定义了Servlet组件,一个Servlet组件运行在Servlet容器(Container)中,通过与容器交互,就可以响应一个HTTP请求;

  • Servlet 2.0:定义了JSP组件,一个JSP页面可以被动态编译为Servlet组件;

  • Servlet 2.4:定义了Filter(过滤器)组件,可以实现过滤功能;

  • Servlet 2.5:支持注解,提供了ServletContextListener接口,增加了一些安全性相关的特性;

  • Servlet 3.0:支持异步处理的Servlet,支持注解配置Servlet和过滤器,增加了SessionCookieConfig接口;

  • Servlet 3.1:提供了WebSocket的支持,增加了对HTTP请求和响应的流式操作的支持,增加了对HTTP协议的新特性的支持;

  • Servlet 4.0:支持HTTP/2的新特性,提供了HTTP/2的Server Push等特性;

  • Servlet 5.0:主要是把javax.servlet包名改成了jakarta.servlet

  • Servlet 6.0:继续增加一些新功能,并废除一部分功能。

目前最新的Servlet版本是6.0,我们开发Jerrymouse Server也是基于最新的Servlet 6.0。

当Servlet容器接收到用户的HTTP请求后,由容器负责把请求转换为HttpServletRequestHttpServletResponse对象,分别代表HTTP请求和响应,然后,经过若干个Filter组件后,到达最终的Servlet组件,由Servlet组件完成HTTP处理,将响应写入HttpServletResponse对象:

image-20231127103059626

其中,ServletContext代表整个容器的信息,如果容器实现了ServletContext接口,也可以把ServletContext可以看作容器本身。ServletContextHttpServletRequestHttpServletResponse都是接口,具体实现由Web服务器完成。FilterServlet组件也是接口,但具体实现由Web App完成。此外,还有一种Listener接口,可以监听各种事件,但不直接参与处理HTTP请求,具体实现由Web App完成,何时调用则由容器决定。因此,针对Web App的三大组件:ServletFilterListener都是运行在容器中的组件,只有容器才能主动调用它们。(此处略去JSP组件,因为我们不打算支持JSP)

对于Jerrymouse服务器来说,开发服务器就必须实现Servlet容器本身,容器实现ServletContext接口,容器内部管理若干个ServletFilterListener组件。

对每个请求,需要创建HttpServletRequestHttpServletResponse实例,查找并匹配合适的一组Filter和一个Servlet,让它们处理HTTP请求。在处理过程中,会产生各种事件,容器负责将产生的事件发送到Listener组件处理。

以上就是我们编写Servlet容器按照Servlet规范所必须的全部功能。

三、实现HTTP服务器

我们设计的Jerrymouse Server的架构如下:

image-20231127103341619

在实现Servlet支持之前,我们先实现一个HTTP Connector。

所谓Connector,这里可以简化为一个能处理HTTP请求的服务器,HTTP/1.x协议是基于TCP连接的一个简单的请求-响应协议,首先由浏览器发送请求:

1
2
3
4
5
GET /hello HTTP/1.1
Host: www.example.com
User-Agent: curl/7.88.1
Accept: */*

请求头指出了请求的方法GET,主机www.example.com,路径/hello,接下来服务器解析请求,输出响应:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: Simple HttpServer/1.0
Date: Fri, 07 Jul 2023 23:15:09 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 22
Connection: keep-alive

<h1>Hello, world.</h1>

响应返回状态码200,每个响应头Header: Value,最后是以\r\n\r\n分隔的响应内容。

所以我们编写HTTP Server实际上就是:

  1. 监听TCP端口,等待浏览器连接;
  2. 接受TCP连接后,创建一个线程处理该TCP连接:
    1. 接收浏览器发送的HTTP请求;
    2. 解析HTTP请求;
    3. 处理请求;
    4. 发送HTTP响应;
    5. 重复1~4直到TCP连接关闭。

整个流程不复杂,但是处理步骤比较繁琐,尤其是解析HTTP请求,是个体力活,不但要去读HTTP协议手册,还要做大量的兼容性测试。

所以我们选择直接使用JDK内置的jdk.httpserverjdk.httpserver从JDK 9开始作为一个公开模块可以直接使用,它的包是com.sun.net.httpserver,主要提供以下几个类:

  • HttpServer:通过指定IP地址和端口号,定义一个HTTP服务实例;
  • HttpHandler:处理HTTP请求的核心接口,必须实现handle(HttpExchange)方法;
  • HttpExchange:可以读取HTTP请求的输入,并将HTTP响应输出给它。

一个能处理HTTP请求的简单类实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SimpleHttpHandler implements HttpHandler {
final Logger logger = LoggerFactory.getLogger(getClass());

@Override
public void handle(HttpExchange exchange) throws IOException {
// 获取请求方法、URI、Path、Query等:
String method = exchange.getRequestMethod();
URI uri = exchange.getRequestURI();
String path = uri.getPath();
String query = uri.getRawQuery();
logger.info("{}: {}?{}", method, path, query);
// 输出响应的Header:
Headers respHeaders = exchange.getResponseHeaders();
respHeaders.set("Content-Type", "text/html; charset=utf-8");
respHeaders.set("Cache-Control", "no-cache");
// 设置200响应:
exchange.sendResponseHeaders(200, 0);
// 输出响应的内容:
String s = "<h1>Hello, world.</h1><p>" + LocalDateTime.now().withNano(0) + "</p>";
try (OutputStream out = exchange.getResponseBody()) {
out.write(s.getBytes(StandardCharsets.UTF_8));
}
}
}

可见,HttpExchange封装了HTTP请求和响应,我们不再需要解析原始的HTTP请求,也无需构造原始的HTTP响应,而是通过HttpExchange间接操作,大大简化了HTTP请求的处理。

最后写一个SimpleHttpServer把启动HttpServer、处理请求连起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package org.learn;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;

public class SimpleHttpServer implements HttpHandler, AutoCloseable {
final Logger logger = LoggerFactory.getLogger(getClass());
final HttpServer httpServer;
final String host;
final int port;

public SimpleHttpServer(String host, int port) throws IOException {
this.host = host;
this.port = port;
// 定义一个HTTP服务器实例
this.httpServer = HttpServer.create(new InetSocketAddress(host, port), 0);
// 定义HTTP服务器实例的统一路径映射和处理程序handler
this.httpServer.createContext("/", this);
// 启动HTTP服务器实例
this.httpServer.start();
logger.info("start jerrymouse http server at {}:{}", host, port);
}

public static void main(String[] args) {
String host = "0.0.0.0";
int port = 8080;
try (SimpleHttpServer connector = new SimpleHttpServer(host, port)) {
for (;;) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void handle(HttpExchange exchange) throws IOException {
// 获取请求方法、URI、Path、Query等
String method = exchange.getRequestMethod();
URI uri = exchange.getRequestURI();
String path = uri.getPath();
String query = uri.getQuery();
logger.info("{}: {}?{}", method, path, query);
// 输出响应的Header
Headers respHeaders = exchange.getResponseHeaders();
respHeaders.set("Content-Type", "text/html;charset=utf-8");
respHeaders.set("Cache-Control", "no-cache");
// 设置200响应,采用分块传输
exchange.sendResponseHeaders(200, 0);
// 输出响应的内容
String data = "<h1>Hello, world.</h1><p>" + LocalDateTime.now().withNano(0) + "</p>";
try (OutputStream out = exchange.getResponseBody()) {
out.write(data.getBytes(StandardCharsets.UTF_8));
}
}

@Override
public void close() throws Exception {
this.httpServer.stop(3);
}
}

image-20231127113418131

运行后打开浏览器,访问http://localhost:8080,可以看到如下输出:

image-20231127105121210

这样,我们就成功实现了一个简单的HTTP Server。

四、实现Servlet服务器

在上一节,我们已经成功实现了一个简单的HTTP服务器,但是,好像和Servlet没啥关系,因为整个操作都是基于HttpExchange接口做的。而Servlet处理HTTP的接口是基于HttpServletRequestHttpServletResponse,前者负责读取HTTP请求,后者负责写入HTTP响应。

怎么把基于HttpExchange的操作转换为基于HttpServletRequestHttpServletResponse?答案是使用Adapter模式

首先我们定义HttpExchangeAdapter,它持有一个HttpExchange实例,并实现HttpExchangeRequestHttpExchangeResponse接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public interface HttpExchangeRequest {
String getRequestMethod();
URI getRequestURI();
}

public interface HttpExchangeResponse {
Headers getResponseHeaders();
void sendResponseHeaders(int rCode, long responseLength) throws IOException;
OutputStream getResponseBody();
}

public class HttpExchangeAdapter implements HttpExchangeRequest, HttpExchangeResponse{
final HttpExchange exchange;

public HttpExchangeAdapter(HttpExchange exchange) {
this.exchange = exchange;
}

@Override
public String getRequestMethod() {
return this.exchange.getRequestMethod();
}

@Override
public URI getRequestURI() {
return this.exchange.getRequestURI();
}

@Override
public Headers getResponseHeaders() {
return this.exchange.getResponseHeaders();
}

@Override
public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
this.exchange.sendResponseHeaders(rCode, responseLength);
}

@Override
public OutputStream getResponseBody() {
return this.exchange.getResponseBody();
}
}

紧接着我们编写HttpServletRequestImpl,它内部持有HttpExchangeRequest,并实现了HttpServletRequest接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class HttpServletRequestImpl implements HttpServletRequest {

final HttpExchangeRequest exchangeRequest;

public HttpServletRequestImpl(HttpExchangeRequest exchangeRequest) {
this.exchangeRequest = exchangeRequest;
}

@Override
public String getParameter(String name) {
String query = this.exchangeRequest.getRequestURI().getRawQuery();
if (query != null) {
Map<String, String> params = parseQuery(query);
return params.get(name);
}
return null;
}

Map<String, String> parseQuery(String query) {
if (query == null || query.isEmpty()) {
return Map.of();
}
String[] ss = Pattern.compile("\\&").split(query);
Map<String, String> map = new HashMap<>();
for (String s : ss) {
int n = s.indexOf('=');
if (n >= 1) {
String key = s.substring(0, n);
String value = s.substring(n + 1);
map.putIfAbsent(key, URLDecoder.decode(value, StandardCharsets.UTF_8));
}
}
return map;
}

@Override
public String getMethod() {
return this.exchangeRequest.getRequestMethod();
}

// not implemented yet:
// 未实现的若干个方法...
}

同理,编写HttpServletResponseImpl如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class HttpServletResponseImpl implements HttpServletResponse {
final HttpExchangeResponse exchangeResponse;

public HttpServletResponseImpl(HttpExchangeResponse exchangeResponse) {
this.exchangeResponse = exchangeResponse;
this.setContentType("text/html");
}

@Override
public PrintWriter getWriter() throws IOException {
this.exchangeResponse.sendResponseHeaders(200, 0);
return new PrintWriter(this.exchangeResponse.getResponseBody(), true, StandardCharsets.UTF_8);
}

@Override
public void setContentType(String s) {
setHeader("Content-Type", s);
}

@Override
public void setHeader(String name, String value) {
this.exchangeResponse.getResponseHeaders().set(name, value);
}

// not implemented yet:
// 未实现的若干个方法...
}

用一个图表示从HttpExchange转换为HttpServletRequestHttpServletResponse如下:

image-20231127112317491

接下来我们改造处理HTTP请求的HttpHandler接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class HttpConnector implements HttpHandler, AutoCloseable {
final Logger logger = LoggerFactory.getLogger(getClass());
final HttpServer httpServer;

public HttpConnector() throws IOException {
String host = "0.0.0.0";
int port = 8080;
this.httpServer = HttpServer.create(new InetSocketAddress(host, port), 0);
this.httpServer.createContext("/", this);
this.httpServer.start();
logger.info("start jerrymouse http server at {}:{}", host, port);
}

@Override
public void handle(HttpExchange exchange) throws IOException {
// exchange每个请求线程都是独有的,不存在线程安全问题
logger.info("{}: {}?{}", exchange.getRequestMethod(), exchange.getRequestURI().getPath(), exchange.getRequestURI().getRawQuery());
// 创建HttpExchangeRequest和HttpExchangeResponse的HttpExchangeAdapter适配器
HttpExchangeAdapter adapter = new HttpExchangeAdapter(exchange);
// 创建HttpServletRequest的实现类,底层操作适配器对象
HttpServletRequestImpl request = new HttpServletRequestImpl(adapter);
// 创建HttpServletResponse的实现类,底层操作适配器对象
HttpServletResponseImpl response = new HttpServletResponseImpl(adapter);
try {
process(request, response);
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}

private void process(HttpServletRequest request, HttpServletResponse response) throws IOException {
String name = request.getParameter("name");
String html = "<h1>Hello, " + (name == null ? "world" : name) + ".</h1>";
response.setContentType("text/html");
PrintWriter pw = response.getWriter();
pw.write(html);
pw.close();
}

@Override
public void close() throws Exception {
this.httpServer.stop(3);
}
}

handle(HttpExchange)方法内部,我们创建的对象如下:

  1. HttpExchangeAdapter实例:它内部引用了jdk.httpserver提供的HttpExchange实例;
  2. HttpServletRequestImpl实例:它内部引用了HttpExchangeAdapter实例,但是转换为HttpExchangeRequest接口;
  3. HttpServletResponseImpl实例:它内部引用了HttpExchangeAdapter实例,但是转换为HttpExchangeResponse接口。

所以实际上创建的实例只有3个。最后调用process(HttpServletRequest, HttpServletResponse)方法,这个方法内部就可以按照Servlet标准来处理HTTP请求了,因为方法参数是标准的Servlet接口:

1
2
3
4
5
6
7
8
private void process(HttpServletRequest request, HttpServletResponse response) throws IOException {
String name = request.getParameter("name");
String html = "<h1>Hello, " + (name == null ? "world" : name) + ".</h1>";
response.setContentType("text/html");
PrintWriter pw = response.getWriter();
pw.write(html);
pw.close();
}

目前,我们仅实现了代码调用时用到的getParameter()setContentType()getWriter()这几个方法。如果补全HttpServletRequestHttpServletResponse接口所有的方法定义,我们就得到了完整的HttpServletRequestHttpServletResponse接口实现。

image-20231127113209544

运行代码,在浏览器输入http://localhost:8080/?name=World,结果如下:

image-20231127113324759

五、Servlet组件总览

现在,我们已经成功实现了一个HttpConnector,并且,将jdk.httpserver提供的输入输出HttpExchange转换为Servlet标准定义的HttpServletRequestHttpServletResponse接口,最终处理方法如下:

1
2
3
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO
}

这样,我们就有了处理HttpServletRequestHttpServletResponse的入口,回顾一下Jerrymouse设计的架构图:

image-20231127113623190

我们让HttpConnector持有一个Context实例,在Context定义process(req, resp)方法:

image-20231127113645951

这个Context组件本质上可以视为Servlet规范定义的ServletContext,而规范定义的Servlet、Filter、Listener等组件,就可以让ServletContext管理,后续的服务器设计就简化为如何实现ServletContext,以及如何管理Servlet、Filter、Listener等组件。