架构笔记(2):Tomcat是如何做到高内聚低耦合的?
服务端 与 Clients 进行交互的机制?都是基于 Request/Response。
第二个思维模型:需求至简。
从需求来说,Tomcat想解决什么核心的问题?
这个问题应该是最稳定的,否则相当于没有一个要解决的核心问题。反过来说,如果业务需求总是一直在变,抓不住稳定点,那么就需要反思,业务主要解决的核心问题到底是什么?
Tomcat 要实现二个核心功能:
-
连接器(Connector ):连接器负责对外交流,处理 Socket 连接,负责网络字节流与Request/Response 对象的转化。 -
容器(Container):加载和管理Servlet,以及具体处理 Request 请求。
连接器(Connector)是如何设计的?连接器的功能是什么?
需要进一步: 拆。
-
监听网络端口。 -
接受网络连接请求。读取请求网络字节流。 -
根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。将 Tomcat Request 对象转成标准的 ServletRequest。 -
调用 Servlet 容器,得到 ServletResponse。 -
ServletResponse 转成 Tomcat Response 对象。 -
Tomcat Response转成网络字节流,将响应字节流写回给浏览器。
连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别,无论是 HTTP 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。
连接器应该有哪些子模块?
第三个思维模型:高内聚、低耦合。
通过知识结构化,软件设计的最终目标是高内聚低耦合。所有架构的问题也都是耦合的问题(后续再详细展开)。
连接器有需要完成 3 个高内聚的功能:
-
网络通信。 -
应用层协议解析。 -
Request/Response 与 ServletRequest/ServletResponse 的转化。
Tomcat设计了 3 个组件来实现,分别是 EndPoint、Processor 和 Adapter。
低耦合是模块之间要尽可能减少和降低依赖的程度,不产生强依赖。组件之间通过抽象接口交互,封装变化,这是面向对象设计的精髓。将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。
变化点:
1. 网络通信的 I/O 模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。
2. 应用层协议也是变化的,可能是HTTP、HTTPS、AJP。
3. 浏览器端发送的请求信息也是变化的。
稳定点:
整体的处理逻辑是不变的,EndPoint负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest对象给容器。
如果要支持新的I/O方案、新的应用层协议,只需要实现相关的具体子类,上层通用的处理逻辑是不变的。
由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO2+AJP。Tomcat将网络通信和应用层协议解析放在一起考虑,设计了一个叫ProtocolHandler的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。比如:Http11NioProtocol 和 AjpNioProtocol。
还有一些相对稳定的部分,因此 Tomcat 设计了一系列抽象基类来封装这些稳定的部分,抽象基类 AbstractProtocol 实现了 ProtocolHandler 接口。每一种应用层协议有自己的抽象基类,比如 AbstractAjpProtocol和AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。
目的是尽量将稳定的部分放到抽象基类,同时每一种 I/O 模型和协议的组合都有相应的具体实现类,在使用时可以自由选择。
这个组件接口是什么样的?如何与其它组件连接在一起的?
连接器模块用三个核心组件:Endpoint 和 Processor放在一起抽象成了ProtocolHandler 接口。
连接器用ProtocolHandler来处理网络连接和应用层协议,有 2 个重要部件::
-
EndPoint 是通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,EndPoint是用来实现 TCP/IP 协议的。 -
Acceptor 用于监听 Socket连接请求。SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 Run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。而这个线程池叫作执行器(Executor)。
EndPoint 是用来实现 TCP/IP 协议的,那么 Processor 用来实现 HTTP 协议, Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。
Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AJPProcessor、HTTP11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。
1. EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理。例如Nio2Endpoint中定义一个SocketProcessor内部类。SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议。
/**
* NIO2 endpoint.
*/
public class Nio2Endpoint extends AbstractJsseEndpoint<Nio2Channel,AsynchronousSocketChannel> {
// ---------------------------------------------- SocketProcessor Inner Class
/**
* This class is the equivalent of the Worker, but will simply use in an
* external Executor thread pool.
*/
protected class SocketProcessor extends SocketProcessorBase<Nio2Channel> {
public SocketProcessor(SocketWrapperBase<Nio2Channel> socketWrapper, SocketEvent event) {
super(socketWrapper, event);
}
@Override
protected void doRun() {
boolean launch = false;
try {
......
} catch (VirtualMachineError vme) {
......
} finally {
......
}
}
}
2. Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。
3. Adapter 组件 由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来“存放”这些请求信息。ProtocolHandler 接口负责解析请求并生成 Tomcat Request 类。但是这个 Request 对象不是标准的 ServletRequest,也就意味着, 不能用 Tomcat Request 作为参数来调用容器。Tomcat 定义了 CoyoteAdapter(适配器模式),连接器调用 CoyoteAdapter 的 Sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest。
5. 再调用容器的 Service 方法。容器负责内部处理。加载和管理 Servlet,以及具体处理 Request 请求。
Tomcat 的整体架构包含了两个核心组件连接器和容器。连接器负责对外交流,容器负责内部处理。连接器用 ProtocolHandler 接口来封装通信协议和 I/O 模型的差异,ProtocolHandler 内部又分为 EndPoint 和 Processor 模块,EndPoint 负责底层 Socket通信,Proccesor 负责应用层协议解析。
连接器通过适配器 Adapter 调用容器。
总结:
1 需求至简
1 需求最核心问题是什么?如何被满足? 2 规格(接口)
2 高内聚低耦合
3 变化点和稳定点:
用接口和抽象基类去封装稳定点,在抽象基类中定义模板方法,让子类自行实现抽象方法,也就是具体子类去实现变化点)