前言
在Java生态中,Tomcat的应用可谓经久不衰,众多Java Web应用都依赖它来进行部署。
虽然我们经常使用它,但是如果不深入了解的话,它对我们来说就一直是一个黑盒。就单纯的作为一个使用者来说,肯定也知道它内部应用了很多知识
- 它是一个Web服务器,支持HTTP与HTTPS协议。
- 它支持并发,内部肯定存在多线程编码。
- 它支持会话连接,内部有Session的实现。
- 它能打印应用运行的日志,内部含有日志模块。
- 它是一个Servlet容器,Servlet究竟是什么?
等等…,这里不再一一列举。可见如果你真正的去将Tomcat深刻了解一遍的话,能学习或者巩固不少知识点,对众多编程技术的应用也是一个很出色的参考案例。
那怎么去深入学习它呢?网络上相关的体系教程很少,几乎没有;相关书籍也是凤毛麟角。我翻看了各大论坛,线索都指向了一本外文书籍《How Tomcat Works》,它也有中文译本《深入剖析Tomcat》。这本书籍很老,是由Budi Kurniawan于2004年出版的,文中代码也是基于jdk 1.4来编写的。不过虽然这本书年代久远,但是它对Tomcat的讲解却是非常详细,更是带着读者从零到一来编写了一个简易版的Tomcat。
所以我的设想是:以《深入剖析Tomcat》作为参照,按照文章中的代码示例来一步一步造出一个完整的Tomcat出来。
由于这本书的示例代码太久远了,当我把项目下载下来打开后,选择使用jdk 1.8来编译,出现了很多编译报错,不过我都一一解决了,改正后的代码我已传到gitee上,文末附有连接,需要的话自取。
我尝试运行了前两章的示例代码,但是运行结果却不全是书中所描述的那样。因此我决定对书中每一章的代码都重新编码,然后将踩过的坑与大家分享共勉。
接下来进入正题,第一章:一个简单的Web容器
实现一个简单的Web容器
第一章的内容很简单,实现一个基于HTTP协议的Web服务器,并且仅支持静态资源的获取即可。
HTTP协议的详细介绍可以看我这篇历史文章 网络协议(七)应用层-HTTP
HTTP在传输层使用的协议为TCP协议,TCP的详细介绍可以看这篇文章 网络协议(六)传输层
本章的代码使用Socket进行编程,对Socket不了的同学可以先看看这篇文章 Java Socket通信编程
我一口气拉了三篇历史文章过来,全是网络协议相关的内容。可见要建造上层建筑,基础知识必不可少啊!
如果你对HTTP协议与Socket编程很熟悉了,那么接下来设计Web服务就简单多了,大致分为这三步
- 服务端接收Socket的InputStream,转化为Request对象
- 根据Socket的OutputStream,构建Response对象
- 处理客户端的请求,由Response将结果推送回客户端
Request类代码如下
主要功能为解析HTTP请求,并得到客户端要请求的资源uri
package ex01.hml;
import java.io.IOException;
import java.io.InputStream;
public class Request {
private String uri;
// 从输入流中解析出 HTTP 协议内容,并得到客户端要请求的资源uri
public void parse(InputStream inputStream) {
try {
byte[] bytes = new byte[1024];
int readLenth = inputStream.read(bytes);
String content = "";
while(readLenth != -1) {
content += new String(bytes);
if(readLenth < 1024) {
break;
}
readLenth = inputStream.read(bytes);
}
System.out.println("request body --->");
System.out.println(content);
//获取请求头中第一个空格和第二个空格之间的内容,例如:请求头 【GET /index.html HTTP/1.1】,uri即为 【/index.html】
setUri(content);
} catch (IOException e) {
e.printStackTrace();
}
}
private void setUri(String content) {
int index1 = content.indexOf(" ");
if(index1 == -1) {
return;
}
int index2 = content.indexOf(" ", index1+1);
String substring = content.substring(index1+1, index2);
this.uri = substring;
}
public String getUri() {
return uri;
}
}
Reponse类的代码如下
主要功能为访问目标静态资源,并拼装HTTP响应,返给客户端
package ex01.hml;
import java.io.*;
public class Response {
private Request request;
private OutputStream outputStream;
// 静态资源的存放路径
public static final String rootDir = System.getProperty("user.dir") + File.separatorChar + "webroot";
public Response(Request request, OutputStream outputStream) {
this.request = request;
this.outputStream = outputStream;
}
/**
* 通过客户端请求的 uri 获取指定静态资源,并拼装成HTTP响应消息,返回给客户端
*/
public void sendStaticResource() {
try {
if (request.getUri().equals("/shutdown")) {
String msg = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 32\r\n" +
"\r\n" +
"<h1>server already shutdown</h1>";
outputStream.write(msg.getBytes());
return;
}
File file = new File(rootDir + request.getUri());
if (file.exists()) {
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[fileInputStream.available()];
fileInputStream.read(bytes);
String successMsg = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: " + bytes.length + "\r\n" +
"\r\n";
outputStream.write(successMsg.getBytes());
outputStream.write(bytes);
fileInputStream.close();
} else {
String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 23\r\n" +
"\r\n" +
"<h1>File Not Found</h1>";
outputStream.write(errorMessage.getBytes());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
HttpServer服务启动类代码如下
package ex01.hml;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class HttpServer {
// 声明一个结束标识,用来判断是否需要终止服务
public static boolean shutDown = false;
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8080,10,InetAddress.getByName("127.0.0.1"));
Socket socket;
while(!shutDown) {
socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
Request request = new Request();
request.parse(inputStream);
OutputStream outputStream = socket.getOutputStream();
Response response = new Response(request,outputStream);
response.sendStaticResource();
inputStream.close();
socket.close();
// 判断是否是结束服务的请求
shutDown = request.getUri().equals("/shutdown");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
静态资源index.html
<html>
<head>
<title>Welcome to BrainySoftware</title>
</head>
<body>
<img src="./images/logo.gif">
<br>
Welcome to BrainySoftware.
</body>
</html>
请求结果
浏览器截图
服务端日志
request body --->
GET /index.html HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: userToken=t6tnfg5d82lshqufl69g4j
request body --->
GET /images/logo.gif HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
sec-ch-ua-platform: "macOS"
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://127.0.0.1:8080/index.html
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: userToken=t6tnfg5d82lshqufl69g4j
request body --->
GET /favicon.ico HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
sec-ch-ua-platform: "macOS"
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://127.0.0.1:8080/index.html
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: userToken=t6tnfg5d82lshqufl69g4j
/favicon.ico 这个请求是浏览器自动请求的,favicon.ico是指显示在浏览器收藏夹、地址栏和标签标题前面的个性化图标。当你使用浏览器浏览不同站点时,浏览器将自动发送请求。 如果你的浏览器收到有效 favicon.ico 文件,将显示此图标。 如果未收到,则不会显示特殊图标,且会报404错误。
由于我的项目里没有这个favicon.ico文件,所以这里后端返回404。
好了,一个简单的静态资源Web服务器就搭好了,是不是很简单?下篇文章将引入servlet来处理动态内容,敬请期待!
源码分享
https://gitee.com/huo-ming-lu/HowTomcatWorks
原书作者的源码在 ex01.pyrmont 包下,我修改后的代码在 ex01.hml 包下,以此类推,后面每一章的代码都会是这个形式。