-- 更新信息 --
2023/8/17 更新 - 在第一版代码基础上迎来全新升级
包括将单线程改为多线程,并将项目升级为Maven项目,第二版代码的介绍博客地址:最通俗易懂的 - Tomcat 核心源码仿写 第二版http://t.csdn.cn/vOlDQ
-- 源码地址 --
朱元杰的开源仓库 -- Tomcat核心源码仿写https://gitee.com/Speed_Demon/Zhuyuanjie-OpenSource
-- 正文内容 --
实际上 Tomcat 的原理更多的是关于 计算机网络 的知识
下文仅展示 Tomcat 的 核心功能 而不是全部功能
首先回顾Tomcat的核心作用
1、与浏览器建立连接,接收Http报文,解析获取请求和参数,然后根据请求作响应,最后将结果封装成Http报文响应给浏览器
2、管理Servlet应用的生命周期
要完成 第一点功能,需要明白 socket 和 http 报文
socket是网络通信必需的技术,主要作用就是让客户端和服务端建立连接,客户端和服务端都能从该连接中获取输入和输出流,从而进行数据交流
而http报文就是规定的一种数据格式,规定第几个字节是什么内容,这样双方才能知道对方说的是什么
要完成 第二点功能,需要明白 JAVA反射,反射就不过多介绍
Tomcat为了完成上述功能,需要经历三个阶段
1、初始化阶段:Web容器加载Servlet类并创建Servlet对象,Servlet容器会运行该对象的init()方法对该对象进行初始化。
2、运行时阶段:请求到达时,Servlet容器会针对该请求创建ServletRequest对象和ServletResponse对象,然后调用相关Servlet对象的service()方法,service()方法会根据请求调用对应的doGet或doPost等方法。
3、销毁阶段:Java web应用被终止时,Servlet容器会调用所有Servlet对象的destroy()方法(释放Servlet对象所占用的资源)。
现在我们就依次实现这三个周期(结尾有完整代码)
初始阶段
在初始阶段,Tomcat需要扫描 Sevlet 所在的目录,获取该目录下的所有 .java 后缀文件,获取这些Java类的全类名,然后通过反射,获取类上的注解,挑选出包含 @Servlet 注解的类,极为Servlet,实际上 @Servlet 可以自己定义,该注解应该至少包含一个属性用于标注Servlet的路径,如下
//该注解可以应用于类、接口(包括注解类型)、枚举
@Target(ElementType.TYPE)
//该注解标记的元素可以被Javadoc 或类似的工具文档化
@Documented
//该注解的生命周期,由JVM 加载,包含在类文件中,在运行时可以被获取到
@Retention(RetentionPolicy.RUNTIME)
public @interface ServletDemo {
String path() default "";
}
初级阶段代码
全局静态变量
//用于存放全类名
private static ArrayList<String> arr=new ArrayList<>();
//用于存放Servlet的类对象
private static HashMap<String,Class> servletMap=new HashMap<>();
main方法内容 ,其中的Servlet所在目录要根据自己的实际情况进行修改
//启动阶段
String inputPath = "D:\\javaDemo\\src"; //Servlet所在目录
File file = new File(inputPath); //获取其file对象
func(file); //调用该方法用于获取 .java 后缀文件,并获取全类名
System.out.println(arr);
choseServlet(); //调用该方法获取 Servlet 类对象
System.out.println(servletMap);
main中调用到的全局静态方法
private static void func(File file) {
File[] fs = file.listFiles();
String s;
for (File f : fs) {
if (f.isDirectory()) //若是目录,则递归打印该目录下的文件
func(f);
if (f.isFile()){
s=f.toString().split("src")[1];
s=s.substring(1);
if(s.length()>=5 && s.substring(s.length()-5).equals(".java")){
s=s.replace('\\','.');
s=s.substring(0,s.length()-5);
arr.add(s);
}
}
}
}
private static void choseServlet() throws ClassNotFoundException {
for(int i=0;i< arr.size();i++){
String path=arr.get(i);
Class<?> cl=Class.forName(path);
if(cl.isAnnotationPresent(ServletDemo.class)){
servletMap.put(cl.getAnnotation(ServletDemo.class).path(),cl);
}
}
}
运行阶段
运行阶段通过socket与浏览器建立连接,然后处理浏览器的请求,并做出响应,为了简单起见,在这我使用了单线程来完成,但真正的Tomcat是多线程的
下面代码完成的内容是注册端口,建立连接,并接收来之浏览器的Http报文
//注册端口
InetAddress localHost = InetAddress.getLocalHost();
System.out.println("localhost:" + localHost);
ServerSocket serverSocket = new ServerSocket(8080,10, localHost);
//单线程下
System.out.println("等待建立连接");
Socket server = serverSocket.accept();
System.out.println("连接已建立");
//定义线程去接收 Http 报文
HttpAcceptThread httpAcceptThread=new HttpAcceptThread(server);
Thread accept = new Thread(httpAcceptThread);
accept.start();
accept.join();
//处理请求
requestHttp(server,httpAcceptThread.strings.get(0));
class HttpAcceptThread implements Runnable{
private Socket socket;
ArrayList<String> strings=new ArrayList<>();
public HttpAcceptThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
System.out.println("开始接收Http");
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String s;
while ((s = reader.readLine()).length()!=0){
//每次循环接收一行的Http数据
try {
strings.add(s);
System.out.println(s);
}catch (Exception e){
System.out.println("接收Http进程结束");
break;
}
}
System.out.println("接收Http进程结束");
} catch (IOException e) {
e.printStackTrace();
}
}
}
最后的重点就是对请求作处理了,在这一步中主要是解析Http报文,获取请求方式和请求内容,通过反射调用请求的方法,并且返回结果给浏览器,实际上响应Http报文的封装以及回传应该在Tomcat中完成,这里简单起见,直接在Servlet中完成
private static void requestHttp(Socket socket,String http) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException {
//获取请求方式
String requestStyle=http.split(" ")[0];
if(requestStyle.equals("GET")){
String httpPathAndParameter=http.split(" ")[1];
String httpPath;
//创建HttpRequest对象
HttpRequestDemo httpRequestDemo=new HttpRequestDemo();
if(httpPathAndParameter.indexOf("?")!=-1){
httpPath=httpPathAndParameter.substring(1);
httpPath=httpPath.split("\\?")[0];
System.out.println(httpPath);
String parameterString=httpPathAndParameter.split("\\?")[1];
String[] parameters=parameterString.split("&");
for (int i=0;i<parameters.length;i++){
httpRequestDemo.map.put(parameters[i].split("=")[0],parameters[i].split("=")[1]);
}
}else{
httpPath=httpPathAndParameter.substring(1);
System.out.println(httpPath);
}
//创建HttpResponse对象
OutputStream outputStream=socket.getOutputStream();
HttpResponseDemo httpResponseDemo=new HttpResponseDemo(outputStream);
//反射调用doGet
Class<?> servletClass=servletMap.get(httpPath);
Method method=servletClass.getMethod("doGet",HttpRequestDemo.class,HttpResponseDemo.class);
method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo);
}else{
String httpPath=http.split(" ")[1];
httpPath=httpPath.substring(1);
System.out.println(httpPath);
HttpRequestDemo httpRequestDemo=new HttpRequestDemo();
OutputStream outputStream=socket.getOutputStream();
HttpResponseDemo httpResponseDemo=new HttpResponseDemo(outputStream);
Class<?> servletClass=servletMap.get(httpPath);
Method method=servletClass.getMethod("doPost",HttpRequestDemo.class,HttpResponseDemo.class);
method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo);
}
}
其中一个Servlet的源码如下
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
@ServletDemo(path = "address1")
public class Servlet1 {
public void doGet(HttpRequestDemo request,HttpResponseDemo response) throws IOException {
System.out.println("address1 GET响应:");
System.out.println("a="+request.getParameter("a"));
System.out.println("\n响应的http如下:");
String resp= HttpResponseDemo.responsebody+"<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"utf-8\" />\n" +
"</head>\n" +
"<body>\n" +
" \n" +
" <form name=\"my_form\" method=\"POST\">\n" +
" <input type=\"button\" value=\"按下\" onclick=\"alert('你按下了按钮')\">\n" +
" </form>\n" +
" \n" +
"</body>\n" +
"</html>";
System.out.println(resp);
response.outputStream.write(resp.getBytes());
response.outputStream.flush();
response.outputStream.close();
}
public void doPost(HttpRequestDemo request,HttpResponseDemo response) throws IOException {
System.out.println("\n响应的http如下:");
String resp= HttpResponseDemo.responsebody+
"{\"sorry\":\"we only respond to method GET now\"},\r\n"+
"";
System.out.println(resp);
response.outputStream.write(resp.getBytes());
response.outputStream.flush();
response.outputStream.close();
}
}
HttpRequestDemo 如下
import java.util.HashMap;
public class HttpRequestDemo {
public HashMap<String, String> map = new HashMap<>();
public String getParameter(String key) {
return map.get(key);
}
}
HttpResponseDemo 如下
import java.io.OutputStream;
public class HttpResponseDemo {
public OutputStream outputStream;
public static final String responsebody="HTTP/1.1 200+\r\n"+"Content-Type:text/html+\r\n"
+"\r\n";
public HttpResponseDemo(OutputStream outputStream){
this.outputStream=outputStream;
}
}
我将代码开源到了 gitee 中,需要的通过以下获取
朱元杰的开源仓库 -- Tomcat核心源码仿写https://gitee.com/Speed_Demon/Zhuyuanjie-OpenSource
效果展示
运行tomcatDemo主方法,控制台会输出
这里启动阶段完成,控制台输出了 url 路径对应的 Servlet 类对象
并输出了本机的IP地址 169.254.214.160
然后浏览器进行访问
得到的结果
控制台输出接受到的Http请求报文
以及响应的Http报文
并且中间也获取到了 url 中传入的参数
到此一个Tomcat核心功能仿写完成,你也去试试吧 ^-^