这本书(How Tomcat Works 中文下载地址)之前就看过,然而也就开了个头就废弃了,所以一直耿耿于怀。这次决定重新开始,在此记录一些学习心得和本书的主要知识点。
所有代码也将托管在GitHub上面。O(∩_∩)O
上节回顾
剖析了Tomcat的HttpConnector
类。
工作原理梗概如下:
- 用工厂模式创建出ServerSocket。
- 创建特定数量的HttpProcessor对象池,用于处理Socket请求;
- 同时,会启动HttpProcessor线程。由于没有请求,所以所有的HttpProcessor都会阻塞在那里。
- 调用HttpConnector的run方法,等待Socket请求。
- 当请求来临,从对象池中pop出一个HttpProcessor实例。
- 调用HttpProcessor的
assign
方法,将Socket实例传递给HttpProcessor; - 并且,唤醒在HttpProcessor中被阻塞的线程,用于处理Socket请求。
概要
这里会主要关注HttpProcessor
异步处理请求的方法。
HttpProcessor类
在前几个章节中,我们自己写的处理请求的代码是同步的。他必须等待处理完process
方法,才能接收新的Socket。
public void run() {
...
while (!stopped) {
Socket socket = null;
try {
socket = serversocket.accept();
}
catch (Exception e) {
continue;
}
// Hand this socket off to an Httpprocessor
HttpProcessor processor = new Httpprocessor(this);
processor.process(socket);
}
}
而Tomcat中,这是异步的。
run方法
它的逻辑是,接收Socket,然后处理请求,最后将HttpConnector实例放回到对象池中。
public void run() {
// Process requests until we receive a shutdown signal
while (!stopped) {
// Wait for the next socket to be assigned
Socket socket = await();
if (socket == null)
continue;
// Process the request from this socket
try {
process(socket);
} catch (Throwable t) {
log("process.invoke", t);
}
// Finish up this request
connector.recycle(this);
}
// Tell threadStop() we have shut ourselves down successfully
synchronized (threadSync) {
threadSync.notifyAll();
}
}
while方法做循环,很常见。
在上一章节中我们已经说过,HttpConnector会创建HttpProcessor,并且启动线程的run方法,同时它被放在对象池中,一直阻塞着。
那么run方法为何会一直阻塞着呢?很简单,因为在while循环中的await
方法。
await方法
执行的流程如下:
- while循环中
available
条件的初始状态是false,表示没有请求。所以它就一直处于wait
等待状态。 - 一旦有请求(在assign方法中,会将available设置为true,并且释放线程锁。await得以调用。),await将会获取线程锁,代码跳出循环得以向下执行。
- 然后,它将
available
条件设置为false,通过notifyAll
释放线程锁,返回Socket实例。 - 这样,当前Scoket的await方法就执行完成,返回run方法中。
- 在run方法中,继续其他操作。
private synchronized Socket await() {
// Wait for the Connector to provide a new Socket
while (!available) {
try {
wait();
} catch (InterruptedException e) {
}
}
// Notify the Connector that we have received this Socket
Socket socket = this.socket;
available = false;
notifyAll();
if ((debug >= 1) && (socket != null))
log(" The incoming request has been awaited");
return (socket);
}
一个问题
这里有个问题值得注意一下,为何需要调用notifyAll方法,作用是什么,是否多此一举呢?
书上是这么解释的:
为什么 await 需要使用一个本地变量(socket)而不是返回实例的 socket 变量呢?因为这样一来,在当前 socket 被完全处理之前,实例的 socket 变量可以赋给下一个前来的 socket。
为什么 await 方法需要调用 notifyAll 呢? 这是为了防止在 available 为 true 的时候另一个 socket 到来。在这种情况下,连接器线程将会在 assign 方法的 while 循环中停止,直到接收到处理器线程的 notifyAll 调用。
意思大概就是,一个HttpProcessor实例在同一时间只能处理一个Socket的请求,但是设置一个本地socket变量,可以让HttpProcessor提前存储下一个Socket请求。
可是将Socket传递给HttpProcessor的时候,HttpProcessor是需要从对象池中获取而来的。在处理当前请求的HttpProcessor明显不在对象池中,它是如何能够提前存储下一个Socket请求呢?
这个问题我还未找到答案。待以后处理。
assign方法
刚刚我们说了,await方法会阻塞。直到assign方法被调用,释放了线程锁,await才得以继续执行。
assign
方法的作用就是,
- 接收由HttpConnector传递的Socket实例。
- 释放线程锁,让
await
方法得以调用。
available
属性是false,我们得知。
synchronized void assign(Socket socket) {
// Wait for the Processor to get the previous Socket
while (available) {
try {
wait();
} catch (InterruptedException e) {
}
}
// Store the newly available Socket and notify our thread
this.socket = socket;
available = true;
notifyAll();
if ((debug >= 1) && (socket != null))
log(" An incoming request is being assigned");
}
处理请求
pocess
方法,会做:
- 解析连接
- 解析请求
- 解析头部
解析数据是在一个white循环中进行的。
循环条件中keepAlive
是由Http请求控制的。
keepAlive = true;
while (!stopped && ok && keepAlive) {
...
if ( "close".equals(response.getHeader("Connection")) ) {
keepAlive = false;
}
...
}
里面的处理数据代码主要是:
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
方法从套接字中获取到网络地址并把它赋予 HttpRequestImpl 对象。
private void parseConnection(Socket socket) throws IOException, ServletException {
if (debug >= 2)
log(" parseConnection: address=" + socket.getInetAddress() +
", port=" + connector.getPort());
((HttpRequestImpl) request).setInet(socket.getInetAddress());
if (proxyPort != 0)
request.setServerPort(proxyPort);
else
request.setServerPort(serverPort);
request.setSocket(socket);
}
解析请求
parseRequest
方法和我们之前章节实现的差不多。
解析头部
parseHeaders
方法使用包org.apache.catalina.connector.http
里边的 HttpHeader 和 DefaultHeaders 类。类 HttpHeader 指代一个 HTTP 请求头部。内容也类似,略过。
简单的容器
容器的作用,
- 用来动态load出Servlet,
- 并且将Request和Response传递给它,执行它的service方法。
它需要实现org.apache.catalina.Container
接口。
现在,我们自己实现一个容器,然后将这个容器传递给org.apache.catalina.connector.http.HttpConnector
。
代码和之前的很类似,只是从将一些功能从ServeletProcessor
中分离了出来。
public class SimpleContainer implements Container {
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
...
public void invoke(Request request, Response response)
throws IOException, ServletException {
String servletName = ( (HttpServletRequest) request).getRequestURI();
servletName = servletName.substring(servletName.lastIndexOf("/") + 1);
URLClassLoader loader = null;
try {
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(WEB_ROOT);
String repository = (new URL("file", null, classPath.getCanonicalPath()
+ File.separator)).toString() ;
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
}
catch (IOException e) {
System.out.println(e.toString() );
}
Class myClass = null;
try {
myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
System.out.println(e.toString());
}
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((HttpServletRequest) request, (HttpServletResponse) response);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
}
}
...
}
启动类
package com.ext04.pyrmont.startup;
import com.ext04.pyrmont.core.SimpleContainer;
import org.apache.catalina.connector.http.HttpConnector;
/**
* Created by laiwenqiang on 2017/5/25.
*/
public class BootStrap {
public static void main(String[] args) {
HttpConnector connector = new HttpConnector();
SimpleContainer container = new SimpleContainer();
connector.setContainer(container);
try {
connector.initialize();
connector.start();
// 加上这句话,防止main方法退出。阻塞在那。
System.in.read();
} catch (Exception e) {
e.printStackTrace();
}
}
}
打开浏览器,输入http://localhost:8080/servlet/PrimitiveServlet
,得出结果。
一步一个脚印
本章完成,(^__^)