1 Tomcat核心功能
我们知道如果要设计一个系统,首先是要了解需求。 Tomcat 要实现本质是2个核心功能
- 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象相互转化。
- 加载和管理 Servlet,以及具体处理 Request 请求。
2 Tomcat架构设计
通过tomcat功能可以将Tomcat设计了两个核心组件连接器(Connector)和容器(Container)来分别来处理对外交流,内部处理。
Tomcat中一个容器可能对接多个连接器,每一个连接器都对应某种协议某种IO模型,tomcat将单个容器和多个连接器组成一个service组件,一个tomcat中可能存在多个Service组件
Connector:将不同协议不同IO模型的请求转换为标准的标准的 ServletRequest 对象交给容器处理。
Container:Container本质上是一个Servlet容器,负责servelt的加载和管理,处理请求ServletRequest,并返回标准的 ServletResponse 对象给连接器。
3 Connector架构设计
3.1 核心功能
1 监听网络端口,等待客户端网络连接请求。
2 接受客户端网络连接请求,于客户端建立Socket连接,并监听客户端发送的请求。
3 从Socket中读取客户端请求网络字节流。
4 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。
5 将 Tomcat Request 对象转成标准的 ServletRequest。
6 查找并调用当前请求适配的Servlet 容器,得到 ServletResponse
7 Servlet 容器处理完成后,将 ServletResponse 转成 Tomcat Response 对象。
8 根据具体应用层协议(HTTP/AJP)将 Tomcat Response 转成网络字节流,写入Socket中,。
9 将Socket中响应字节流发送给客户端。
3.2 支持多个协议
Tomcat 支持的应用层协议
HTTP/1.1:这是大部分 Web 应用采用的访问协议。
AJP:用于和 Web 服务器集成(如 Apache)。
HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
3.3 支持多个I/O模型
NIO:非阻塞 I/O,采用 Java NIO 类库实现
NIO2:异步 I/O,采用 JDK 7 最新的 NIO2 类库实现。
APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。
3.4 通用架构设计
优秀的模块化设计应该考虑高内聚、低耦合。
- 高内聚是指相关度比较高的功能要尽可能集中,不要分散。
- 低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。
3.4.1 Connector高内聚的功能
通过分析连接器的详细功能列表,我们发现连接器需要完成 3 个高内聚的功能,*
-
网络通信。
- 1 监听网络端口,等待客户端网络连接请求。
- 2 接受客户端网络连接请求,于客户端建立Socket连接,并监听客户端发送的请求。
- 3 从Socket中读取客户端请求网络字节流。
- 9 将Socket中响应字节流发送给客户端。
-
应用层协议解析。
- 4 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。
- 8 根据具体应用层协议(HTTP/AJP)将 Tomcat Response 转成网络字节流,写入Socket中。
-
Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。
- 5 将 Tomcat Request 对象转成标准的 ServletRequest。
- 6 查找并调用当前请求适配的Servlet 容器,得到 ServletResponse。
- 7 Servlet 容器处理完成后,将 ServletResponse 转成 Tomcat Response 对象。
3.5 Connector中核心组件
3个高内聚的功能并分别对应Connector中三个核心组件:EndPoint,Processor,Adapter。
3.5.1 EndPoint组件
Endpoint 翻译过来是"通信端点",主要负责网络通信,这其中就包括,监听客户端连接创建于客户端连接的Socket,并负责连接Socket 接收和发送处理器。因此Endpoint是对传输层的抽象,是用来实现 TCP/IP 协议的。
EndPoint类结构图
EndPoint用基类用抽象类AbstractEndpoint来表示,对于不同的Linux IO模型通过使用不同子类来实现。
3.5.2 Processor组件
Processor:翻译过来是"处理器",主要负责根据具体应用层协议(HTTP/AJP)读取字节流解析成 Tomcat Request 和 Response,因此Processor是对应用层的抽象,是用来实现 HTTP/AJP 协议的。
Processor类结构图
3.5.3 Adapter组件
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来“存放”这些请求信息。ProtocolHandler 接口负责解析请求并生成 Tomcat Request/Response类。但是这个 Request/Response 对象不是标准的 ServletRequest/ServletResponse,也就意味着,不能用Tomcat Request/Response 作为参数来调用容器。
Tomcat 设计者的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,负责将Tomcat Request/Response 与 ServletRequest/ServletResponse 的相互转化,实现连接器(Connector)和容器(Container)的解耦。
3.5.4 ProtocolHandler组件
ProtocolHandler组件EndPoint组件,Processor组件合并在一起表示协议处理器。用来处理tomcat支持多种IO模型和多种协议的组件。
ProtocolHandler类图
3.5 Connector处理流程
Endpoint内部Acceptor组件用于监听Socket 连接请求,当发送客户端连接到服务端Acceptor组件负责于客户端建立连接创建Socket,每当连接客户端发起请求,Endpoint会创建一个SocketProcessor对象SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。而这个线程池叫作执行器(Executor)
Processor 接收来自 Endpoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,接着会调用 Adapter 的 Service 方法。并通过 Adapter 将其提交到容器处理
连接器调用 CoyoteAdapter 的 sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 service 方法。
4 Container架构设计
4.1 Servlet容器
Container本质上是一个Servlet容器,负责servelt的加载和管理,处理请求ServletRequest,并返回标准的 ServletResponse 对象给连接器。
工作流程
当客户请求某个资源时,HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法,Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化,接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给 HTTP 服务器,HTTP 服务器会把响应发送给客户端
4.2 核心功能
按照servlet规范我们可以将多个servelt打包一个war包,而这个war包就表示一个web应用程序,war名称就是应用程序的名称。一个一个网站来说会存在多个域名,每个域名则会对应多个web应用程序。
例如有一个网购系统,有面向网站管理人员的后台管理系统,还有面向终端客户的在线购物系统。这两个系统跑在同一个 Tomcat 上,为了隔离它们的访问域名,配置了两个虚拟域名:manage.shopping.com和user.shopping.com,网站管理人员通过manage.shopping.com域名访问 Tomcat 去管理用户和商品,而用户管理和商品管理是两个单独的 Web 应用。终端客户通过user.shopping.com域名去搜索商品和下订单,搜索功能和订单管理也是两个独立的 Web 应用。
4.3 Container中核心组件
tomcat 将Container容器按功能分为4个组件,分别是 Engine、Host、Context 和 Wrapper。这 4 种容器不是平行关系,而是父子关系。
Wrapper:表示一个 Servlet
Context:表示一个 Web 应用程序,一个 Web 应用程序中可能会有多个 Servlet
Host:表示的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序
Engine:表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。
可以再通过 Tomcat 的server.xml配置文件来加深对 Tomcat 容器的理解。Tomcat 采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是 Server,其他组件按照一定的格式要求配置在这个顶层容器中。
4.4 组件类图
Container容器中定义了Container 接口用来描述Container容器中所有的组件,不同的子组件分别定义了不同子接口做描述。容器组件之间具有父子关系。
public interface Container extends Lifecycle {
public void setName(String name);
public Container getParent();
public void setParent(Container container);
public void addChild(Container child);
public void removeChild(Container child);
public Container findChild(String name);
}
4.5 请求定位 Servlet 的过程
你可能好奇,设计了这么多层次的容器,Tomcat 是怎么确定请求是由哪个 Wrapper 容器里的 Servlet 来处理的呢?答案是,Tomcat 是用 Mapper 组件来完成这个任务的。
Mapper 组件的功能就是将用户请求的 URL 定位到一个 Servlet,它的工作原理是:Mapper 组件里保存了 Web 应用的配置信息,其实就是容器组件与访问路径的映射关系,比如 Host 容器里配置的域名、Context 容器里的 Web 应用路径,以及 Wrapper 容器里 Servlet 映射的路径,你可以想象这些配置信息就是一个多层次的 Map。
当一个请求到来时,Mapper 组件通过解析请求 URL 里的域名和路径,再到自己保存的 Map 里去查找,就能定位到一个 Servlet。请你注意,一个请求 URL 最后只会定位到一个 Wrapper 容器,也就是一个 Servlet。
针对上面网购系统我可以这样的部署到tomcat,Tomcat 会创建一个 Service 组件和一个 Engine 容器组件,在 Engine 容器下创建两个 Host 子容器,在每个 Host 容器下创建两个 Context 子容器。由于一个 Web 应用通常有多个 Servlet,Tomcat 还会在每个 Context 容器里创建多个 Wrapper 子容器。每个容器都有对应的访问路径,你可以通过下面这张图来帮助你理解。
假如有用户访问一个 URL,比如图中的http://user.shopping.com:8080/order/buy,Tomcat 如何将这个 URL 定位到一个 Servlet 呢?
首先,根据协议和端口号选定 Service 和 Engine。
我们知道 Tomcat 的每个连接器都监听不同的端口,比如 Tomcat 默认的 HTTP 连接器监听 8080 端口、默认的 AJP 连接器监听 8009 端口。上面例子中的 URL 访问的是 8080 端口,因此这个请求会被 HTTP 连接器接收,而一个连接器是属于一个 Service 组件的,这样 Service 组件就确定了。我们还知道一个 Service 组件里除了有多个连接器,还有一个容器组件,具体来说就是一个 Engine 容器,因此 Service 确定了也就意味着 Engine 也确定了。
然后,根据域名选定 Host。
Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器,比如例子中的 URL 访问的域名是user.shopping.com,因此 Mapper 会找到 Host2 这个容器。
之后,根据 URL 路径找到 Context 组件。
Host 确定以后,Mapper 根据 URL 的路径来匹配相应的 Web 应用的路径,比如例子中访问的是/order,因此找到了 Context4 这个 Context 容器。
最后,根据 URL 路径找到 Wrapper(Servlet)。
Context 确定后,Mapper 再根据web.xml中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet。
看到这里,我想你应该已经了解了什么是容器,以及 Tomcat 如何通过一层一层的父子容器找到某个 Servlet 来处理请求。需要注意的是,并不是说只有 Servlet 才会去处理请求,实际上这个查找路径上的父子容器都会对请求做一些处理。我在上一期说过,连接器中的 Adapter 会调用容器的 Service 方法来执行 Servlet,最先拿到请求的是 Engine 容器,Engine 容器对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用最终的 Servlet 来处理。那么这个调用过程具体是怎么实现的呢?答案是使用 Pipeline-Valve 管道。
4.6 Tomcat中责任链模式
责任链模式
Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。
类图
每一个容器组件都有一个 Pipeline 对象,Pipeline 中维护了 Valve 链表,默认时每一个Pipeline存放了一个默认的BasicValue,
这里每一个Value表示一个处理点,当调用addValve 方法时会将添加Vaule添加到链表头部,Pipeline 中没有 invoke
方法,请求处理时Pipeline只需要获取链表中第一个Valve调用incoke去执行,执行完毕后当前Value会调用
getNext.invoke() 来触发下一个 Valve 调用
tomcat中责任链
每一个容器在执行到最后一个默认BasicValue时,会负责调用下层容器的 Pipeline 里的第一个 Valve