通过之前的介绍,我们大致了解了Tomcat的基础功能,即为Servlet生效提供环境支持。那么Tomcat肯定需要提供以下基础功能:
- 提供Socket服务:实现对某些端口的监听,从而实现请求到来时,Tomcat可以感知到。同时该Socket服务也需要支持HTTP协议。
- 封装请求和响应:通过之前的介绍,我们知道在我们开发Servlet时,Web容器已经将Context、Request、Response替我们封装好了,可以直接使用。Tomcat作为一款Servlet容器,肯定要支持这项功能。
- 请求分发:一个Tomcat可以提供多个Web服务,那么Tomcat需要支持将不同的url分发到不同的应用,调用不同的Servlet。
本篇文章我们就来自己实现一个简单的,具有上述基础功能的Servlet容器——MyTomcat。大致的通信过程如下图所示:
1. 基础概念
因为我们自定义处理的web服务器需要处理Http请求,所以必须支持Http协议。这里的支持其实简单的讲就是,能够正确识别客户端浏览器的请求内容,以及按照HTTP协议规定向客户端返回正确格式的返回内容。关于网络相关的基础概念,我们在之前的文章golang socket编程、理解Https中都已经介绍过。这里我们再简单讲一下HTTP协议。
1.1 HTTP协议
Internet的基本协议是TCP/IP协议(传输控制协议和网际协议),目前广泛使用的 FTP、HTTP(超文本传输协议)、Archie Gopher都是建立在TCP/IP上面的应用层协议,不同的协议对应不同的应用。而HTTP协议是Web应用所使用的主要协议。
HTTP协议是基于请求响应模式的。客户端向服务器发送一个请求,请求头包含请求的方法、 URI、协议版本、以及包含请求修饰符、客户端信息和内容的类似MIME的消息结果。服务器则以一个状态行作为响应,相应的内容包括消息协议的版本、成功或者错误编码加上包含服务器信息、实体元信息以及可能的实体内容。
HTTP是无状态协议,依赖于瞬间或者近乎瞬间的请求处理。请求信息被立即发送,理想的情况是没有延时的进行处理,不过,延时还是客观存在的。HTTP有一种内置的机制,在消息的传递时间上由一定的灵活性:超时机制。一个超时就是客户端等待请求消息的返回信息的最长时间。
1.1.1 HTTP请求报文示例
1.1.2 HTTP响应报文示例
1.2 Socket
从我们之前学习的一些概念,可以知道TCP要保证可靠传输,要通过三次握手建立连接,传输数据时,要有滑动窗口、累积确认、分组缓存、流量控制等约束,断开连接时要通过四次挥手断开连接,是一个非常麻烦的协议。如果有个需求,使用TCP协议编程,设计一个客户端和服务端的通信系统,代价将是特别大的。这时候我们肯定想到一个概念——抽象。因为TCP协议非常复杂,不能要求每个程序员都去实现建立连接的三次握手、累计确认、分组缓存,这些应该属于操作系统内核部分,没必要重复开发。但是对于程序员来讲,操作系统需要抽象出一个概念,让上层应用可以使用抽象概念去编程,而这个抽象的概念就是Socket(Socket是操作系统抽象出来出我们更方便使用TCP协议的)。
1.3 为什么要使用应用层HTTP协议
通过上面的介绍,我们知道,通过Socket协议,我们已经可以实现客户端\服务端使用TCP传输协议通信,那么为什么还要定义一些如FTP、HTTP的应用层协议?
其实,虽然通过Socket可以实现客户端\服务端通信,但是通信内容其实是“无意义”的。比如针对浏览器\服务端通信,我们可能有一些特殊的要求,比如超时时间、请求方法、响应码等,不仅仅是交换内容那么简单。所以这时候就需要更细致,满足使用场景的协议。这就是应用层协议的意义。
2. Servlet接口定义
既然要自定义实现Tomcat Web容器,那么必然要抽象出一个接口供程序员使用,就像Servlet一样。这里我们提供一个抽象类MyServlet,只要程序员实现了该抽象类,就可以通过我们的自定义Web容器对外提供服务。
public abstract class MyServlet {
public void doGet(MyRequest request, MyResponse response) {
// 这里其实应该是response返回405这种状态码,因为我们的MyResponse没有这个功能,所以这里直接抛异常了
throw new RuntimeException("不支持的HTTP GET method");
}
public void doPost(MyRequest request, MyResponse response) {
// 这里其实应该是response返回405这种状态码,因为我们的MyResponse没有这个功能,所以这里直接抛异常了
throw new RuntimeException("不支持的HTTP POST method");
}
public void service(MyRequest request, MyResponse response) {
String method = request.getMethod();
if (method.equalsIgnoreCase("POST")) {
doPost(request, response);
} else if (method.equalsIgnoreCase("GET")) {
doGet(request, response);
}
}
}
这里其实参考了HttpServlet的实现,关于HttpServlet,我们在之前的文章透过现象看本质——什么是servlet中已经介绍了。
2. Request\Response抽象
2.1 请求类抽象——MyRequest
用过Socket的都知道,Socket请求都是字节流。又因为我们的Web容器其实是处理Http请求的,所以字节流中的内容其实就是HTTP请求体。我们抽象的请求体对象,必须能够支持HTTP协议的解析。
public class MyRequest {
private String url;
private String method;
public MyRequest(InputStream inputStream) throws IOException {
String httpRequest = getStrFromInputStream(inputStream);
/**
* GET /hello HTTP/1.1
* Host: localhost:9999
* Connection: keep-alive
* Cache-Control: max-age=0
* Upgrade-Insecure-Requests: 1
* User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36
* Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng;q=0.8,application/signed-exchange;v=b3;q=0.9
* Sec-Fetch-Site: cross-site
*/
String httpHead = httpRequest.split("\n")[0];
url = httpHead.split("\\s")[1];
method = httpHead.split("\\s")[0];
}
public String getUrl() {
return url;
}
public String getMethod() {
return method;
}
public void setUrl(String url) {
this.url = url;
}
public void setMethod(String method) {
this.method = method;
}
private String getStrFromInputStream(InputStream inputStream) throws IOException {
InputStreamReader r = new InputStreamReader(inputStream);
BufferedReader br = new BufferedReader(r);
return br.readLine();
}
}
其实核心就是读取上述请求示例,关于读取InputStream的方法,这里我试过很多种,只有当通过InputStreamReader读取的时候,我的这个自定义的Tomcat容器才能正确返回浏览器内容,并在浏览器显式。其他方式比如通过ByteArrayInputStream读取,都是不行的,有兴趣的同学可以尝试一下。
2.2 响应内容抽象——MyResponse
public class MyResponse {
private OutputStream outputStream;
public MyResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}
public void write(String content) {
String html = "http/1.1 200 ok\n" + "Content-type: text/html; charset=utf-8\n" + "\n\n" + "<!DOCTYPE html>\n" + "<html><body>"
+ content + "</body></html>";
PrintWriter out = new PrintWriter(outputStream);
out.println(html);
out.flush();
out.close();
}
}
写响应时,其实就是按照HTTP响应体的要求,写响应。这里MyRequest和MyResponse其实就是我们自定义的Tomcat容器对HTTP协议的支持。
3. Servlet映射
做过Socket开发的同学都知道,为了让我们实现的Servlet生效,我们需要再web.xml配置文件中配置我们的Servlet映射。其实就是告诉Tomcat,什么样的请求应该转发到哪一个Servlet中,这里我们自定义的Web服务器实现就不那么复杂了,直接写死映射。
public class MyServletMapping {
private String servletName;
private String url;
private String clazz;
public MyServletMapping(String servletName, String url, String clazz) {
this.servletName = servletName;
this.url = url;
this.clazz = clazz;
}
public String getServletName() {
return servletName;
}
public void setServletName(String servletName) {
this.servletName = servletName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getClazz() {
return clazz;
}
public void setClazz(String clazz) {
this.clazz = clazz;
}
}
public class MyServletMappingConfig {
public static List<MyServletMapping> myServletMappingList = new ArrayList<>();
static {
/**
* 这个映射关系,Tomcat应该是读取web.xml配置获取的,这里为了方便,我们直接写死了
*/
myServletMappingList.add(new MyServletMapping("helloWorld", "/hello", "com.zhuoli.service.test.HelloWorldServlet"));
}
}
4. Socket实现Web容器
这其实就是整个自定义实现的Tomcat容器的核心——Socket实现Web容器。简单的讲,这里我们需要实现最开始讲的Web容器需要提供的三项功能:
- 提供Socket服务,实现端口监听
- 封装请求和响应
- 请求分发
public class MyTomcat {
private int port;
private Map<String, String> url2ServletMap = new HashMap<>();
public MyTomcat(int port) {
this.port = port;
}
public void start() {
initServletMapping();
try {
// 1. 端口监听
ServerSocket server = new ServerSocket(9999);
System.out.println("MyTomcat start on port " + port);
while (true) {
Socket socket = server.accept();
// 2. 封装Request、Response
MyRequest request = new MyRequest(socket.getInputStream());
MyResponse response = new MyResponse(socket.getOutputStream());
// 3. 请求分发
dispatch(request, response);
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void initServletMapping() {
for (MyServletMapping myServletMapping : MyServletMappingConfig.myServletMappingList) {
url2ServletMap.put(myServletMapping.getUrl(), myServletMapping.getClazz());
}
}
private void dispatch(MyRequest request, MyResponse response) {
String servletClazz = url2ServletMap.get(request.getUrl());
try {
Class<MyServlet> myServletClass = (Class<MyServlet>) Class.forName(servletClazz);
MyServlet myServlet = myServletClass.newInstance();
myServlet.service(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new MyTomcat(9999).start();
}
}
5. 测试
这里我们自定义实现一个HelloWorldServlet,并通过上述main方法启动MyTomcat Web容器。
public class HelloWorldServlet extends MyServlet {
@Override
public void doGet(MyRequest request, MyResponse response) {
response.write("hello world");
}
}
说明我们自定义的Tomcat容器生效了。虽然很简单,但是也可以大致了解到Tomcat的工作流程。此外,我们可以发现,既然自定义Tomcat的核心是通过Socket实现的,那么是不是可以通过NIO实现呢,异或是通过Netty实现。答案是肯定的,有兴趣的大佬,可以尝试使用NIO、Netty、设计模式等高级操作,丰富我们的自定义Tomcat容器。
参考链接: