Tomcat 从陌生到熟悉

0 阅读8分钟

摘要:本文从使用底层API实现web容器说起,介绍了Servlet的本质,说明了Tomcat所要实现的功能。简要介绍了Tomcat的核心组件Connector、Container,以及http请求如何被处理。使读者对Tomcat核心内容有了整体认知。

本文中源码来自tomcat 9.0.x版本,地址github.com/apache/tomc…

一 最初的模样

1.1 简易Web容器

在 Tomcat 等成熟 Web 容器出现之前,开发者主要基于java.net 包(核心是 ServerSocketSocket)手动实现简易 Web 服务器。 现在,我们使用底层API来实现一个Web Server,直观地感受 HTTP 协议和 Web 服务器的底层原理 。

首先,需要明确Web 服务器本质:

  1. 监听指定端口(如 8080),等待客户端(浏览器)连接;
  2. 接收客户端发送的 HTTP 请求,解析请求行 / 头 / 体;
  3. 根据请求路径返回对应的 HTTP 响应(静态资源或动态内容);
  4. 关闭 Socket 连接。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * 基于 java.net 实现的简易 Web 服务器,访问静态资源
 */
public class SimpleWebServer {

  // 服务器监听端口
  private static final int PORT = 8080;
  // 静态资源根目录(如 "D:/webroot")
  private static final String WEB_ROOT = System.getProperty("user.dir") + "/webroot";

  public static void main(String[] args) {
    try (ServerSocket serverSocket = new ServerSocket(PORT)) {
      while (true) {
        // 阻塞等待客户端连接,获取 Socket 实例
        Socket clientSocket = serverSocket.accept();
        System.out.println("新客户端连接:" + clientSocket.getInetAddress());
        // 启动独立线程处理
        new Thread(new RequestHandler(clientSocket)).start();
      }
    } catch (IOException e) {
      System.err.println("服务器启动失败:" + e.getMessage());
      e.printStackTrace();
    }
  }

  /**
   * 处理单个客户端请求的线程类
   */
  static class RequestHandler implements Runnable {

    private final Socket clientSocket;

    public RequestHandler(Socket clientSocket) {
      this.clientSocket = clientSocket;
    }

    @Override
    public void run() {
      try (
          BufferedReader reader = new BufferedReader(
              new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8)
          );
          OutputStreamWriter writer = new OutputStreamWriter(
              clientSocket.getOutputStream(), StandardCharsets.UTF_8
          )
      ) {
        // 1. 解析 HTTP 请求行(第一行格式:GET /index.html HTTP/1.1)
        String[] requestParts = requestLine.split(" ");
        String method = requestParts[0];
        String requestPath = requestParts[1];

        if ("GET".equalsIgnoreCase(method)) {
          handleGetRequest(requestPath, writer);
        } else {
          // 仅支持 GET 方法,其他返回 405
          sendResponse(writer, 405, "Method Not Allowed", "不支持的请求方法:" + method);
        }
      } catch (IOException e) {
        System.err.println("处理客户端请求失败:" + e.getMessage());
      } finally {
        try {
          // 关闭客户端连接(HTTP 1.0 短连接)
          clientSocket.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }

    /**
     * 处理 GET 请求,返回静态资源或默认页面
     */
    private void handleGetRequest(String requestPath, OutputStreamWriter writer) throws IOException {
      // 读取服务器本地文件
      sendResponse(writer, 200, "OK", content.toString());
    }

    /**
     * 通用响应发送方法
     */
    private void sendResponse(OutputStreamWriter writer, int statusCode, String statusMsg, String content) throws IOException {
      // 构建 HTTP 响应头
      writer.write("HTTP/1.1 " + statusCode + " " + statusMsg + "\r\n");
      writer.write("Content-Type: text/html; charset=UTF-8\r\n");
      writer.write("Content-Length: " + content.getBytes(StandardCharsets.UTF_8).length + "\r\n");
      // 短连接
      writer.write("Connection: close\r\n");
      writer.write("\r\n");
      // 写入响应体
      writer.write(content);
      writer.flush();
    }
  }
}

上面代码仅仅提供了访问静态资源这一基础功能,无法用于企业生产。而Tomcat等web容器提供了更强大的功能:

  • 多线程优化(线程池、NIO 而非 BIO);
  • HTTP 协议完整支持(POST/PUT/DELETE、Cookie/Session、文件上传);
  • Servlet 规范实现(动态资源处理、请求分发);
  • 安全控制、性能优化、集群部署等企业级特性。

其实,Tomcat 本质是对原生 java.net / java.nio 的封装,解决了原生开发的复杂性,实现了 Servlet 规范和企业级特性。

1.2 Servlet原生实现

在Tomcat等Web容器出现后,我们是如何做服务端开发的?这就需要用到javax.servlet 包,通过实现Servlet接口,提供访问动态资源能力。 而Servlet本质就是对 HTTP 请求的封装

  • 首先在maven项目的 pom.xml 中引入Servlet依赖。
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>${servlet-api.version}</version>
    <scope>provided</scope>
</dependency>
  • 然后继承HttpServlet,实现业务逻辑(如用户查询)。
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 基于标准 javax.servlet.Servlet 实现的用户列表查询 Servlet
 */
@WebServlet("/user/list") // 映射路径:http://ip:port/user/list
public class UserListServlet extends HttpServlet {

    /**
   * 处理 GET 请求(核心方法)
   */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        List<User> userList = queryUsers();
        writer.write(JSON.toJSONString(userList));
        writer.flush();
        writer.close();
    }

    /**
   * 模拟数据库查询
   */
    public List<User> queryUsers() throws ServletException {
        List<User> userList = new ArrayList<>();
        userList.add(new User(1, "张三", 25));
        userList.add(new User(2, "李四", 30));
        userList.add(new User(3, "王五", 28));
        return userList;
    }

    /**
   * 销毁方法(Servlet 生命周期:服务器关闭时调用)
   */
    @Override
    public void destroy() {
        super.destroy();
    }
}

二 Tomcat概览

Tomcat 其实主要做了两件事:

  1. HTTP 服务器:接收请求、解析协议、返回响应
  2. Servlet 容器:管理 Servlet、Filter、Listener、Session、类加载

2.1 核心组件

Tomcat有 4 层核心组件:

  • Server:整个 Tomcat 实例,包含多个Service,并负责启动和管理它们;
  • Service:一组 Connector + 一个 Engine
  • Connector:接收网络请求(HTTP、AJP), 通过标准的 ServletRequest、ServletResponse与Container通信,输出响应;
  • Container:容器层级,Engine → Host → Context → Wrapper

2.2 Connector组件

Connector是Tomcat 与客户端的连接器,监听固定端口接收外部请求,并将 Servlet的处理结果响应给外部。它有 3 个核心功能:

  1. 基于Socket实现网络通信;
  2. 应用层协议解析,如http;
  3. Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。

为此,Tomcat 的设计者设计了 3 个组件:EndPoint、Processor 和 Adapter。

  1. EndPoint:封装Socket,提供字节流给 Processor,接收响应写出字节流到Socket;
  2. Processor:处理应用层协议,负责提供 Tomcat Request 对象给 Adapter,响应 Tomcat Response给EndPoint ;
  3. Adapter:负责提供 ServletRequest 给容器,并接收ServletResponse。

由于 I/O 模型和应用层协议可以自由组合,Tomcat 将网络通信和应用层协议的实现放在一起,设计了ProtocolHandler 接口。 实现类如AjpNioProtocol、Http11NioProtocol等。

2.3 Container组件

Tomcat中设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。它们之间不是平行关系,而是父子关系

  1. Engine:引擎,Servlet 的顶层容器,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine;
  2. Host:虚拟站点,负责 web 应用的部署和 Context 的创建;
  3. Context:Web 应用上下文,包含多个 Wrapper,负责 web 配置的解 析、管理所有的 Web 资源。一个Context对应一个 Web 应用程序。
  4. Wrapper:最底层的容器,是对 javax.servlet.Servlet的封装,负责 Servlet 实例的创建、执行和销毁。

同时提供了 Container 顶层接口,来实现层级管理、生命周期管理。

public interface Container extends Lifecycle {
    // 部分方法
    public void setName(String name); 
    public void setParent(Container container); 
    public void addChild(Container child); 
    public void removeChild(Container child); 
    public Container findChild(String name);
}

三 核心工作原理

我们已知晓Tomcat支持http协议,实现了Servlet规范。当客户端发送某个请求时,在Tomcat中又是如何被处理的?

3.1 定位servlet

当客户端或浏览器访问某个URL(如http://localhost:8080/myapp/user/login)时,如何定位到我们自定义的业务接口呢?

Tomcat中提供了Mapper接口,使用多级映射表,存储维护URL到容器的映射关系。按照 精确匹配 > 路径匹配 > 扩展名匹配 > 默认Servlet 的优先级,完成URL到Servlet的查找。

这块涉及3个组件:

  1. Mapper,映射器,维护URL到容器的映射关系
  2. MapperListener:监听器,动态更新Mapper中的映射关系
  3. CoyoteAdapter:适配器,连接Connector和Container,调用Mapper进行URL映射

核心简化代码如下

// 核心映射方法
public void map(MessageBytes host, MessageBytes uri, String version,
                MappingData mappingData) {
    // 1. 查找Host
    MappedHost[] hosts = this.hosts;
    MappedHost mappedHost = exactFindIgnoreCase(hosts, host);

    // 2. 查找Context
    ContextList contextList = mappedHost.contextList;
    MappedContext[] contexts = contextList.contexts;

    // 3. 查找Wrapper
    internalMapWrapper(contextVersion, uri, mappingData);
}
// org.apache.catalina.connector.CoyoteAdapter
public void service(org.apache.coyote.Request req, 
                    org.apache.coyote.Response res) {
    // 创建Request和Response对象
    Request request = (Request) req.getNote(ADAPTER_NOTES);
    Response response = (Response) res.getNote(ADAPTER_NOTES);
    
    // 执行URL映射
    connector.getService().getMapper().map(
        serverName, decodedURI, version, request.getMappingData());
    
    // 调用容器处理
    connector.getService().getContainer().getPipeline()
        .getFirst().invoke(request, response);
}

3.2 执行servlet

连接器中Adapter#service 方法,会获取Engine 容器处理请求,然后传给子容器 Host 继续处
理,依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用某个 Servlet 来执行业务逻辑。

整个过程使用了 Pipeline-Valve 管道,类似于 职责链模式。

  • Pipeline接口是对管道的抽象,每个容器都关联一个 Pipeline 实例。一个Pipeline中有多个有序的Value对象,最后一个Value负责将请求传递给下一层Container。
public interface Pipeline extends Contained {
  Container getContainer();
  // 必须将请求传递给下一层容器
  Valve getBasic();
  void addValve(Valve valve);
  void removeValve(Valve valve);
  Valve[] getValves();
}  
  • Valve接口声明了处理请求的一个逻辑单元,就像管道上一个个阀门,比如权限认证、记录日志等。每层容器都有一个基础实现,如StandardEngineValve、StandardWrapperValve。
public interface Valve {
  Valve getNext();
  void invoke(Request request, Response response) throws IOException, ServletException;
}

用一张图表示整个过程

最终在StandardWrapperValve中,调用了Filter、Servlet。

那么,Valve 和 Filter 有什么区别:Valve 是 Tomcat 的私有机制,其他Web容器可没有;Filter是Servlet API 标准的接口,所有 Web 容器都必须支持。

3.3 生命周期管理

Tomcat通过 Lifecycle 接口,提供了一致的机制来启动和停止各个组件。

上层组件的初始化会触发子组件的初始化,上层组件的启动会触发子组件的启动。因此我们把组件的生命周期定义成一个个状态,把状态的转变看作是一个事件,通过监听器观察状态变化,触发相应的动作。这不就是观察者模式吗?

Lifecycle中提供了添加、移除LifecycleListener的方法。

void addLifecycleListener(LifecycleListener listener);
void removeLifecycleListener(LifecycleListener listener);

LifecycleListener仅有一个方法,实现监听事件LifecycleEvent。使用LifecycleState建立状态和事件的映射关系。

Tomcat 定义一个基类 LifeCycleBase 来实现 LifeCycle 接口,把一些公共的逻辑放到
基类中去,比如生命状态的转变与维护、生命周期事件的触发以及监听器的添加和删除等,
而子类就负责实现自己的初始化、启动和停止等方法。

LifeCycleBase 还定义了相应的抽象方法交给具体子类去实现。这儿使用了模板方法模式