TOMCAT封神之旅(二)-Catalina组成

本系列文章来自《How Tomcat Works》,因本书中描述的tomcat版本是5以下的,所以后面章节在实操部分用了tomcat8的源码。

一个Servlet容器怎样工作

对于一个Servlet服务一个请求基本上包括三件事情:

创建一个request对象并填充那些有可能被所引用的servlet使用的信息,如参数、头部、cookies、查询字符串、URI等等。一个request对象是javax.servlet.ServletRequest或javax.servlet.http.ServletRequest接口的一个实例。

创建一个response对象,所引用的servlet使用它来给客户端发送响应。一个response对象javax.servlet.ServletResponse或javax.servlet.http.ServletResponse接口的一个实例。

调用servlet的service方法,并传入request和response对象。在这里servlet会从request对象取值,给response写值。

Catalina架构图

在这里插入图片描述

不同模块处理不同的任务,如:manager 模块处理用户的sessions,loader模块加载servlet的类。

16章的shutdown钩子,Tomcat使用它总能获得一个机会用于clean-up,而无论用户是怎样停止它的(即适当的发送一个shutdown命令或者不适当的简单关闭控制台)。

Charpter 1: A simple Web Server

在HTTP中,始终都是客户端通过建立连接和发送一个HTTP请求从而开启一个事务。web服务器不需要联系客户端或者对客户端做一个回调连接。无论是客户端或者服务器都可以提前终止连接。举例来说,当你正在使用一个web浏览器的时候,可以通过点击浏览器上的停止按钮来停止一个文件的下载进程,从而有效的关闭与web服务器的HTTP连接。

HTTP Requests

一个HTTP请求包括三个组成部分:

 方法—统一资源标识符(URI)—协议/版本

 请求的头部

 主体内容

统一资源定位器(URL)其实是一种URI(查看http://www.ietf.org/rfc/rfc2396.txt)

来的。该协议版本代表了正在使用的HTTP协议的版本。

请求的头部包含了关于客户端环境和请求的主体内容的有用信息。例如它可能包括浏览器设置的语言,主体内容的长度等等。每个头部通过一个回车换行符(CRLF)来分隔的。

对于HTTP请求格式来说,头部和主体内容之间有一个回车换行符(CRLF)是相当重要的。CRLF告诉HTTP服务器主体内容是在什么地方开始的。在一些互联网编程书籍中,CRLF还被认为是HTTP请求的第四部分。

HTTP Response

类似于HTTP请求,一个HTTP响应也包括三个组成部分:

 方法—统一资源标识符(URI)—协议/版本

 响应的头部

 主体内容

Socket Class

要创建一个客户端的套接字连接远程服务器,你可以使用Socket类众多构造方法中的一个。其中一个接收主机名称和端口号:

public Socket (java.lang.String host, int port)

在这里主机是指远程机器名称或者IP地址,端口是指远程应用的端口号。例如,要连接yahoo.com的80端口,你需要构造以下的Socket对象:

new Socket (“yahoo.com”, 80);

一旦你成功创建了一个Socket类的实例,你可以使用它来发送和接受字节流。要发送字节流,你首先必须调用Socket类的getOutputStream方法来获取一个java.io.OutputStream对象。要发送文本到一个远程应用,你经常要从返回的OutputStream对象中构造一个java.io.PrintWriter对象。要从连接的另一端接受字节流,你可以调用Socket类的getInputStream方法用来返回一个java.io.InputStream对象。

ServerSocket Class

为了让你的应用能随时待命,你需要使用java.net.ServerSocket类。这是服务器套接字的实现。

要创建一个服务器套接字,你需要使用ServerSocket类提供的四个构造方法中的一个。你需要指定IP地址和服务器套接字将要进行监听的端口号。通常,IP地址将会是127.0.0.1,也就是说,服务器套接字将会监听本地机器。服务器套接字正在监听的IP地址被称为是绑定地址。服务器套接字的另一个重要的属性是backlog,这是服务器套接字开始拒绝传入的请求之前,传入的连接请求的最大队列长度。 其中一个ServerSocket类的构造方法如下所示:

public ServerSocket(int port, int backLog, InetAddress bindingAddress);

Application

Application由三个类组成:

 HttpServer

 Request

 Response

这个应用程序的入口点(静态main方法)可以在HttpServer类里边找到。main方法创建了一个HttpServer的实例并调用了它的await方法。await方法,顾名思义就是在一个指定的端口上等待HTTP请求,处理它们并发送响应返回客户端。它一直等待直至接收到shutdown命令。 应用程序不能做什么,除了发送静态资源,例如放在一个特定目录的HTML文件和图像文件。它也在控制台上显示传入的HTTP请求的字节流。不过,它不给浏览器发送任何的头部例如日期或者cookies。

Chapter2:A Simple Servlet Container

servlet容器为servlet请求调用它的service方法。servlet容器传递一个javax.servlet.ServletRequest对象和javax.servlet.ServletResponse对象。ServletRequest对象包括客户端的HTTP请求信息,而ServletResponse对象封装servlet的响应。在servlet的生命周期中,service方法将会给调用多次.

当从服务中移除一个servlet实例的时候,servlet容器调用destroy方法。这通常发生在servlet容器正在被关闭或者servlet容器需要一些空闲内存的时候。仅仅在所有servlet线程的service方法已经退出或者超时淘汰的时候,这个方法才被调用。在servlet容器已经调用完destroy方法之后,在同一个servlet里边将不会再调用service方法。destroy方法提供了一个机会来清理任何已经被占用的资源,例如内存,文件句柄和线程,并确保任何持久化状态和servlet的内存当前状态是同步的。

Application 1

当第一次调用servlet的时候,加载该servlet类并调用servlet的init方法(仅仅一次)。  对每次请求,构造一个javax.servlet.ServletRequest实例和一个javax.servlet.ServletResponse实例。

 调用servlet的service方法,同时传递ServletRequest和ServletResponse对象。

 当servlet类被关闭的时候,调用servlet的destroy方法并卸载servlet类。

Chapter3:Connector

Tomcat为每个包都分配一个属性文件。例如,在包 org.apache.catalina.connector里边的属性文件包含了该包所有的类抛出的所有错误信息。每个属性文件都会被一个 org.apache.catalina.util.StringManager类的实例所处理。

在servlet/JSP编程中,参数名jsessionid是用来携带一个会话标识符。会话标识符经常被作为cookie来嵌入,但是程序员可以选择把它嵌入到查询字

符串去,例如,当浏览器的cookie被禁用的时候。

Obtaining Parameters

If the user requested the servlet using the GET method, all parameters are on the query string. If the POST method is used, you may find some in the request body too. All the name/value pairs are stored in a HashMap.

Because parameters can exist in the query string and or the HTTP request body, the parseParameters method checks both the query string and the request body.

the method tries to see if the HTTP request body contains parameters. This happens if the use sends the request using the POST method, the content length is greater than zero, and the content type is application/x-www-form-urlencoded.

Chapter 4: Tomcat Default Connector

Inside the invoke method, the container loads the servlet class, call its service method, manage sessions, log error messages,etc.

HTTP1.1 New Features

persistent Connection

当一个页面被请求的时候,浏览器同样需要下载页面所引用到的资源。加入页面和它所引用到的全部资源使用不同连接来 下载的话,进程将会非常慢。那就是为什么HTTP1.1引入持久连接的原因了。使用持久连接的时候,当页面下载的时候,服务器并不直接关闭连接。相反,它 等待web客户端请求页面所引用的全部资源。这种情况下,页面和所引用的资源使用同一个连接来下载。

Chunked Encoding

在HTTP1.0中,服务器可以仅仅省略content-length 头部,并保持写入连接。当写入完成的时候,它将简单的关闭连接。在这种情况下,客户端将会保持读取状态,直到获取到-1,表示已经到达文件的尾部。 HTTP1.1使用一个特别的头部transfer-encoding来表示有多少以块形式的字节流将会被发送。For every chunk, the length(in hexadecimal) followed by CR/LF is sent prior to the data. A transaction is marked with a zero length chunk.

Use of the 100(Continue) status

在发送请求内容之前,HTTP 1.1客户端可以发送Expect: 100-continue头部到服务器,并等待服务器的确认。这个一般发生在当客户端需要发送一份长的请求内容而未能确保服务器愿意接受它的时候。如果你 发送一份长的请求内容仅仅发现服务器拒绝了它,那将是一种浪费来的。 当接受到Expect: 100-continue头部的时候,假如乐意或者可以处理请求的话,服务器响应100-continue头部,后边跟着两对CRLF字符。 HTTP/1.1 100 Continue 接着,服务器应该会继续读取输入流。

Connector接口

一个Tomcat连接器必须实现org.apache.catalina.Connector接口。这个接口中的比较重要的方法包括,getContainer,setContainer, createRequest, and createResponse。HttpConnector类就是现实了该接口。而连接器与容器之间是一对一的关系。

HttpConnector类

HttpConnector类实现了Connector,Runnable,Lifecycle三个接口。Lifecycle接口用于维持每个Catalina组件的生命周期并实现它。所以,当创建一个HttpConnector实例之后,应该先调用它的initialize和start方法。并且两个方法在生命周期内只能调用一次。

创建一个服务端套接字

HttpConnector的初始化方法initialize返回一个Socket。前面的例子在每个时刻只能处理一个HTTP请求。但是在默认的连接器,HttpConnector连机器有一池子的HttpProcessor对象,所以每个HttpProcessor实例都有它自己的线程,这样HttpConnector能同步服务多个HTTP请求。很显然,池化技术是为了解决Java中一直创建对象。每个HttpProcerssor负责解析HTTP请求行和HTTP头和添加请求对象。因此,每个实例与一个请求对象和一个返回对象联系。一个HttpProcessor类的构造函数包含调用HttpConnector类的createRequest和createResponse方法。

服务HTTP请求

对于每个HTTP请求,通过调用createProcessor私有方法获取一个HttpProcessor。但是,大部分时间createProcessor并不会创建一个HttpProcessor对象。而是从pool中获取一个。如果在栈上仍然有HttpProcessor,就弹出一个HttpProcessor用于处理请求。如果为空或者没有超过最大HttpProcessor实例就创建一个。如果到达了最大实例,则createProcessor返回null。如果发生这种情况,socket就简单关闭并且不处理HTTP请求。如果createProcessor没有返回null,客户端套接字传递给HttpProcessor类的assign方法。

processor.assign(socket);

现在HttpProcessor实例的任务就是读取socket中的输入流和解析HTTP请求。

HttpProcessor类

在这一节中,我们将学会HttpProcessor怎么工作和怎么使assign方法异步以至于HttpConnector实例能在同一时刻服务很多HTTP请求。需要注意的是, run中的while循环在await方法中结束。 await方法持有处理线程的控制流,直到从HttpConnector中获取到一个新的套接字。用另外一种说法就是,直到 HttpConnector调用 HttpProcessor 实例的 assign 方法。但是, await 方法和 assign 方 法运行在不同的线程上。 assign 方法从 HttpConnector 的 run 方法中调用。我们就说这个线程是 HttpConnector 实
例的 run 方法运行的处理线程。 assign 方法是如何通知已经被调用的 await 方法的?就是通过
一个布尔变量 available 并且使用 java.lang.Object 的 wait 和 notifyAll 方法。
注意: wait 方法让当前线程等待直到另一个线程为这个对象调用 notify 或者 notifyAll 方法为止。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3cKXGm2r-1619231481739)(E:\study\tomcat\img\httpProcessor.png)]

刚开始的时候,当处理器线程刚启动的时候, available 为 false,线程在 while 循环里边
等待(见 Table 4.1 的第 1 列)。它将等待另一个线程调用 notify 或 notifyAll。这就是说,调
用 wait 方法让处理器线程暂停,直到连接器线程调用 HttpProcessor 实例的 notifyAll 方法。现在,看看第 2 列,当一个新的套接字被分配的时候, 连接器线程调用 HttpProcessor 的assign 方法。 available 的值是 false,所以 while 循环给跳过,并且套接字给赋值给HttpProcessor 实例的 socket 变量:
this.socket = socket;
连接器线程把 available 设置为 true 并调用 notifyAll。这就唤醒了处理器线程,因为available 为 true,所以程序控制跳出 while 循环:把实例的 socket 赋值给一个本地变量,并把 available 设置为 false,调用 notifyAll,返回最后需要进行处理的 socket。为什么 await 需要使用一个本地变量(socket)而不是返回实例的 socket 变量呢?因为这样一来,在当前 socket 被完全处理之前,实例的 socket 变量可以赋给下一个前来的 socket。为什么 await 方法需要调用 notifyAll 呢? 这是为了防止在 available 为 true 的时候另一
个 socket 到来。在这种情况下,连接器线程将会在 assign 方法的 while 循环中停止,直到接收到处理器线程的 notifyAll 调用。

请求对象

默认连接器的HTTP请求对象由org.apache.catalina.Request接口表示。这个接口直接由RequestBase类实现,而RequestBase是HttpRequest的父类。最终的实现是HttpRequestImpl类,它继承了HttpRequest类。

返回对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4jGwqhzb-1619231481740)(E:\study\tomcat\img\responseObject.png)]

处理请求

在这节中我们关注 HttpProcessor 类的 process 方法,它是一个套接字赋给它之后,在 HttpProcessor 类的 run 方法中调用的。 process 方法会做下面这些工作。

  1. 解析连接。
  2. 解析请求。
  3. 解析头部。

process 方法使用布尔变量 ok 来指代在处理过程中是否发现错误,并使用布尔变量
finishResponse 来指代 Response 接口中的 finishResponse 方法是否应该被调用。
boolean ok = true;
boolean finishResponse = true;
另外, process 方法也使用了布尔变量 keepAlive,stopped 和 http11。 keepAlive 表示连接
是否是持久的, stopped 表示 HttpProcessor 实例是否已经被连接器终止来确认 process 是否也
应该停止, http11 表示 从 web 客户端过来的 HTTP 请求是否支持 HTTP 1.1。

有一个 SocketInputStream 实例用来包装套接字的输入流。 有个 while 循环用来保持从输入流中读取,直到 HttpProcessor 被停止,一个异常被抛出或者连接给关闭为止。

keepAlive = true;
while (!stopped && ok && keepAlive) {
     ...
}

在 while 循环的内部, process 方法首先把 finishResponse 设置为 true,并获得输出流,并对请求和响应对象做些初始化处理。

finishResponse = true;
try {
	request.setStream(input);
	request.setResponse(response);
	output = socket.getOutputStream();
	response.setStream(output);
	response.setRequest(request);
	((HttpServletResponse) response.getResponse()).setHeader
		("Server", SERVER_INFO);
} catch (Exception e) {
	log("process.create", e);
	ok = false;
}

随后,process方法通过调用parseConnection, parseRequest和parseHeaders方法解析HTTP请求。 这些方法将在这节的小节中讨论。

if (ok) {
	parseConnection(socket);
	parseRequest(input, output);
	if (!request.getRequest().getProtocol()
		.startsWith("HTTP/0"))
		parseHeaders(input);
	if (http11) {
		// Sending a request acknowledge back to the client if
		// requested.
		ackRequest(output);
		// If the protocol is HTTP/1.1, chunking is allowed.
		if (connector.isChunkingAllowed())
			response.setAllowChunking(true);
	}

}

parseConnection 方法获得协议的值,像 HTTP0.9, HTTP1.0 或 HTTP1.1。如果协议是 HTTP1.0,
keepAlive 设置为 false,因为 HTTP1.0 不支持持久连接。如果在 HTTP 请求里边找到 Expect:
100-continue 的头部信息,则 parseHeaders 方法将把 sendAck 设置为 true。
如果协议是 HTTP1.1,并且 web 客户端发送头部 Expect: 100-continue 的话,通过调用ackRequest 方法它将响应这个头部。它将会测试组块是否是允许的 。

ackRequest 方法测试 sendAck 的值,并在 sendAck 为 true 的时候发送下面的字符串:
HTTP/1.1 100 Continue\r\n\r\n
在解析 HTTP 请求的过程中,有可能会抛出异常。任何异常将会把 ok 或者 finishResponse
设置为 false。在解析过后, process 方法把请求和响应对象传递给容器的 invoke 方法:

// Ask our Container to process this request
try {
	((HttpServletResponse) response).setHeader
		("Date", FastHttpDateFormat.getCurrentDate());
	if (ok) {
		connector.getContainer().invoke(request, response);
	}
} catch (ServletException e) {...}

接着,如果finishResponse仍然是true,响应对象的finishResponse方法和请求对象的finishRequest方法将被调用,并且结束输出。

// Finish up the handling of the request
if (finishResponse) {
	try {
		response.finishResponse();
         ...
        request.finishRequest();
         ...
        output.flush();
	} catch (IOException e) {...}

while 循环的最后一部分检查响应的 Connection 头部是否已经在 servlet 内部设为 close,
或者协议是 HTTP1.0.如果是这种情况的话, keepAlive 设置为 false。同样,请求和响应对象接
着会被回收利用。

if ( "close".equals(response.getHeader("Connection")) ) {
	keepAlive = false;
}
// End of request processing
status = Constants.PROCESSOR_IDLE;
// Recycling the request and the response objects
request.recycle();
response.recycle();

解析连接

parseConnection方法从套接字中获取到网络地址并把它赋予HttpRequestImpl对象。它也检查是否使用代理并把套接字赋予请求对象。

解析请求

parseRequest 方法是第 3 章中类似方法的完整版本。

解析头部

parseHeaders方法包含一个while循环,可以持续读取HTTP请求直到再也没有更多的头部可以读取到。 while循环首先调用请求对象的allocateHeader方法来获取一个空的 HttpHead 实例。这个实例被传递给SocketInputStream 的 readHeader方法。

while (true) {
    HttpHeader header = request.allocateHeader();
    // Read the next header
    input.readHeader(header);
}

假如所有的头部都被已经被读取的话, readHeader方法将不会赋值给HttpHeader实例,这个时候 parseHeaders 方法将会返回。

if (header.nameEnd == 0) {
	if (header.valueEnd == 0) {
		return;
	} else {
		throw new ServletException
			(sm.getString("httpProcessor.parseHeaders.colon"));
	}
}

如果存在一个头部的名称的话,这里必须同样会有一个头部的值:
String value = new String(header.value, 0, header.valueEnd);
接下去,像第3章那样, parseHeaders方法将会把头部名称和DefaultHeaders里边的名称做对比。注意的是,这样的对比是基于两个字符数组之间,而不是两个字符串之间的。

if (header.equals(DefaultHeaders.AUTHORIZATION_NAME)) {
     request.setAuthorization(value);
}  
...

Headers.colon"));
}
}


如果存在一个头部的名称的话,这里必须同样会有一个头部的值:
String value = new String(header.value, 0, header.valueEnd);
接下去,像第3章那样, parseHeaders方法将会把头部名称和DefaultHeaders里边的名称做对比。注意的是,这样的对比是基于两个字符数组之间,而不是两个字符串之间的。

```java
if (header.equals(DefaultHeaders.AUTHORIZATION_NAME)) {
     request.setAuthorization(value);
}  
...
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值