Tomcat 实现思路一瞥

2021-02-08 1132点热度 0人点赞 0条评论

实现思路猜想

在正式开始看 Tomcat 源码前,我们先猜测一下它大致的实现思路。

Tomcat 官网的描述如下:

The Apache Tomcat® software is an open source implementation of the Jakarta Servlet, Jakarta Server Pages, Jakarta Expression Language, Jakarta WebSocket, Jakarta Annotations and Jakarta Authentication specifications. These specifications are part of the Jakarta EE platform.

Tomcat 作为一个 Web 容器,其底层一定是通过某种机制建立和管理 Socket 连接,在实现网络服务的。

class MyTomcat {
    ServerSocket server = new ServerSocket(8080);
    Socket socket = server.accept();
    // 处理请求
    socket.getInputStream();
    socket.getOutputStream();
}

而 Tomcat 同时又作为 Servlet 容器,一个 Servlet 规范如下:

public interface Servlet {
    public void init(ServletConfig config) throws ServletException;
    public ServletConfig getServletConfig();
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
    public String getServletInfo();
    public void destroy();
}

一个基于 Servlet 实现的服务类,通过覆写 doGet()doPost()方法提供服务:

// 一个业务代码类
class MyServiceServlet extends HttpServlet {
    @Override
    void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
        // handle GET request
    }

    @Override
    void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
        // handle POST request
    }
}

然后在 web.xml 中配置 Servlet 与 URL 的映射,即可对外提供 Web 服务。

<servlet>
    <servlet-name>myServlet</servlet-name>
    <servlet-class>com.myservice.servlets.MyServiceServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>myServlet</servlet-name>
    <url-pattern>/my-service</url-pattern>
</servlet-mapping>

有了 Servlet 规范,我们可以管理一个 Servlet 容器,来提供 Web 服务。

class MyTomcat {
    List<Servlet> servlets;

    ServerSocket server = new ServerSocket(8080);
    Socket socket = server.accept();

    // 通过扫描 web.xml 或注解,管理 Servlet 对象容器
    list.add(servlets);
}

实现思路验证

基于上述的猜想,我们想在源码中验证两件事:

  1. Tomcat 中用于监听 Socket 端口的服务
  2. Tomcat 中 Servlet 容器的管理

Endpoint.bind()

从 Tomcat 的启跟踪调用栈如下:

bind:220, NioEndpoint (org.apache.tomcat.util.net)
init:1147, AbstractEndpoint (org.apache.tomcat.util.net)
init:222, AbstractJsseEndpoint (org.apache.tomcat.util.net)
init:599, AbstractProtocol (org.apache.coyote)
init:80, AbstractHttp11Protocol (org.apache.coyote.http11)
initInternal:1074, Connector (org.apache.catalina.connector)
init:136, LifecycleBase (org.apache.catalina.util)
initInternal:552, StandardService (org.apache.catalina.core)
init:136, LifecycleBase (org.apache.catalina.util)
initInternal:843, StandardServer (org.apache.catalina.core)
init:136, LifecycleBase (org.apache.catalina.util)
load:639, Catalina (org.apache.catalina.startup)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
load:305, Bootstrap (org.apache.catalina.startup)
main:475, Bootstrap (org.apache.catalina.startup)

可以看到 NIOEndpoint.bind()方法(Tomcat 8.5.x 后默认):

public abstract class AbstractEndpoint<S> {
    public void init() throws Exception {
        if (bindOnInit) {
            bind();
            bindState = BindState.BOUND_ON_INIT;
        }
        // ...
    }
}

public class NioEndpoint extends AbstractJsseEndpoint<NioChannel> {
    @Override
    public void bind() throws Exception {
        if (!getUseInheritedChannel()) {
            serverSock = ServerSocketChannel.open();
            socketProperties.setProperties(serverSock.socket());
            InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
            serverSock.socket().bind(addr,getAcceptCount());
        }
        // ...
    }
}

此处验证了在 Endpoint 类的底层,确实是通过建立 Socket 连接的方式提供了 Web 服务。

如果 Spring Boot 内置的 Tomcat,也会有相似的代码吗?

Servlet 容器管理

首先我们找到 Tomcat 中管理 Web 项目的类 Context.javaTomcat 文档中是这么描述的:

A Context represents a web application. A Host may contain multiple contexts, each with a unique path. The Context interface may be implemented to create custom Contexts, but this is rarely the case because the StandardContext provides significant additional functionality.

因此,在 Tomcat 中,Context.class 就代表这一个 Web 项目,我们继续看他的实现类 StandardContext.java

public class StandardContext extends ContainerBase
        implements Context, NotificationEmitter {

    public boolean loadOnStartup(Container children[]) {

        // Collect "load on startup" servlets that need to be initialized
        TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
        for (Container child : children) {
            Wrapper wrapper = (Wrapper) child;
            int loadOnStartup = wrapper.getLoadOnStartup();
            if (loadOnStartup < 0) {
                continue;
            }
            Integer key = Integer.valueOf(loadOnStartup);
            ArrayList<Wrapper> list = map.get(key);
            if (list == null) {
                list = new ArrayList<>();
                map.put(key, list);
            }
            list.add(wrapper);
        }
        // ...
    }
}

那么,这里的 Wrapper 对象和 Servlet 对象是什么关系呢?我们继续在 ContextConfig 类中,我们可以看到解析 web.xml 的代码片段:

public class ContextConfig implements LifecycleListener {
    private void configureContext(WebXml webxml) {
        for (ServletDef servlet : webxml.getServlets().values()) {
            Wrapper wrapper = context.createWrapper();

            if (servlet.getLoadOnStartup() != null) {
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            if (servlet.getEnabled() != null) {
                wrapper.setEnabled(servlet.getEnabled().booleanValue());
            }
            wrapper.setName(servlet.getServletName());

            // ...
            wrapper.setOverridable(servlet.isOverridable());
            context.addChild(wrapper);
        }
    }
}

可以看出,这里的 Wrapper 对象就是 Servlet 的包装器。至此,我们找到了 Tomcat 作为 Servlet 容器,解析并加载 Servlet 的代码。

从配置文件看架构图

我们从 Tomcat 的配置文件 web.xml 的嵌套结构中,可以得到其组件的架构图。

tomcat-8-architecture

对于一个 HTTP 请求http://localhost:8080/myApp/login,解析过程如下:

  1. 根据请求的协议 HTTP 与监听端口 8080,由 Service 交给 HTTP Connector 处理;
  2. Connector 将请求交给 Engine;
  3. Engine 找到响应域名 localhost 的虚拟主机 Host;
  4. Host 查找监听 Web 资源 myApp 的 WebApp(即 Context);
  5. Context 将 request 交由负责拦截/login 路径的 Servlet 处理,此时开始执行我们定义的业务逻辑代码。

Tomcat 组件

Valve

Valve 是 Tomcat 容器级别的拦截器(与应用级别的 Filter 不同),我们可以用来拦截请求并做一些设置,如拦截请求并生成 Request TraceId 。

public class RequestTraceIdValve extends ValveBase {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String traceId = httpRequest.getHeader("X-Trace-Id");
        // 如果客户端没传 Trace Id,拦截并生成一个
        if (null == traceId) {
            traceId = UUID.randomUUID().toString();
            org.apache.coyote.Request coyoteRequest = request.getCoyoteRequest();
            MimeHeaders mimeHeaders = coyoteRequest.getMimeHeaders();
            MessageBytes messageBytes = mimeHeaders.addValue("X-Trace-Id");
            messageBytes.setString(traceId);
        }
        getNext().invoke(request, response);
    }
}

Manager

Manager 组件是用来管理一个 Context(Web App)中的 Session 资源池的,提供了创建 Session 、查询 Session 、销毁 Session 等接口。

public interface Manager {
    public Session createSession(String sessionId);
    public Session findSession(String id) throws IOException;
    public void remove(Session session);
    // ...
}

Connector 组件

Endpoint 容器

Endpoint 容器提供了对于传输层协议(TCP/UDP)的抽象,通过 Socket 代码来实现。

Tomcat 8.5 取消了对 BIO 的 JIoEndpoint 的支持,支持的 IO 方式有 AprEndpointNioEndpointNio2Endpoint

web.xml 文件中,默认的 Connector 对象配置如下:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />

Connector.java 中,看到构造函数中调用 setProtocol 方法:

public class Connector extends LifecycleMBeanBase  {

    public Connector() {
        this(null);
    }

    public Connector(String protocol) {
        setProtocol(protocol);
        // ...
    }

    public void setProtocol(String protocol) {

        boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
                AprLifecycleListener.getUseAprConnector();

        if ("HTTP/1.1".equals(protocol) || protocol == null) {
            if (aprConnector) {
                setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
            } else {
                setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
            }
        }
        // ...
    }
}

可以看到默认的 NIO 实现类是 org.apache.coyote.http11.Http11NioProtocol

Processor

Endpoint 组件接收到大量请求后,将其交由对应 ProtocolHandler 的线程池 Executor,通过事件触发 Processor 接口方法来处理请求,将 Socket 连接转换成 org.apache.coyote.Request 对象。

public interface Processor {
    SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status) throws IOException;
}

Adapter

Adapter 组件的 service()方法将 org.apache.coyote.Request 对象,包装成 HttpServletRequest 对象。这里是典型的适配器设计模式的应用。

public class CoyoteAdapter implements Adapter {

    @Override
    public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
            throws Exception {

        org.apache.catalina.connector.Request request = (Request) req.getNote(ADAPTER_NOTES);
        org.apache.catalina.connector.Response response = (Response) res.getNote(ADAPTER_NOTES);

        // ...
    }
}

public class Request implements HttpServletRequest { }

官方文档对 Connector 部分的启动过程描述如下图。

tomcat-request-flow-connector

SilverLining

也可能是只程序猿

文章评论