深入拆解Tomcat&Jetty(一)
专栏地址:https://time.geekbang.org/column/intro/100027701
1、Web容器是什么
早期的 Web 应用主要用于浏览新闻等静态页面,HTTP 服务器(比如 Apache、Nginx)向浏览器返回静态 HTML,浏览器负责解析 HTML,将结果呈现给用户。
随着互联网的发展,我们已经不满足于仅仅浏览静态页面,还希望通过一些交互操作,来获取动态结果,因此也就需要一些扩展机制能够让 HTTP 服务器调用服务端程序
。
于是 Sun 公司推出了 Servlet 技术。你可以把 Servlet 简单理解为运行在服务端的 Java 小程序,但是 Servlet 没有 main 方法,不能独立运行,因此必须把它部署到 Servlet 容器中,由容器来实例化并调用 Servlet。
而 Tomcat 和 Jetty 就是一个 Servlet 容器。为了方便使用,它们也具有 HTTP 服务器的功能,因此Tomcat 或者 Jetty 就是一个“HTTP 服务器 + Servlet 容器”,我们也叫它们 Web 容器。
其他应用服务器比如 JBoss 和 WebLogic,它们不仅仅有 Servlet 容器的功能,也包含 EJB 容器,是完整的 Java EE 应用服务器。从这个角度看,Tomcat 和 Jetty 算是一个轻量级的应用服务器。
2、Http协议
2.1、Http工作原理
HTTP 协议是浏览器与服务器之间的数据传送协议。作为应用层协议,HTTP 是基于 TCP/IP 协议来传递数据的(HTML 文件、图片、查询结果等),HTTP 协议不涉及数据包(Packet)传输,主要规定了客户端和服务器之间的通信格式。
假如浏览器需要从远程 HTTP 服务器获取一个 HTML 文本,在这个过程中,浏览器实际上要做两件事情。
- 与服务器建立 Socket 连接。
- 生成请求数据并通过 Socket 发送出去。
请求数据包含的内容:发送方意图(请求数据还是提交数据)+ 发送内容(我想请求的内容||我想提交的内容)
HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。约定了请求数据的存储格式。
Tomcat 和 Jetty 作为 HTTP 服务器,在这个过程中主要是处理 接受连接、解析请求数据、处理请求和发送响应这几个步骤。
HTTP 请求数据由三部分组成,分别是请求行、请求报头、请求正文。当这个 HTTP 请求数据到达 Tomcat 后,Tomcat 会把 HTTP 请求数据字节流解析成一个 Request 对象,这个 Request 对象封装了 HTTP 所有的请求信息。接着 Tomcat 把这个 Request 对象交给 Web 应用去处理,处理完后得到一个 Response 对象,Tomcat 会把这个 Response 对象转成 HTTP 格式的响应数据并发送给浏览器。
HTTP 的响应也是由三部分组成,分别是状态行、响应报头、报文主体。
HTTP 是通信的方式,HTML 才是通信的目的,就好比 HTTP 是信封,信封里面的信(HTML)才是内容;但是没有信封,信也没办法寄出去。
2.2、Cookie和Session
Cookie 是 HTTP 报文的一个请求头,Web 应用可以将用户的标识信息或者其他一些信息(用户名、角色等)存储在 Cookie 中。用户经过验证之后,每次 HTTP 请求报文中都包含 Cookie,这样服务器读取这个 Cookie 请求头就知道用户是谁了。Cookie 本质上就是一份存储在用户本地的文件,里面包含了每次请求中都需要传递的信息。
由于 Cookie 以明文的方式存储在本地,而 Cookie 中往往带有用户信息,这样就造成了非常大的安全隐患。而 Session 的出现解决了这个问题,Session 可以理解为***服务器***端开辟的存储空间,里面保存了用户的状态,用户信息以 Session 的形式存储在服务端。当用户请求到来时,服务端可以把用户的请求和用户的 Session 对应起来。那么 Session 是怎么和请求对应起来的呢?答案是通过 Cookie,浏览器在 Cookie 中填充了一个 Session ID 之类的字段用来标识请求。
具体工作过程是这样的:服务器在创建 Session 的同时,会为该 Session 生成唯一的 Session ID,通过 set-cookie 放到响应头里,然后浏览器写到 Cookie 里,当浏览器再次发送请求的时候,会将这个 Session ID 带上,服务器接受到请求之后就会依据 Session ID 找到相应的 Session,找到 Session 后,就可以在 Session 中获取或者添加内容了。而这些内容只会保存在服务器中,发到客户端的只有 Session ID,这样相对安全,也节省了网络流量,因为不需要在 Cookie 中存储大量用户信息。
在 Java 中,Session 是 Web 应用程序在调用 HttpServletRequest 的 getSession 方法时,由 Web 容器(比如 Tomcat)创建的。
2.3、如何理解Http的无状态
Http的无状态是指不同请求间协议内容无相关性,即本次请求与上次请求没有内容的依赖关系,本次响应也只针对本次请求的数据,至于服务器应用程序为用户保存的状态(Cookie、Session)是属于应用层,与协议是无关的。
https://developer.aliyun.com/article/846786
在 HTTP/1.0 时期,每次 HTTP 请求都会创建一个新的 TCP 连接,请求完成后之后这个 TCP 连接就会被关闭。这种通信模式的效率不高。
在 HTTP/1.1 中,引入了 HTTP 长连接的概念,使用长连接的 HTTP 协议,会在响应头加入 Connection:keep-alive
。这样当浏览器完成一次请求后,浏览器和服务器之间的 TCP 连接不会关闭,再次访问这个服务器上的网页时,浏览器会继续使用这一条已经建立的连接,也就是说两个请求可能共用一个 TCP 连接。
Connection:keep-alive
只是建立TCP层的状态,省去了下一次的TCP三次握手,而HTTP本身还是继续保持无状态。
应用程序其实一般不关心这次HTTP请求的TCP传输细节,只关心HTTP协议的内容,因此只要复用TCP连接时做好必要的数据重置,是不算有状态的。不过HTTP/1.1中的长连接没有解决 head of line blocking
(线头阻塞),请求是按顺序排队处理的,前面的HTTP请求处理会阻塞后面的HTTP请求,虽然HTTP pipelining对连接请求做了改善,但是复杂度太大,并没有普及,以至于后面的连接必须等待前面的返回了才能够发送。这个问题直到HTTP/2.0采取二进制分帧编码
方式才彻底解决。
- HTTP 1.0
买一个信封只能传送一个来回的信。 - HTTP 1.1 keep–alive
买一个信封可重复使用,但前提是得等到服务端把这个信封里的信处理完,并送回来!
REST的无状态
REST是一种架构风格:将网络上的信息实体看作是资源,可以是图片、文件、一个服务…资源用URI统一标识,URI中没有动词,这是因为它是资源的标识,那怎么操作这些资源呢,于是定义一些动作:GET、POST、PUT和DELETE。通过URI+动作来操作一个资源。所谓的无状态说的是,为了完成一个操作,请求里包含了所有信息
,你可以理解为服务端不需要保存请求的状态,也就是不需要保存 Session ,没有 Session 的好处是带来了服务端良好的可伸缩性,方便 failover,请求被 LB 转到不同的 server 实例上没有差别。从这个角度看,正是有了REST架构风格的指导,才有了HTTP的无状态特性,顺便提一下,REST 和 HTTP1.1 出自同一人之手。但是理想是丰满的,现实是骨感的,为了方便开发,大多数复杂的 Web 应用不得不在服务端保存 Session。为了尽量减少 Session 带来的弊端,往往将 Session 集中存储到 Redis 上,而不是直接存储在 server 实例上。
3、Servlet容器
3.1、为什么会出现Servlet容器
浏览器发给服务端的是一个 HTTP 格式的请求,HTTP 服务器收到这个请求后,需要调用服务端程序来处理,所谓的服务端程序就是你写的 Java 类,一般来说不同的请求需要由不同的 Java 类来处理。
HTTP服务器该怎么知道去寻找哪个类的哪个方法处理呢?为了避免HTTP服务器和业务类的耦合问题,一伙人就定义了一个接口,各种业务类都必须实现这个接口,这个接口就叫 Servlet 接口,有时我们也把实现了 Servlet 接口的业务类叫作 Servlet。对于特定的请求,HTTP 服务器如何知道由哪个 Servlet 来处理呢?Servlet 又是由谁来实例化呢?显然 HTTP 服务器不适合做这个工作,否则又和业务类耦合了。于是,还是那伙人又发明了 Servlet 容器,Servlet 容器用来加载和管理业务类。HTTP 服务器不直接跟业务类打交道,而是把请求交给 Servlet 容器去处理,Servlet 容器会将请求转发到具体的 Servlet
,如果这个 Servlet 还没创建,就加载并实例化这个 Servlet,然后调用这个 Servlet 的接口方法。因此 Servlet 接口其实是 Servlet 容器跟具体业务类之间的接口。
作为 Java 程序员,如果我们要实现新的业务功能,只需要实现一个 Servlet,并把它注册到 Tomcat(Servlet 容器)中,剩下的事情就由 Tomcat 帮我们处理了。
3.2、Servlet接口
Servlet接口定义了下面五个方法:
public interface Servlet {
void init(ServletConfig config) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest req, ServletResponse res)throws ServletException, IOException;
String getServletInfo();
void destroy();
}
其中最重要是的 service 方法,具体业务类在这个方法里实现处理逻辑。这个方法有两个参数:ServletRequest 和 ServletResponse。ServletRequest 用来封装请求信息,ServletResponse 用来封装响应信息,因此本质上这两个类是对通信协议的封装。(HTTP1.0、HTTP1.1、HTTP2.0…)
当客户请求某个资源时,HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法,Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化,接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给 HTTP 服务器,HTTP 服务器会把响应发送给客户端。
Servlet 容器会实例化和调用 Servlet,那 Servlet 是怎么注册到 Servlet 容器中的呢?一般来说,我们是以 Web 应用程序的方式来部署 Servlet 的,而根据 Servlet 规范,Web 应用程序有一定的目录结构,在这个目录下分别放置了 Servlet 的类文件、配置文件以及静态资源,Servlet 容器通过读取配置文件,就能找到并加载 Servlet。
| - MyWebApp
| - WEB-INF/web.xml -- 配置文件,用来配置 Servlet 等
| - WEB-INF/lib/ -- 存放 Web 应用所需各种 JAR 包
| - WEB-INF/classes/ -- 存放你的应用类,比如 Servlet 类
| - META-INF/ -- 目录存放工程的一些信息
3.3、拓展机制
引入了 Servlet 规范后,开发者不需要关心 Socket 网络通信、不需要关心 HTTP 协议,也不需要关心业务类是如何被实例化和调用的,因为这些都被 Servlet 规范标准化了,只要关心怎么实现业务逻辑。但是如果这个规范不能满足你的业务的个性化需求,就有问题了,因此设计一个规范或者一个中间件,要充分考虑到可扩展性。Servlet 规范提供了两种扩展机制:Filter 和 Listener。
Filter是过滤器,这个接口允许你对请求和响应做一些统一的定制化处理,比如你可以根据请求的频率来限制访问,或者根据国家地区的不同来修改响应内容。过滤器的工作原理是这样的:Web 应用部署完成后,Servlet 容器需要实例化 Filter 并把 Filter 链接成一个 FilterChain。当请求进来时,获取第一个 Filter 并调用 doFilter 方法,doFilter 方法负责调用这个 FilterChain 中的下一个 Filter。
Listener是监听器,这是另一种扩展机制。当 Web 应用在 Servlet 容器中运行时,Servlet 容器内部会不断的发生各种事件,如 Web 应用的启动和停止、用户请求到达等。 Servlet 容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet 容器会负责调用监听器的方法。当然,你可以定义自己的监听器去监听你感兴趣的事件,将监听器配置在 web.xml 中。比如 Spring 就实现了自己的监听器,来监听 ServletContext 的启动事件,目的是当 Servlet 容器启动时,创建并初始化全局的 Spring 容器。
- Filter 是干预过程的,它是过程的一部分,是基于过程行为的。
- Listener 是基于状态的,任何行为改变同一个状态,触发的事件是一致的。