【 Servlet 运行原理 】

一、Servlet 与tomcat 的关系

Servlet 是 tomcat 提供的一组接口,而servlet接口定义的是一套处理网络请求的规范.所有实现servlet的类,都需要实现它那五个方法,其中最主要的是两个生命周期方法 init()和destroy(),还有一个处理请求的service(),也就是说,所有实现servlet接口的类,或者说,所有想要处理网络请求的类,都需要回答这三个问题:1.你初始化时要做什么 2. 你销毁时要做什么 3. 你接受到请求时要做什么

那么单单实现一个 servlet类后就能处理请求吗? 肯定是不能的,这时就需要 servlet与tomcat配合

前面我们已经了解过 tomcat 是一个 HTTP服务器 用来处理请求,除此之外它还是一个 servlet容器存放着很多Servlet对象.单靠一个 Servlet 是不能处理请求并响应的,必须将Servlet部署到一个容器中,而这个容器就可以是 tomcat,tomcat 才是与客户端直接打交道的家伙,他监听了端口,接收外部请求后并封装成 HttpServletRequest,根据 url 等信息将req发送给指定的Servlet去处理,然后调用那个servlet的service方法进而调用doXX方法处理请求,然后 service 方法返回一个HttpServletResponse对象,tomcat再将respo对象返回给客户端

大家也可以参考大佬的动画演示:

在这里插入图片描述

想要了解servlet 与 tomcat 更详细的关系可以参考这里 !!!


二、Tomcat 的定位

在这里插入图片描述

当浏览器给服务器发送请求的时候, Tomcat 作为 HTTP 服务器, 就可以接收到这个请求.
HTTP 协议作为一个应用层协议, 需要底层协议栈来支持工作. 如下图所示:

在这里插入图片描述

扫描二维码关注公众号,回复: 14792879 查看本文章

如上,用户实现即根据请求计算响应,再通过 Servlet接口 与 tomcat 进行交互,最后 tomcat 再将响应通过底层协议栈返回给浏览器

更详细的交互过程可以参考下图:

在这里插入图片描述

接收请求:

  1. 用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求.
  2. 这个 HTTP 请求会经过网络协议栈逐层进行 封装 成二进制的 bit 流, 最终通过物理层的硬件设备转换成光信号/电信号传输出去.
  3. 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达目标主机(这个过程也需要网络层和数据链路层参与).
  4. 服务器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 请求. 并交给 Tomcat 进程进行处理(根据端口号确定进程)
  5. Tomcat 通过 Socket 读取到这个请求(一个字符串), 并按照 HTTP 请求的格式来解析这个请求, 根据请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的 类. 再根据当前请求的方法 (GET/POST/…), 决定调用这个类的 doGet 或者 doPost 等方法. 此时我们的代码中的doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息.

根据请求计算响应:
6. 在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一些信息, 来给 HttpServletResponse 对象设置一些属性. 例如状态码, header, body 等.

返回响应:
7. 我们的 doGet / doPost 执行完毕后, Tomcat 就会自动把 HttpServletResponse 这个我们刚设置好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去.
8. 此时响应数据在服务器的主机上通过网络协议栈层层 封装, 最终又得到一个二进制的 bit 流, 通过物理层硬件设备转换成光信号/电信号传输出去.
9. 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达浏览器所在的主机(这个过程也需要网络层和数据链路层参与).
10. 浏览器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 响应, 并交给浏览器处理.
11. 浏览器也通过 Socket 读到这个响应(一个字符串), 按照 HTTP 响应的格式来解析这个响应. 并且把body 中的数据按照一定的格式显示在浏览器的界面上.


对 Socket,tomcat,Servlet之间的通信做一个简单说明:

  1. Socket是用于在网络上进行通信的编程接口,Servlet是Java Web应用程序的组件,而Tomcat是一个Servlet容器,用于运行和管理Servlet。

  2. 在Tomcat中,当一个客户端(如浏览器)发送请求到Tomcat时,Tomcat会通过Socket建立与客户端的连接。Tomcat会将该请求交给一个适当的Servlet进行处理。Servlet会处理请求并生成响应,然后将响应发送回Tomcat。Tomcat再通过Socket将响应发送回客户端。

简单来说,Tomcat作为一个Web服务器,通过Socket接收和处理客户端的请求,并将请求分配给适当的Servlet进行处理,最终将响应发送回客户端。这些组件的通信是通过网络套接字(Socket)实现的。

三、Tomcat 伪代码

下面通过 “伪代码” 的形式描述了 Tomcat 初始化/处理请求 两部分核心逻辑

Tomcat 初始化流程

class Tomcat{
    
    
    // 用来存储所有的 Servlet 对象
    private List<Servlet> instanceList = new ArrayList<>();
    public void start() {
    
    
    // 根据约定,读取 WEB-INF/web.xml 配置文件;
    // 并解析被 @WebServlet 注解修饰的类
    // 假定这个数组里就包含了我们解析到的所有被 @WebServlet 注解修饰的类.
        Class<Servlet>[] allServletClasses = ...;
    // 这里要做的的是实例化出所有的 Servlet 对象出来;
        for (Class<Servlet> cls : allServletClasses) {
    
    
    // 这里是利用 java 中的反射特性做的
    // 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的
    // 方式(全部在 WEB-INF/classes 文件夹下)存放的,所以 tomcat 内部是
    // 实现了一个自定义的类加载器(ClassLoader)用来负责这部分工作。
            Servlet ins = cls.newInstance();
            instanceList.add(ins);
        }
    // 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次;
        for (Servlet ins : instanceList) {
    
    
            ins.init();
        }
// 利用我们之前学过的知识,启动一个 HTTP 服务器
// 并用线程池的方式分别处理每一个 Request
        ServerSocket serverSocket = new ServerSocket(8080);
// 实际上 tomcat 不是用的固定线程池,这里只是为了说明情况
        ExecuteService pool = Executors.newFixedThreadPool(100);
        while (true) {
    
    
            Socket socket = ServerSocket.accept();
// 每个请求都是用一个线程独立支持,这里体现了我们 Servlet 是运行在多线程环境下的
            pool.execute(new Runnable() {
    
    
                doHttpRequest(socket);
            });
        }
// 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次;
        for (Servlet ins : instanceList) {
    
    
            ins.destroy();
        }
    }
    public static void main(String[] args) {
    
    
        new Tomcat().start();
    }
}

注意:

  1. Tomcat 的代码中内置了 main 方法. 当我们启动 Tomcat 的时候, 就是从 Tomcat 的 main 方法开始执行的.
  2. Tomcat 会在webapps中找到.class对应的servlet类并且需要加载。被 @WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取到, 并集中管理.
  3. Tomcat 通过 反射 这样的语法机制来创建被 @WebServlet 注解修饰的类的实例
  4. 这些实例被创建完了之后, 会 调用其中的 init 方法进行初始化. (这个方法是 HttpServlet 自带的,我们自己写的类可以重写 init)
  5. 这些实例被销毁之前, 会调用其中的 destory 方法进行收尾工作. (这个方法是 HttpServlet 自带的,我们自己写的类可以重写 destory)

如果tomcat按照正常流程退出,主动结束循环,才会调用这里的destory。如果是非正常退出就来不及调用,比如直接结束进程(大部分情况)

  1. Tomcat 内部也是通过 Socket API 进行网络通信.
  2. Tomcat 为了能同时相应多个 HTTP 请求, 采取了多线程的方式实现. 因此 Servlet 是运行在 多线程环境 下的.

Tomcat 处理请求流程

class Tomcat {
    
    
    void doHttpRequest(Socket socket) {
    
    
// 参照我们之前学习的 HTTP 服务器类似的原理,进行 HTTP 协议的请求解析,和响应构建
        HttpServletRequest req = HttpServletRequest.parse(socket);
        HttpServletRequest resp = HttpServletRequest.build(socket);
// 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态
        内容
// 直接使用我们学习过的 IO 进行内容输出
        if (file.exists()) {
    
    
// 返回静态内容
            return;
        }
// 走到这里的逻辑都是动态内容了
// 根据我们在配置中说的,按照 URL -> servlet-name -> Servlet 对象的链条
// 最终找到要处理本次请求的 Servlet 对象
        Servlet ins = findInstance(req.getURL());
// 调用 Servlet 对象的 service 方法
// 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
        try {
    
    
            ins.service(req, resp);
        } catch (Exception e) {
    
    
// 返回 500 页面,表示服务器内部错误
        }
    }
}

注意:

  1. Tomcat 从 Socket 中读到的 HTTP 请求是一个字符串, 然后会按照 HTTP 协议的格式解析成一个HttpServletRequest 对象.
  2. Tomcat 会根据 URL 中的 path 判定这个请求是请求一个静态资源还是动态资源. 如果是静态资源,直接找到对应的文件把文件的内容通过 Socket 返回. 如果是动态资源, 才会执行到 Servlet 的相关逻辑.
  3. Tomcat 会根据 URL 中的 Context Path(确定一个webapp) 和 Servlet Path(确定一个Servlet类) 确定要调用哪个 Servlet 实例的 service方法.
  4. 通过 service 方法, 就会进一步调用到我们之前写的 doGet 或者 doPost

Servlet 的 service 方法的实现

class Servlet {
    
    
    public void service(HttpServletRequest req, HttpServletResponse resp) {
    
    
        String method = req.getMethod();
        if (method.equals("GET")) {
    
    
            doGet(req, resp);
        } else if (method.equals("POST")) {
    
    
            doPost(req, resp);
        } else if (method.equals("PUT")) {
    
    
            doPut(req, resp);
        } else if (method.equals("DELETE")) {
    
    
            doDelete(req, resp);
        }
......
    }
}

注意:

  1. Servlet 的 service 方法内部会根据当前请求的方法, 决定调用其中的某个 doXXX 方法.
  2. 在调用 doXXX 方法的时候, 就会触发 多态 机制, 从而执行到我们自己写的子类中的 doXXX 方法

理解此处的多态:

我们前面自己写的 HelloServlet 类, 继承自 HttpServlet 类. 而 HttpServlet 又继承自 Servlet. 相当
于 HelloServlet 就是 Servlet 的子类.接下来, 在 Tomcat 启动阶段, Tomcat 已经根据注解的描述, 创建了 HelloServlet 的实例, 然后把这个实例放到了 Servlet 数组中.后面我们根据请求的 URL 从数组中获取到了该 HelloServlet 实例, 但是我们是通过 Servlet ins这样的父类引用来获取HelloServlet 实例的.最后, 我们通过 ins.doGet() 这样的代码调用 doGet 的时候, 正是 “父类引用指向子类对象”, 此时就会触发多态机制, 从而调用到我们之前在 HelloServlet 中所实现的 doGet 方法

等价代码:
Servlet ins = new HelloServlet();
ins.doGet(req, resp);


四、Servlet 生命周期

分析Servlet源码

在这里插入图片描述

其中,init( ),service( ),destroy( )是Servlet生命周期的方法。代表了Servlet从“出生”到“工作”再到“死亡 ”的过程。Servlet容器(例如TomCat)会根据下面的规则来调用这三个方法:

  1. init( ),当Servlet第一次被请求时,Servlet容器就会开始调用这个方法来初始化一个Servlet对象出来,但是这个方法在后续请求中不会在被Servlet容器调用,就像人只能“出生”一次一样。我们可以利用init( )方法来执行相应的初始化工作。调用这个方法时,Servlet容器会传入一个ServletConfig对象进来从而对Servlet对象进行初始化。

  2. service( )方法,每当请求Servlet时,Servlet容器就会调用这个方法。就像人一样,需要不停的接受老板的指令并且“工作”。第一次请求时,Servlet容器会先调用init( )方法初始化一个Servlet对象出来,然后会调用它的service( )方法进行工作,但在后续的请求中,Servlet容器只会调用service方法了。

  3. destory,当要销毁Servlet时,Servlet容器就会调用这个方法,就如人一样,到时期了就得死亡。在卸载应用程序或者关闭Servlet容器时,就会发生这种情况,一般在这个方法中会写一些清除代码。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_64317046/article/details/129837725