Web 容器是什么?
早期的 Web 应用主要用于浏览新闻等静态页面,HTTP 服务器(比如 Apache、Nginx)向浏览器返回静态 HTML,浏览器负责解析 HTML,将结果呈现给用户。
随着互联网的发展,我们已经不满足于仅仅浏览静态页面,还希望通过一些交互操作,来获取动态结果,因此也就需要一些扩展机制能够让 HTTP 服务器调用服务端程序。
于是 Sun 公司推出了 Servlet 技术。你可以把 Servlet 简单理解为运行在服务端的 Java 小程序,但是 Servlet 没有 main 方法,不能独立运行,因此必须把它部署到 Servlet 容器中,由容器来实例化并调用 Servlet。
而 Tomcat 就是一个 Servlet 容器。为了方便使用,它们也具有 HTTP 服务器的功能,因此Tomcat 就是一个“HTTP 服务器 + Servlet 容器”,我们也叫它们 Web 容器。
有个概念需要get一下。就是 HTTP 服务器和应用服务器的区别
- HTTP 服务器:负责响应来自用户端比如浏览器的请求,并向客户端返回静态资源的网页,比如图片,视频,网盘上分享的各种文件下载等。Web 服务器只处理静态的文件而不处理动态内容,仅接受和完成HTTP超文本传输协议的请求。目前流行的用来搭建Web服务可选软件有Apache,Nginx及微软的IIS等。
- 应用服务器:为客户端提供对业务逻辑的访问。这种服务器根据客户端的请求,将数据转换为动态内容。比如上面打开个人微博的例子,需要应用服务器执行程序,从数据库中找到用户的最新微博信息再把信息转换成HTML网页显示在客户面前。通常满足一个用户需求还需要数据库来支持。
HTTP协议
HTTP 协议是浏览器与服务器之间的数据传送协议。作为应用层协议,HTTP 是基于 TCP/IP 协议来传递数据的(HTML 文件、图片、查询结果等),HTTP 协议不涉及数据包(Packet)传输,主要规定了客户端和服务器之间的通信格式。
假如浏览器需要从远程 HTTP 服务器获取一个 HTML 文本,在这个过程中,浏览器实际上要做两件事情。
- 与服务器建立 Socket 连接。
- 生成请求数据并通过 Socket 发送出去。
Socket
套接字是网络连接的端点。套接字使应用程序可以从网络中读取数据,可以向网络中写入数据。不同计算机上的两个应用程序可以通过连接发送或接收字节流,以此达到相互通信的目的。
为了从一个应用程序向另一个应用程序发送消息,需要知道另一个应用程序中套接字的 IP 地址和端口号。在Java 中,套接字由 java.net.Socket 表示。
要创建一个套接字,可以使用Socket 类中众多构造函数中的一个。其中一个构造函数接收两个参数:主机名和端口号。
public Socket (java.lang.String host, int port)其中参数 host 是远程主机的名称或 IP 地址,参数 port 是连接远程应用程序的端口号。
一旦成功地创建了 Socket 类的实例,就可以使用该实例发送或接收字节流。要发送字节流,需要调用Socket 类的 getOutputStream() 方法获取一个 java.io.OutputStream 对象。要发送文本到远程应用程序,通常需要使用返回的 OutputStream 对象创建一个java.io.PrintWriter 对象。若想要从连接的另一端接收字节流,需要调用 Socket 类的 getlnputStream() 方法,该法会返回一个一个 java.io.IutputStream
从图上你可以看到,这个过程是:
- 用户通过浏览器进行了一个操作,比如输入网址并回车,或者是点击链接,接着浏览器获取了这个事件。
- 浏览器向服务端发出 TCP 连接请求。
- 服务程序接受浏览器的连接请求,并经过 TCP 三次握手建立连接。
- 浏览器将请求数据打包成一个 HTTP 协议格式的数据包。
- 浏览器将该数据包推入网络,数据包经过网络传输,最终达到端服务程序。
- 服务端程序拿到这个数据包后,同样以 HTTP 协议格式解包,获取到客户端的意图。
- 得知客户端意图后进行处理,比如提供静态文件或者调用服务端程序获得动态结果。
- 服务器将响应结果(可能是 HTML 或者图片等)按照 HTTP 协议格式打包
- 服务器将响应数据包推入网络,数据包经过网络传输最终达到到浏览器
- 浏览器拿到数据包后,以 HTTP 协议的格式解包,然后解析数据,假设这里的数据是 HTML。
- 浏览器将 HTML 文件展示在页面上。
在 HTTP 中,总是由客户端通过建立连接并发送 HTTP 请求来初始化一个事务的。Web 服务器端并不负责联系客户端或建立一个到客户端的回调连接。客户端或服务器端可提前关闭连接。例如,当使用 Web 浏览器浏览网页时,可以单击浏览器上的 Stop 按钮来停止下载文件,这样就有效地关闭了一个 Web 服务器的 HTTP 连接。
HTTP服务器请求处理
浏览器发给服务端的是一个 HTTP 格式的请求,HTTP 服务器收到这个请求后,需要调用服务端程序来处理,所谓的服务端程序就是你写的 Java 类,一般来说不同的请求需要由不同的 Java 类来处理。
图的左边表示 HTTP 服务器直接调用具体业务类,它们是紧耦合的。再看图的右边,HTTP 服务器不直接调用业务类,而是把请求交给容器来处理,容器通过 Servlet 接口调用业务类。因此 Servlet 接口和 Servlet 容器的出现,达到了 HTTP 服务器与业务类解耦的目的。 服务器不直接调用业务类,而是把请求直接交给容器来处理,容器通过Servlet接口调用业务类。因此Servlet接口和Servlet容器的出现,是用来解耦HTTP服务器和业务类。Servlet容器和Servlet接口这一套规范叫做Servlet规范。
Tomcat按照Servlet规范的要求来实现Servlet容器,同时也具有HTTP服务器的功能。作为Java程序员,如果要是现实新的业务功能,只要自定义一个Servlet类实现Servlet接口,并把它注册到tomcat容器中即可。
Servlet容器工作流程
为了解耦,HTTP服务器不直接调用Servlet,而是把请求交给Servlet容器来处理,那servlet容器又是怎么工作的呢?
当客户请求某个资源时,HTTP服务器会用一个ServletRequest对象把客户的请求信息封装起来,然后调用Servlet容器的service方法,Servlet容器拿到请求后,根据请求的URL和Servlet的映射关系,找到相应的Servlet,如果servlet还没有被加载,就用反射机制创建这个Servlet,并调用Servlet的init方法来完成初始化,接着调用servlet的service方法来处理请求,把ServletResponse对象返回给HTTP服务器,HTTP服务器会把响应发送给客户端。
Tomcat整体架构
我们已经了解了Tomcat容器要实现两大功能。
- 处理Socket连接,负责网络字节流与Request和Response对象的转换
- 加载管理Servlet,以及具体处理Request请求
因此 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事情。
- 连接器负责对外交流
- 容器负责内部处理。
从图上你可以看到,最顶层是 Server,这里的 Server 指的就是一个 Tomcat 实例。一个 Server 中有一个或者多个 Service,一个 Service 中有多个连接器和一个容器。连接器与容器之间通过标准的 ServletRequest 和 ServletResponse 通信。
连接器(connector)
Catalina 中有两个主要的模块,连接器(connector)和容器(container)。
架构介绍
Coyote是Tomcat的连接器框架的名称,是Tomcat服务器提供的的供客户端访问的外部接口。客户端通过coyote与服务器建立连接、发送请求并接受响应。
Coyote封装了底层的网络通信(Socket请求及响应处理),为 Catalina容器提供了统一的接口,使Catalina容器与具体的请求协议及IO操作方式完全解耦。Coyote将Socket输入转换封装为R equest对象,交由Catalina容器进行处理,处理请求完成后, Catalina通过coyote提供的Response对象将结果写入输出流。
Coyote作为独立的模块,只负责具体协议和IO的相关操作,与 Servlet规范实现没有直接关系,因此即便是Request和 Response对象也并未实现Servlet规范对应的接口,而是在Catalina 中将他们进一步封装为ServletRequest和ServletResponse
IO模型与协议
在coyote中,Tomcat支持多种IO模型和应用层协议
IO模型
Tomcat支持的IO模型(自8.5/9.0版本起,Tomcat移除了对BIO的支持)
| IO模型 | 描述 |
|---|---|
| NIO | 非阻塞IO,采用java NIO 类库实现 |
| NO2 | 异步IO,采用jdk 7最新的NIO2类库实现 |
| ARP | 采用Apche可移植运行库实现,是C/C++编写的本地库,如果选择该方案,需要单独安装APR库 |
Tomcat支持的应用层协议
| 应用层协议 | 描述 |
|---|---|
| HTTP/1.1 | 这是大部分Web应用采用的访问协议 |
| AJP | 用于和web服务器集成,以实现对静态资源的优化及集群部署,当前支持AJP/1.3 |
| HTTP/2 | HTTP2.0 大幅度的提升了web性能,下一代HTTP协议, |
协议层
| 应用层 | HTTP | AJP | HTTP2 |
|---|---|---|---|
| 传输层 | NIO | NIO2 | APR |
在8.0之前,tomcat默认采用的IO模型为BIO,之后改为NIO。
Tomcat 为了实现支持多种 I/O 模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作 Service 组件。这里请你注意,Service 本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat 内可能有多个 Service,这样的设计也是出于灵活性的考虑。通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。
连接器的组件
连接器应该有哪些子模块?优秀的模块化设计应该考虑高内聚、低耦合。
- 高内聚是指相关度比较高的功能要尽可能集中,不要分散。
- 低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。
通过分析连接器的详细功能列表,我们发现连接器需要完成 3 个高内聚的功能:
- 网络通信。
- 应用层协议解析。
- Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。
因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能,分别是 EndPoint、Processor 和 Adapter。
组件之间通过抽象接口交互。这样做还有一个好处是封装变化。这是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。
网络通信的 I/O 模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。
但是整体的处理逻辑是不变的,
- EndPoint 负责提供字节流给 Processor
- Processor 负责提供 Tomcat Request 对象给 Adapter
- Adapter 负责提供 ServletRequest 对象给容器
容器
Tomcat是一个由一系列可配置的组件构成的web容器,而Catalina是Tomcat的servlet容器
Catalina是Servlet容器实现,他通过松耦合的方式集成Coyote,以完成按照请求协议进行数据读写,同时,它还包含我们的启动入口,shell程序等。
Tomcat模块分层示意图
Tomcat本质就是一款Servlet容器,因此Catalina才是Tomcat的核心,其他模块都是为Catalina提供支撑的,比如通过Coyota模块提供通信,Jasper模块提供JSP引擎,Naming提供JNDI服务,Juli提供日志服务
Catalina结构
Catalina的主要组件结构如下
如上所示,Catalina负责管理Server,而Server表示整个服务器。Server下面多个服务Service,每个服务都包含着多个连接器组件和一个容器组件,在Tomcat启动的时候,会初始化一个Catalina的实例
Catalina各个组件的职责
| 组件 | 职责 |
|---|---|
| Catalina | 负责解析Tomcat的配置文件,以此来创建服务器Server组件,并根据命令来对其进行管理 |
| Server | 服务器,表示整个Catalina Servlet容器以及其他组件,负责组装并启动Servlet引擎,Tomcat连接器。Server通过实现lifecycle接口,提供了一种优雅的启动和关闭真个系统的方式 |
| Service | 服务是Server内部组件,一个Server包含多个Service。它将若干个Connector组件绑定一个Container(Engine)上 |
| Connector | 连接器,处理与客户的通信,他负责接收客户的请求,然后转给相关的容器处理,最后向客户返回响应结果 |
| Container | 容器,负责处理用户的Service请求,并返回对象给web用户的模块 |
Container 结构
Tomcat设计了4中容器,分别是Engine,host,context和Wrapper。这4种容器不是平行关系,而是父子关系,Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性。
我们也可以再通过Tomcat的server.xml配置文件来加深对Tomcat容器的理解。Tomcat采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是Server,其他组件按照一定的格式要求配置在这个顶层容器中。
那么,Tomcat是怎么管理这些容器的呢?你会发现这些容器具有父子关系,形成一个树形结构,你可能马上就想到了设计模式中的组合模式。没错,Tomcat就是用组合模式来管理这些容器的。具体实现方法是,所有容器组件都实现了container接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。这里单容器对象指的是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine。
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);
}
在上面的接口看到了 getParent、SetParent、addChild 和 removeChild 等方法。
Container 接口扩展了 LifeCycle 接口,LifeCycle 接口用来统一管理各组件的生命周期