Mini Spring-Boot(二)Servlet 容器

续言

上篇文章中我们把框架的功能和结构都预定好了,从本篇开始逐步添加功能点,使它迅速丰满起来。

实现过程呢,就顺从敏捷开发思想的引导,首先使框架能 Run 起来,再通过快速迭代、小步快跑地优化。框架是没法独立运行起来的,还需要搭配上工程实现业务逻辑才行。所以,我们向项目内添加一个同级模块使用 winter 框架,这也算是 TDD 测试驱动开发的另一种表现形式了。

当然了,开始阶段只有一个入口类,项目也只依赖框架,一个普通 Main 入口类即可。

项目地址: winter-zhenbianshu-Github

转载随意,文章会持续修订,请注明来源地址: https://zhenbianshu.github.io

Servlet 和容器

Server

我们知道,一个 HTTP 请求被发送到服务器时,都是二进制的字节流,是操作系统的 TCP/IP 栈把这些字节流解析成带有 IP 和端口的 TCP 请求,然后将请求分配给对应的进程来处理。能够受理外部请求的进程我们暂时把它称作 socket 服务进程,每个服务进程都监听着一个服务器端口,操作系统就是根据这个端口来对应每个外部请求和服务进程的。

由上,由于服务器会监听系统的端口,受理操作系统 CGI 请求的应该是服务器,而职责单一的服务器是不应该跟业务耦合的,具体的业务逻辑还是应该由服务器交给我们的 Java 应用程序来处理。

规范

那么从操作系统接收到的 CGI 请求应该怎么给 Java 程序,Java 程序又应该怎么响应回来呢?如果每种服务器或每种 Java 程序都自己定义自己的结构,那服务器与 Java 程序的适配将成为一个大问题。这时候就需要一种规范。

于是 Java 就制定了一种标准,在 Java 语言内实现为接口,叫 Servlet ,它的包全路径为 javax.servlet.Servlet

它有五个预定义的方法:

public interface Servlet {
    // servlet 初始化方法
    public void init(ServletConfig config) throws ServletException;

    // 获取 servlet 配置
    public ServletConfig getServletConfig();

    // servlet 响应请求方法
    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;

    // 获取 servlet 信息(版本、版权等)方法
    public String getServletInfo();

    // servlet 销毁方法
    public void destroy();
}

如果一个 Java 程序都实现这个接口,那么这个 Java 程序我们称为一个 servlet 。而服务器保存着多个 servlet 的实例,所以 Java 服务器也被称为 servlet 容器

流程

每个服务器会保留接口保存 servlet 和它们要处理业务逻辑(通常是 host + uri )的映射,将 uri 匹配的请求分配给 servlet,调用 servlet 的 service(ServletRequest req, ServletResponse res) 方法执行后将结果返回,这样就解决了服务器与 Java 程序的适配问题。

那么一个 HTTP 请求从发到操作系统到被 Java 程序处理后再响应的整个流程如图(实线是请求,虚线是响应):

收到操作系统的 CGI 请求后,由服务器将这些 CGI 请求包装成 ServletRequest 后分派给 Java 程序处理后,Java 程序将响应结果写入 ServletRequest 传回给服务器, 服务器再将响应解析为 CGI 响应给操作系统,再由操作系统将结果通过网络连接响应给客户端。

Tomcat

服务进程的编写就要涉及到 Java 的 sockets 编程了,对 HTTP 请求的处理和封装非常繁杂,涉及到的 CGI 编程也不应该是我们要涉及的范围,引用现成的服务器组件就是我们最好的选择了。

Tomcat 应该是 Java 开发工程师接触最多的服务器了。它是由 Java 语言编写,运行在 JVM 上的,对 Java 开发者来说很亲切。背靠 Java 和 Apache 的大腿,又有 Spring 的默认支持,市场占有率一直居高不下,当然了,高性能和对各种 I/O 模型的支持才是它成功的关键。

我们并不需要使用它的高级功能,多路复用的 I/O 模型和它默认的单实例多线程线程模型就够了。

Tomcat 从 7 开始支持嵌入式功能,我们在框架内直接实例化 org.apache.catalina.startup.Tomcat 类,再调用其 start() 方法即可启动一个 Tomcat 线程,默认监听 8080 端口,开始接受操作系统分配的 CGI 请求。

请求分发

DispatcherServlet

在 servlet 最黑暗的年代,我们需要使用 servlet 就要先定义一个类实现 Servlet 接口,这个类的 service() 方法负责处理一个或一组 uri,然后将这个 servlet 实例配置在 web.xml 中,建立起 uri 和 servlet 的映射关系,请求到达服务器时由服务器来决定调用哪个 servlet。

而在大型项目中,往往会有多组多个 uri,这也就需要我们实例化 N 个 servlet 实例,再在 web.xml 中配置多个 servlet 映射,servlet 实例和 web.xml 的管理就是个大问题,一个大而杂乱的 web.xml 配置文件让每个人看了都头大。

Spring 出现后,便用强权手段结束了服务器对 web.xml 的统治,Spring 定义了一个 DispatcherServlet ,在服务器注册 servlet 时,声明它能处理 uri 匹配 "/" 的请求,也就是说所有的 uri 都由 DispatcherServlet 来处理,这样分发请求的任务全被 Spring 承包了。

至于所有请求到达 DispatcherServlet 的 service 方法后,又该交给哪个类哪个方法执行,就是 Spring 的事了,也就进入了 Spring 容器时代。

实现

Tomcat 的初始化很简单了,我们还要实现 Servlet 来受理请求,既然是模拟 Spring,我们也需要一个类似 DispatcherServlet 一样的”大总管”,我们暂时起名为 WinterServlet ,方法里我们先留一些占位符,请求的分发我们在实现请求处理器后再进行分发。

public class WinterServlet implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("server starting...");
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        // todo dispatch requests
        System.out.println("Hello World!");
    }

    @Override
    public String getServletInfo() {
        return "Winter Framework";
    }

    @Override
    public void destroy() {
        System.out.println("server stopped.");
    }
}

连接服务器

Tomcat 容器

在 Tomcat 容器中,servlet 并不是直接依附 Tomcat 而生的,Tomcat 中将容器分为四个级别,将容器的职责进行了解耦。

下图展示了 Tomcat 中各级别容器的关系:

  • Engine 容器是最顶级的容器,可以理解为总控中心,在 Nginx 中就相当于 nginx.conf 文件中的配置。
  • Host 容器对应一个虚拟主机,管理一个主机的信息和其子容器,相当于 Nginx 中的一个 vhosts 配置。
  • Context 容器是最接近 servlet 的容器了,我们通过 context 可以设置一些资源属性和管理组件,Nginx 中好像并不到合适的对应,牵强一点的话就是一些 日志和静态文件配置吧。
  • Wrapper 容器是对一个 servlet 的封装,负责 servlet 的加载、初始化、执行和销毁。

实现

我们在之前声明的 WinterServlet 和 Tomcat 之间建立联系。

public void startTomcat() {
        // 简单地初始化一个 Tomcat 服务器
        tomcat = new Tomcat();
        tomcat.setPort(6699);
        tomcat.start();

        // 实例化一个 Context 容器的默认实现
        Context context = new StandardContext();
        context.setPath("");
        context.addLifecycleListener(new Tomcat.FixContextListener());

        // 实例化我们创建的 WinterServlet 并将它添加到 Context 容器中
        Servlet servlet = new WinterServlet();
        Tomcat.addServlet(context, "winterServlet", servlet).setAsyncSupported(true);
        context.addServletMappingDecoded("/*", "winterServlet"); // 注意其匹配的 URI 为所有

        tomcat.getHost().addChild(context);

        // 将 Tomcat 的运行包装成独立线程
        Thread awaitThread = new Thread("container-tomcat") {
            @Override
            public void run() {
                TomcatServer.this.tomcat.getServer().await();
            }
        };
        awaitThread.setContextClassLoader(getClass().getClassLoader());
        awaitThread.setDaemon(false);
        awaitThread.start();

小结

这样,一个最基本的 WEB 框架就 OK 了,虽然启动后所有的请求都只会响应 “Hello World!”。

Tomcat 容器的相关知识可以不必去纠结,毕竟太过于专有,但像 Servlet 和 Spring DispatcherServlet 这样的设计还是非常值得我们去研究和参考的。

关于本文有什么疑问可以在下面留言交流,如果您觉得本文对您有帮助,欢迎关注我的微博 或 GitHub 。您也可以在我的 博客REPO 右上角点击 Watch 并选择 Releases only 项来 订阅 我的博客,有新文章发布会第一时间通知您。