45

Mini Spring-Boot(二)Servlet 容器

 5 years ago
source link: https://zhenbianshu.github.io/2019/03/mini_spring_framework_2_servlet.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

续言

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

实现过程呢,就顺从敏捷开发思想的引导,首先使框架能 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 程序处理后再响应的整个流程如图(实线是请求,虚线是响应):

YbANjyY.png!web

收到操作系统的 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 中各级别容器的关系:

eU7Brii.jpg!web

  • 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 项来 订阅 我的博客,有新文章发布会第一时间通知您。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK