Tomcat 核心源码解析 及 仿写

实际上 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核心源码仿写

效果展示

运行tomcatDemo主方法,控制台会输出

这里启动阶段完成,控制台输出了 url 路径对应的 Servlet 类对象

并输出了本机的IP地址 169.254.214.160

然后浏览器进行访问

得到的结果

控制台输出接受到的Http请求报文

以及响应的Http报文

以及响应的Http报文

** 到此一个Tomcat核心功能仿写完成,你也去试试吧 ^-^**

end

评论

新增邮件回复功能,回复将会通过邮件形式提醒,请填写有效的邮件!