Tomcat 从陌生到熟悉

摘要:本文从使用底层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 连接。
java 复制代码
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依赖。
xml 复制代码
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>${servlet-api.version}</version>
    <scope>provided</scope>
</dependency>
  • 然后继承HttpServlet,实现业务逻辑(如用户查询)。
java 复制代码
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 顶层接口,来实现层级管理、生命周期管理。

java 复制代码
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映射

核心简化代码如下

java 复制代码
// 核心映射方法
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);
}
java 复制代码
// 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。
java 复制代码
public interface Pipeline extends Contained {
  Container getContainer();
  // 必须将请求传递给下一层容器
  Valve getBasic();
  void addValve(Valve valve);
  void removeValve(Valve valve);
  Valve[] getValves();
}  
  • Valve接口声明了处理请求的一个逻辑单元,就像管道上一个个阀门,比如权限认证、记录日志等。每层容器都有一个基础实现,如StandardEngineValve、StandardWrapperValve。
java 复制代码
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的方法。

java 复制代码
void addLifecycleListener(LifecycleListener listener);
void removeLifecycleListener(LifecycleListener listener);

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

Tomcat 定义一个基类 LifeCycleBase 来实现 LifeCycle 接口,把一些公共的逻辑放到

基类中去,比如生命状态的转变与维护、生命周期事件的触发以及监听器的添加和删除等,

而子类就负责实现自己的初始化、启动和停止等方法。

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

相关推荐
橙淮2 小时前
并发编程(六)
java·jvm
拽着尾巴的鱼儿2 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
白露与泡影2 小时前
2026大厂Java面试题大全!牛客网最新版
java·开发语言
EntyIU3 小时前
JVM内存与GC笔记
java·jvm·笔记
XS0301063 小时前
并发编程 六
java·后端
yaoxin5211233 小时前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道3 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试
x***r1513 小时前
linux安装 jdk-8u291-linux-x64.tar.gz 详细步骤(解压配置环境变量)
java
极光代码工作室4 小时前
基于SpringBoot的校园论坛系统
java·springboot·web开发·后端开发
XS0301064 小时前
Spring Bean 作用域 & 生命周期
java·后端·spring