最近想学一下 tomcat 到底是怎么运行的,于是手写一个简易的 tomcat 服务器,加深一下印象。
涉及到的知识:
- 计算机网络
- tcp/ip 协议 , 网络编程 ,socket
- 浏览器请求,响应报文规范
- java 反射,注解
实现的功能:
- 可打包成独立应用
- 能实现一个简单的接口
- 能重定向
其实一个人是没那么多时间搞完tomcat那么多事情的,只能做些简单的实现,还是有很多漏洞bug的,所以仅供参考学习,切勿拿来使用。
Tomcat 运行大致流程:
- Socket连接
- 读取请求网络中的字节流
- 根据相应的协议(Http/AJP)解析字节流,生成统一的Tomcat Request对象
- 将Tomcat Reques传给容器
- 容器返回Tomcat Response对象
- 将Tomcat Response对象转换为字节流
- 将字节流返回给客户端
目录结构
entity
: java实体类,一些pojo,和常量类handler
: 实现步骤3
,封装request
和response
request
:request
对象,以及拓展类response
:response
对象, 拓展类,和一些常量类server
: 主要的代码RequestRunnable.java
: 负责处理浏览器的请求,然后通过请求路径,分发给指定的servlet
处理,实现步骤2,3,4
Server.java
: 读取配置,开启 socket ,接收浏览器请求,并开启线程交给RequestRunnable.java
, 实现步骤1,2
ThreadPool.java
: 线程池工厂
service
: 业务层 , 通过 RequestRunnable 分发,交给 指定 web 应用处理。utils
: 工具包ServletClassLoader.java
: 加载指定配置路径下的 class 文件,并通过反射加载servlet
实现类到 classloder 中, 最后交给RequestRunnable
处理。
成果预览
先看一下成果:
startup.bat
chcp 65001
java -jar tomcat.jar webAppsPath="D:/桌面/tomcat/webapps/net" port=9999
pause
我创建了一个新的 java 项目,并实现了一个 Servlet 。
MyServlet.java
package tomcat;
import cn.enncy.tomcat.request.HttpRequest;
import cn.enncy.tomcat.response.HttpResponse;
import cn.enncy.tomcat.service.HttpServlet;
import cn.enncy.tomcat.service.WebService;
import java.io.IOException;
@WebService(path = "/hello/tomcat")
public class MyServlet extends HttpServlet {
@Override
public void doGet(HttpRequest req, HttpResponse res) {
try {
res.write("hello tomcat");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(HttpRequest httpRequest, HttpResponse httpResponse) {
doGet(httpRequest,httpResponse);
}
}
最后运行 startup.bat
源码
1. 加载 Servlet
首先最重要的就是,当你打包成一个 jar 包之后,怎么样加载 指定文件下的项目,以及 class 文件。这里我们用到了 URLClassLoader
,通过反射加载
package cn.enncy.tomcat.utils;
import cn.enncy.tomcat.service.WebService;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
public class ServletClassLoader {
public static Class[] loadClasses(String rootClassPath) throws Exception {
Set<Class<?>> classSet = new HashSet<>();
// 设置class文件所在根路径
File clazzPath = new File(rootClassPath);
// 记录加载.class文件的数量
int clazzCount = 0;
if (clazzPath.exists() && clazzPath.isDirectory()) {
// 获取路径长度
int clazzPathLen = clazzPath.getAbsolutePath().length() + 1;
Stack<File> stack = new Stack<>();
stack.push(clazzPath);
// 遍历类路径
while (!stack.isEmpty()) {
File path = stack.pop();
File[] classFiles = path.listFiles(pathname -> {
//只加载class文件
return pathname.isDirectory() || pathname.getName().endsWith(".class");
});
if (classFiles == null) {
break;
}
for (File subFile : classFiles) {
if (subFile.isDirectory()) {
stack.push(subFile);
} else {
if (clazzCount++ == 0) {
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
if (! method.isAccessible()) {
method.setAccessible(true);
}
// 设置类加载器
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
// 将当前类路径加入到类加载器中
method.invoke(classLoader, clazzPath.toURI().toURL());
}
// 文件名称
String className = subFile.getAbsolutePath();
//去掉后缀名
className = className.substring(clazzPathLen, className.length() - 6);
//将/替换成. 得到全路径类名
className = className.replace(File.separatorChar, '.');
// 加载 Servlet 类
Class<?> clazz = Class.forName(className);
if(clazz.isAnnotationPresent(WebService.class)){
classSet.add(clazz);
}
}
}
}
}
return classSet.toArray(new Class[0]);
}
}
2. 创建 socket 链接
package cn.enncy.tomcat.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void startup(int port,Class[] classes,Runnable callback) throws IOException {
//创建线程池 最大线程1000个
ThreadPool threadPool = new ThreadPool(1000,1000,1000);
//开启服务器
ServerSocket serverSocket = new ServerSocket(port);
//回调
callback.run();
while(true){
//获取请求
Socket accept = serverSocket.accept();
//创建新的任务 Runnable
RequestRunnable requestTaskRunnable = new RequestRunnable(accept,classes);
//开启新的线程去执行
threadPool.execute(requestTaskRunnable);
}
}
}
创建线程池
使用线程池建立线程,去处理请求,每一个请求对应一个线程。切勿手动创建线程,防止内存溢出。
package cn.enncy.tomcat.server;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPool extends ThreadPoolExecutor{
public ThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime) {
super( corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024),
new WorkThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
class WorkThreadFactory implements ThreadFactory {
private final AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
int c = atomicInteger.incrementAndGet();
//通过计数器,可以更好的管理线程
return new Thread(r,"pool-1-thread-"+c);
}
}
3. 处理请求
最核心的地方也就是在这里了,
- 根据相应的协议解析字节流,生成统一的Tomcat Request对象
- 将Tomcat Reques 传给 web应用
package cn.enncy.tomcat.server;
import cn.enncy.tomcat.handler.RequestHandler;
import cn.enncy.tomcat.handler.ResponseHandler;
import cn.enncy.tomcat.request.HttpRequest;
import cn.enncy.tomcat.response.HttpResponse;
import cn.enncy.tomcat.service.HttpServlet;
import cn.enncy.tomcat.service.WebService;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RequestRunnable implements Runnable{
private Socket socket;
private final Class[] classes;
public RequestRunnable(Socket socket,Class[] classes) {
this.socket = socket;
this.classes = classes;
}
@Override
public void run() {
try{
//封装请求
HttpRequest request = RequestHandler.createRequest (socket);
assert request != null;
System.out.println("[Tomcat-"+Thread.currentThread().getName()+"] "+request.getMethod()+" "+request.getPath()+" "+request.getProtocol());
//封装响应
HttpResponse response = ResponseHandler.createResponse(socket);
//遍历资源类,反射获取 servlet
for (Class clazz : classes) {
WebService webService = (WebService) clazz.getAnnotation(WebService.class);
String path = webService.path();
//如果当前请求的文件路径和 servlet 的path路径匹配,则吧请求交给这个类处理
Matcher matcher = Pattern.compile(path).matcher(request.getFilePath());
if(matcher.find()){
//实例类,然后执行方法
Object object = clazz.getConstructor().newInstance();
HttpServlet httpServlet = (HttpServlet) object;
httpServlet.doGet(request, response);
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
剩下的各种 request
和 response
的封装其实也就是跟 字符串
和浏览器报文规范
,打交道,这里就不细说了。大概封装流程如下:
- 通过
socket
获取InputStream
,你会读取到类似以下的大致内容
Host: localhost:9999
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36
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://localhost:9999/hello/tomcat
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: Webstorm-59be97a3=8d34a989-fd21-411a-9c0f-29b1cefcc5b7; _ga=GA1.1.1609156561.1611760673; AMCV_71FF20B3534568190A490D45%40AdobeOrg=-1124106680%7CMCIDTS%7C18685%7CMCMID%7C23713747909870943654360722308276803315%7CMCAID%7CNONE%7CMCOPTOUT-1614324543s%7CNONE%7CvVersion%7C5.2.0
格式如下
2. 然后你自己再封装一个 response
,通过 socket
的 OutputStream
- write 写出去给浏览器,类似如下内容
HTTP/1.1 200 OK
Server: Tomcat
Date: Tue, 4 May 2021 11:08:13 GMT
Content-Type: text/html; charset=utf-8
这里是你要传给浏览器的内容
格式如下
代码以及实例,我已经上传到 github ,源码大家自行查阅
https://github.com/enncy/tomcat