tomcat 需求
前文提到,Tomcat 要完成 5个工作。通过分离变化的模块,得到两部分:
1 处理连接,负责网络字节流与 Request 和 Response 对象的转化。
2 加载和管理 Servlet,以及根据 url 路径来调用具体的 Servlet 来处理 Request 请求。
Tomcat 设计了两个核心组件:
连接器(Connector),负责对外交流。
容器(Container),负责内部处理。
tomcat 架构
I/O 模型和应用层协议
Tomcat 支持的 I/O 模型有:
NIO:非阻塞 I/O,采用 Java NIO 类库实现。
NIO.2:异步 I/O,采用 JDK 7 最新的 NIO.2 类库实现。
APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。
Tomcat 支持的应用层协议有:
HTTP/1.1:这是大部分 Web 应用采用的访问协议。
AJP:用于和 Web 服务器集成(如 Apache)。
HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
Service 组件
为了支持多种 I/O 模型和应用层协议,一个容器可能对接多个连接器。
连接器与容器通过标准的 ServletRequest 和 ServletResponse 通信。
两者构成的整体叫作 Service 组件。
Service 组件 只是在连接器和容器外面封装的一层,相当于上层领导。
这里的 Server 指的就是一个 Tomcat 实例。
一个 Server 中有一个或者多个 Service。
一个 Service 中有多个连接器和一个容器。
Servlet 容器在启动时会加载 Web 应用。
通过配置多个 Service,可以通过不同的端口号来访问同一台机器上部署的不同应用。
连接器的设计
连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别。无论是 HTTP 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。
连接器的需求
这里的需求,其实就是上一篇中tomcat要完成的工作,只不过处理请求是调用了 Servlet 容器。
注意connector,就是处理和业务相关的逻辑。这是顶层的请求,要和nginx进行区分。redis、mysql的请求是数据的请求。
需求列表:
监听网络端口。
接受网络连接请求。
读取网络请求字节流。
根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。
将 Tomcat Request 对象转成标准的 ServletRequest。
调用 Servlet 容器,得到 ServletResponse。(外包!)
将 ServletResponse 转成 Tomcat Response 对象。
将 Tomcat Response 转成网络字节流。
将响应字节流写回给浏览器。
连接器的模块化设计
列清楚了详细的功能列表,还要进行模块化设计。
优秀的模块化设计应该考虑高内聚、低耦合。
高内聚是指相关度比较高的功能要尽可能集中,不要分散。
低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。
连接器需要完成 3 个高内聚的功能,分别对应三个功能模块:
Endpoint:网络通信。
Processor:应用层协议解析。
Adapter:Tomcat Request/Response 与 Servlet Request/Response 的转化。
三个模块各自都是变化的:网络通信的 I/O 模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。
但是整体的处理逻辑是不变的:Endpoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容器。
由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO.2 + AJP。
Tomcat 设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫 ProtocolHandler 的接口来封装这两种变化点。
各种协议和通信模型的组合有相应的具体实现类,比如:Http11NioProtocol 和 AjpNioProtocol。
连接器的类层次
抽象基类 AbstractProtocol 实现了 ProtocolHandler 接口。每一种应用层协议有自己的抽象基类,比如 AbstractAjpProtocol 和 AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。
连接器组件剖析
连接器有三个核心组件:Endpoint、Processor 和 Adapter。它们分别完成网络通信,应用层协议解析,tomcat request/response到servlet request/response的转化。其中 Endpoint 和 Processor 放在一起抽象成了 ProtocolHandler 组件,关系如下图所示。
连接器组件之间的交互:
Endpoint 子组件
Endpoint 是一个接口,抽象实现类是 AbstractEndpoint,具体子类有 NioEndpoint 和 Nio2Endpoint 等。
具体子类有两个内部类(子组件):Acceptor 和 SocketProcessor。
Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求。
SocketProcessor 实现了 Runnable 接口,在 run 方法里调用协议处理组件 Processor 进行处理。
为了提高处理能力,SocketProcessor 被提交到线程池来执行。
Processor 子组件
Processor 是一个接口,抽象实现类是 AbstractProcessor,具体子类有 AjpProcessor、Http11Processor 等。
具体子类实现了特定协议的解析方法和请求处理方式。
Processor 接收来自 Endpoint 的 Socket,读取字节流解析成 Tomcat 的 Request 和 Response 对象。
Processor 调用 Adapter 的 Service 方法,通过 Adapter 将其提交到容器处理。
Processor线程最终会调用业务代码。
Adapter 子组件与 request 对象
由于协议不同,客户端发过来的请求信息也不尽相同。Tomcat 定义了自己的 Request 类来“存放”这些请求信息。ProtocolHandler 接口负责解析请求并生成 Tomcat Request 类。
但是这个 Request 对象不是标准的 ServletRequest,也就意味着,不能用 Tomcat Request 作为参数来调用 servlet 的 service 方法。
// servlet提供的接口
void service(ServletRequest req, ServletResponse res)throws ServletException, IOException;
Tomcat 引入 CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapter 的 sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 service 方法。
连接器和 Netty 的比较
可以把Netty理解成Tomcat中的 Connector,它们都负责网络通信,都利用了Java NIO非阻塞特性。Netty素以高性能高并发著称,为什么Tomcat不把连接器替换成Netty呢?
第一个原因是Tomcat的连接器性能已经足够好了,同样是Java NIO编程,套路都差不多。
第二个原因是Tomcat做为Web容器,需要考虑到Servlet规范,Servlet规范规定了对HTTP Body的读写是阻塞的###,因此即使用到了Netty###,也不能充分发挥它的优势。Netty一般用在非HTTP协议和非Servlet的场景下。
设计心得
列清楚功能列表
模块化设计
根据高内聚低耦合的原则确定子模块。
高内聚是指相关度比较高的功能要尽可能集中,不要分散。
低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。
实现子模块
找出子模块中的变化点和不变点。
用接口和抽象基类去封装不变点,在抽象基类中定义模板方法。
子类自行实现抽象方法,也就是具体子类去实现变化点。
问题
tomcat到底是怎么解析http协议,读取数据的?
延迟解析!
数据包的后续操作比如decode呢?