最通俗易懂的 - Tomcat 核心源码仿写

本文介绍了Tomcat的核心功能和工作原理,包括与浏览器建立连接、管理Servlet生命周期。文章详细讲解了Tomcat的三个阶段:初始化、运行时和销毁,并提供了从扫描Servlet目录到处理HTTP请求的代码示例。通过升级到多线程和Maven项目,仿写更加接近真实Tomcat的工作流程。
摘要由CSDN通过智能技术生成

-- 更新信息 -- 

2023/8/17 更新 - 在第一版代码基础上迎来全新升级

包括将单线程改为多线程,并将项目升级为Maven项目,第二版代码的介绍博客地址:最通俗易懂的 - Tomcat 核心源码仿写 第二版icon-default.png?t=N6B9http://t.csdn.cn/vOlDQ

-- 源码地址 --

朱元杰的开源仓库 -- Tomcat核心源码仿写https://gitee.com/Speed_Demon/Zhuyuanjie-OpenSource

-- 正文内容 --

实际上 Tomcat 的原理更多的是关于 计算机网络 的知识

下文仅展示 Tomcat 的 核心功能 而不是全部功能

首先回顾Tomcat的核心作用

         1、与浏览器建立连接,接收Http报文,解析获取请求和参数,然后根据请求作响应,最后将结果封装成Http报文响应给浏览器

         2、管理Servlet应用的生命周期

        要完成 第一点功能,需要明白 sockethttp 报文

        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核心功能仿写完成,你也去试试吧 ^-^

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值