likes
comments
collection
share

Tomcat——总体架构解析

作者站长头像
站长
· 阅读数 18

  Tomcat是基于Java语言的轻量级应用服务器,是一款完全开源Servlet容器实现。同时,它支持HTML、JS等静态资源的处理,因此又可以作为轻量级Web服务器使用。

  因此 Tomcat 就是一个HTTP 服务器 + Servlet 容器,也叫Web容器。

手写一个Web容器

HTTP 服务器的实现

  我们先用ServerSocket来实现一个很简单的HTTP服务器,就是接收浏览器请求,根据不同的请求路径返回对应的信息。

public static void main(String[] args) {
    try {
        //监听端口号8080
        ServerSocket ss = new ServerSocket(8080);
        while (true) {
            Socket socket = ss.accept();
            //获取客户端请求信息
            BufferedReader bd = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String requestHeader;
            while ((requestHeader = bd.readLine()) != null && !requestHeader.isEmpty()) {
                if (requestHeader.startsWith("GET")) {
                   //获取请求路径
                    String url = requestHeader.split(" ")[1];
                    if (url.contains("/test")){
                        responseHeader (socket,"success");
                    }else  if (url.contains("/demo")){
                        responseHeader (socket,"error");
                    }else  if (url.contains("/aaa")){
                        responseHeader (socket,"aaa");
                    }else  if (url.contains("/bbb")){
                        responseHeader (socket,"bbb");
                    }else  if (url.contains("/ccc")){
                        responseHeader (socket,"ccc");
                    }else  if (url.contains("/ccc")){
                      // ......
                    }
                }
            }
            socket.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
private static void responseHeader (Socket socket,String response) throws IOException {
    PrintWriter pw = new PrintWriter(socket.getOutputStream());
    pw.println("HTTP/1.1 200 OK");
    pw.println("Content-type:text/html");
    pw.println();
    pw.println("<h1>"+response+"</h1>");
    pw.flush();
}

Servlet 容器的实现

  可以发现,如果我们接口很多的话,我们需要在上面的HTTP服务器代码里写一大堆if else逻辑判断:如果是 A 请求就调 A 类的 M1 方法,如果是 B 请求就调 B 类的 M1 方法。这样的话 HTTP 服务器的代码跟业务完全耦合在一起了,如果新加一个业务方法还要改 HTTP 服务器的代码。

  为了解决HTTP服务器与业务的耦合,便有了 Servlet 接口,各种业务类都必须实现这个接口。

  HTTP服务器如何知道由哪个 Servlet 来处理呢?

  Servlet 容器闪亮登场,Servlet 容器负责加载和实例化 Servlet,管理它们的生命周期以及提供运行时环境。HTTP 服务器不直接跟业务类打交道,而是把请求交给 Servlet 容器去处理,Servlet 容器会将请求,转发到具体的 Servlet来处理该请求。然后将请求的数据传递给 Servlet,并接收 Servlet 处理后的响应数据。Servlet 容器再将响应数据发送回客户端。

Tomcat——总体架构解析

public static void main(String[] args) {
 try {
     //监听端口号8080
     ServerSocket ss = new ServerSocket(8080);
     while (true) {
         Socket socket = ss.accept();
         //获取客户端请求信息
         BufferedReader bd = new BufferedReader(new InputStreamReader(socket.getInputStream()));
         String requestHeader;
         while ((requestHeader = bd.readLine()) != null && !requestHeader.isEmpty()) {
             ServletProcessor processor = new ServletProcessor();
             //request response 转换类大家可以自己去实现
             processor.process(request, response);
         }
         socket.close();
     }
 } catch (IOException e) {
     e.printStackTrace();
 }
}

public class ServletProcessor {
  public static final String WEB_ROOT = System.getProperty("user.dir")
          + File.separator + "webroot";

  public void process(ServletRequest request, ServletResponse response) {
      String uri = "";
      String servletName = "";
      //类加载器,用于从指定JAR文件或目录加载类
      URLClassLoader loader = null;
      try {
          URLStreamHandler streamHandler = null;
          //创建类加载器
          loader = new URLClassLoader(new URL[]{new URL(null, "file:" + WEB_ROOT, streamHandler)});
      } catch (IOException e) {
          e.printStackTrace();
      }
      Class<?> myClass = null;
      try {
          //加载对应的servlet类
          myClass = loader.loadClass(servletName);
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
      Servlet servlet = null;
      try {
          //初始化servlet实例
          servlet = (Servlet) myClass.newInstance();
          //调用service方法
          servlet.service(request, response);
      } catch (Exception e) {
          e.printStackTrace();
      }

  }
}

  一个简单的 HTTP 服务器 + Servlet 容器实现了,通过动态类加载实现了业务逻辑与服务器解耦。使得程序员可以专注业务逻辑的开发。Servlet接口跟 Servlet容器这一整套规范叫作Servlet规范。

Tomcat总体架构

  前面我们已经了解了Tomcat 是一个HTTP 服务器 + Servlet 容器,所以需要实现以下2个核心功能:

  1. Socket 监听客户端请求连接,返回响应数据。

  2. 加载和管理 Servlet,以及具体处理请求

   Tomcat 设计了两个核心组件来分别做这两件事情:

  • Connector(连接器)负责开启Socket并监听客户端请求、返回响应数据;

  • Container(容器)负责具体的请求处理。

  它们分别拥有自己的start()stop()方法来加载和释放自己维护的资源。

Tomcat——总体架构解析   最顶层是 Server也就是一个 Tomcat 实例。一个 Server中有一个或者多个 Service,一个 Service 中有多个连接器Connector和一个 Container容器。连接器与容器之间通过标准的 ServletRequestServletResponse 通信

连接器(Connector)

  连接器的主要功能:

  • 监听服务器端口,读取客户端请求

  • 将请求数据按照具体的协议解析(HTTP/AJP)生成统一的请求对象。

  • 调用 Servlet 容器,进行业务处理

  • 得到响应对象,返回客户端

  连接器模块主要的两个核心组件:ProtocolHandlerAdapter    Tomcat——总体架构解析

  下面详细介绍这两个顶层组件

协议处理器(ProtocolHandler)

Tomcat支持多协议(HTTP/AJP)以及多种I/O方式(BIO/NIO)。由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO2 + AJP,因此使用了 ProtocolHandler 的接口来封装这两种变化点。

ProtocolHandler表示一个协议处理器,针对不同协议和I/O方式,提供了不同的实现,如下图Http11NioProtocol表示基于NIOHTTP协议处理器。

Tomcat——总体架构解析

  它还包含了2个重要部件:EndPointProcessor

EndPoint

EndPoint是一个接口,对应的抽象实现类是AbstractEndpoint。用于启动Socket监听,该接口按照I/O方式进行分类实现,如Nio2Endpoint表示非阻塞式Socket I/O。这有两个重要的子组件: AcceptorSocketProcessorTomcat——总体架构解析

  其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 提交到线程池(Executor)来执行。调用协议处理组件 Processor 进行处理。

Processor

Processor用于按照指定协议读取数据,并将请求交由容器处理,如Http11Processor实现了HTTP 1.1协议的解析方法和请求处理方式。

Tomcat——总体架构解析

Adapter组件

  由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来存放这些请求信息。通过 Processor调用 CoyoteAdapterSevice 方法,将 Tomcat Request 转成ServletRequest

小结

Tomcat——总体架构解析

  连接器用 ProtocolHandler 接口来封装通信协议和I/O 模型的差异, ProtocolHandler 内部又分为 EndPointProcessor 模块,EndPoint 负责底层 Socket通信,Proccesor 负责应用层协议解析。连接器通过适配器 Adapter 调用容器

容器(Container)

Tomcat里,容器就是用来装载Servlet的。那 TomcatServlet 容器是如何设计的呢?

Tomcat 设计了 4 种容器,分别是 Engine、Host、ContextWrapper。这 4 种容器不是平行关系,而是父子关系。

Tomcat——总体架构解析

请求定位 Servlet 的过程

  设计了这么多层次的容器,Tomcat 是怎么确定请求是由哪个 Wrapper 容器里的 Servlet 来处理的呢?

首先根据协议和端口号选定ServiceEngine

Tomcat 的每个连接器都监听不同的端口,一个 Service 组件里除了有多个连接器,还有一个容器组件,具体来说就是一个 Engine 容器,因此 Service 确定了也就意味着 Engine 也确定了

然后,根据域名选定 Host   通过 URL 中的域名去查找相应的 Host 容器

之后,根据 URL 路径找到 Context 组件

  根据 URL 的路径来匹配相应的 Web 应用的路径,找到了Context 容器

最后,根据 URL 路径找到 WrapperServlet

Mapper 再根据 web.xml 中配置的 Servlet 映射路径来找到具体的 WrapperServlet Tomcat——总体架构解析

Pipeline-Valve

  连接器中的 Adapter 会调用容器的 Service 方法来执行 Servlet,最先拿到请求的是 Engine 容器,对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,依此类推, 最后这个请求会传给 Wrapper 容器调用最终的 Servlet 来处理。

这个调用过程具体是怎么实现的呢?

  通过 Pipeline-Valve 责任链模式,在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。

     public interface Pipeline extends Contained {
            Valve getBasic();
            void setBasic(Valve var1);
            void addValve(Valve var1);
            Valve[] getValves();
            void removeValve(Valve var1);
            Valve getFirst();
            boolean isAsyncSupported();
            void findNonAsyncValves(Set<String> var1);
        }
        
        public interface Valve {
            Valve getNext();
            void setNext(Valve var1);
            void backgroundProcess();
            void invoke(Request var1, Response var2) throws IOException, ServletException;
            boolean isAsyncSupported();
        }

  整个调用过程起点是由 CoyoteAdapter中的 service()方法触发的,它会调用 Engine 的第一个 Valve

//CoyoteAdapter#service  Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
     request, response);
     

Tomcat——总体架构解析

Tomcat 请求处理流程

  了解完Tomcat整体架构之后,最后我们来看下请求处理流程

Tomcat——总体架构解析

  1. 当连接器Connector启动时,会同时启动其持有的Endpoint实例。Endpoint并行运行多个线程(由属性acceptorThreadCount确定),循环监听端口通信。

  2. 当监听到请求时,AcceptorSocket封装为SocketWrapper实例(此时并未读取数据),并交由一个SocketProcessor对象处理(此过程也由线程池异步处理)。此部分根据I/O方式的不同处理会有所不同。调用协议处理组件 Processor 进行处理。

  3. 根据连接器Connector的请求Request和响应Response 对象创建Servlet请求对象和响应对象。

  4. 转换请求参数并完成请求映射

  5. 得到当前Engine的第一个Valve并执行(invoke),以完成客户端请求处理。

Tomcat 启动流程

  最后我们来看下 Tomcat 启动流程

startup.sh 脚本启动

Tomcat——总体架构解析 1.Tomcat 本质上是一个 Java 程序,因此 startup.sh 脚本会启动一个 JVM 来运行启动类 Bootstrap

2.Bootstrap 的主要任务是初始化 Tomcat 的类加载器,并且创建 Catalina

3.Catalina 是一个启动类,它通过解析 server.xml、创建相应的组件,并调用 Serverstart 方法。

4.Server 组件的职责就是管理 Service 组件,它会负责调用 Servicestart 方法。

5.Service 组件的职责就是管理连接器和顶层容器 Engine,因此它会调用连接器和 Enginestart 方法。

Spring Boot 嵌入式启动

  为了方便开发和部署,Spring Boot 在内部启动了一个嵌入式的 Web 容器。在内嵌式的模式下,BootstrapCatalina 的工作就由 Spring Boot 来做了,其实就是调用了 TomcatAPI 来启动这些组件。

  要支持多种 Web 容器,Spring Boot 对内嵌式Web容器进行了抽象,定义了WebServer接口,各种 Web 容器比如 TomcatJetty 需要去实现这个接口。

public interface WebServer {
 void start() throws WebServerException;
 void stop() throws WebServerException;
 int getPort();
 default void shutDownGracefully(GracefulShutdownCallback callback) {
    callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
 }
}

Tomcat——总体架构解析

  主要看下 TomcatServletWebServerFactory#getWebSever 具体做了什么,以 Tomcat 为例,主要调用 TomcatAPI 去创建各种组件

public WebServer getWebServer(ServletContextInitializer... initializers) {
   if (this.disableMBeanRegistry) {
      Registry.disableRegistry();
   }
//1. 实例化一个 Tomcat,可以理解为 Server 组件。
   Tomcat tomcat = new Tomcat();
//2. 创建一个临时目录
   File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
   tomcat.setBaseDir(baseDir.getAbsolutePath());
//3. 初始化各种组件
   Connector connector = new Connector(this.protocol);
   connector.setThrowOnFailure(true);
   tomcat.getService().addConnector(connector);
   customizeConnector(connector);
   tomcat.setConnector(connector);
   tomcat.getHost().setAutoDeploy(false);
   configureEngine(tomcat.getEngine());
   for (Connector additionalConnector : this.additionalTomcatConnectors) {
      tomcat.getService().addConnector(additionalConnector);
   }
//4. 创建定制版的 "Context" 组件
   prepareContext(tomcat.getHost(), initializers);
   return getTomcatWebServer(tomcat);
}

那如何在 Spring Boot 中定制 Web 容器?

  通过通用的 Web 容器工厂 ConfigurableServletWebServerFactory,来定制一些 Web 容器通用的参数:

@Component
public class MyTomcatCustomizer implements
        WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.setPort(8081);
        factory.setContextPath("/hello");
        //设置使用协议
        factory.setProtocol("org.apache.coyote.http11.Http11NioProtocol");
    }
}

参考

转载自:https://juejin.cn/post/7241487780463362085
评论
请登录