Tomcat 源码解析 (1)-HTTPRequestResponse

本贴最后更新于 1923 天前,其中的信息可能已经时移俗易

HTTP

Web 服务器也称为超文本传输协议服务器,因为他使用 HTTP 与其客户端进行通讯。

HTTP 允许 Web 服务器和浏览器通过 Internet 发送请求,他是一种基于“请求-响应”的协议。客户端请求一个文件,服务端对于该请求进行响应。

HTTP 请求

一个 HTTP 请求包括三部分:

  • 请求方法---统一资源标识符 URI 协议/版本

  • 请求头

  • 实体

POST /ajax/ShowCaptcha HTTP/1.1\r\n  
Content-Type: application/x-www-form-urlencoded\r\n  
Host: www.renren.com\r\n  
Content-Length: 36\r\n  
\r\n  
email=%E5%B7%A5&password=asdasdsadas

请求方法 -- URI -- 协议/版本

POST /ajax/ShowCaptcha HTTP/1.1\r\n

会出现在第一行

每个请求头之前都会用回车/换行符隔开 (CRLF)

并且请求头和请求实体之间会有一个空行,空行只有 CRLF 符号。CRLF 告诉 HTTP 服务器请求的正文从哪里开始。

HTTP 响应

与 HTTP 请求相似,HTTP 响应也分三部分:

  • 协议-- 状态码

  • 响应头

  • 响应实体段

HTTP/1.1 200 OK\r\n  
Date: Sat, 31 Dec 2005 23:59:59 GMT\r\n  
Content-Type: text/html;charset=ISO-8859-1\r\n  
Content-Length: 122\r\n  
\r\n  
<html>  
​  
<head>  
<title>Wrox Homepage</title>  
</head>  
​  
<body>  
<!-- body goes here -->  
</body>  
</html>

HTTP/1.1 200 OK Date: Sat, 31 Dec 2005 23:59:59 GMT Content-Type: text/html;charset=ISO-8859-1 Content-Length: 122

Wrox Homepage

第一行类似使用的协议以及状态码(200 表示请求成功

StandardServer

此类是 Server 标准实现类,Server 仅此一个实现类。是 Tomcat 顶级容器。Server 是 Tomcat 中最顶层的组件,它可以包含多个 Service 组件。这一节主要给大家讲解 Tomcat 是如何关闭的。之后的章节会给大家带来 addService() 和 findService(String) 方法的解析。

这个 StandardServer 继承了 Server

并且实现了其中比较关键的一个方法:

 /**  
 * Wait until a proper shutdown command is received, then return.  
 */  
 public void await();

z

try {  
 InputStream stream;  
 try {  
 socket = serverSocket.accept();  
 socket.setSoTimeout(10 * 1000);  // Ten seconds  
 stream = socket.getInputStream();  
 } catch (AccessControlException ace) {  
 log.warn("StandardServer.accept security exception: "  
 + ace.getMessage(), ace);  
 continue;  
 } catch (IOException e) {  
 if (stopAwait) {  
 // Wait was aborted with socket.close()  
 break;  
 }  
 log.error("StandardServer.await: accept: ", e);  
 break;  
 }  
 while (expected > 0) {  
 int ch = -1;  
 try {  
 ch = stream.read();  
 } catch (IOException e) {  
 log.warn("StandardServer.await: read: ", e);  
 ch = -1;  
 }  
 if (ch < 32)  // Control character or EOF terminates loop  
 break;  
 command.append((char) ch);  
 expected--;  
 }finally {  
 // Close the socket now that we are done with it  
 try {  
 if (socket != null) {  
 socket.close();  
 }  
 } catch (IOException e) {  
 // Ignore  
 }  
 }  
 // Match against our command string  
 boolean match = command.toString().equals(shutdown);  
 if (match) {  
 log.info(sm.getString("standardServer.shutdownViaPort"));  
 break;  
 } else  
 log.warn("StandardServer.await: Invalid command '"  
 + command.toString() + "' received");  
 

根据源码上的注释 我们可以大致了解,在启动 Tomcat 的时候,会开启一个 8005 的端口,这个服务负责监听到来的 telnet 连接,当受到 为 SHUTDOWN 的命令时候,销毁 Tomcat 的所有服务并且关闭 Tomcat。

Request & Response

在阅读 Tomcat Request 源码的时候,我发现了一个比较有趣的东西:

private MessageBytes schemeMB = MessageBytes.newInstance();  
private MessageBytes methodMB = MessageBytes.newInstance();  
private MessageBytes unparsedURIMB = MessageBytes.newInstance();  
private MessageBytes uriMB = MessageBytes.newInstance();  
private MessageBytes decodedUriMB = MessageBytes.newInstance();  
private MessageBytes queryMB = MessageBytes.newInstance();  
private MessageBytes protoMB = MessageBytes.newInstance();  
// remote address/host  
private MessageBytes remoteAddrMB = MessageBytes.newInstance();  
private MessageBytes localNameMB = MessageBytes.newInstance();  
private MessageBytes remoteHostMB = MessageBytes.newInstance();  
private MessageBytes localAddrMB = MessageBytes.newInstance();

他的大多数成员变量都是 MessageBytes 的实例,这让我产生了兴趣,这个 MessageBytes 到底是什么东西?

后来通过查阅资料发现 Tomcat 为了提升性能,用了一些很有趣的 Tricks

Tomcat 对于读取来的字节流不会立马解析,而是将它进行打标 + 延时提取的方式来实现 按需使用。

下面我来跑一个小 demo 来了解一下 MessageBytes 是个什么样的东西?

Request 是如何被解析的

他是如何判断打标的位置的?

下面为以给请求行中的 URI 打标为大家解释

我们要探寻的是:

/**  
 * Implementation of InputBuffer which provides HTTP request header parsing as  
 * well as transfer decoding.  
 *  
 * @author <a href="mailto:remm@apache.org">Remy Maucherat</a>  
 * @author Filip Hanik  
 */  
public class InternalNioInputBuffer extends AbstractInputBuffer<NioChannel> {  
 @Override  
 public boolean parseRequestLine(boolean useAvailableDataOnly)  
 throws IOException {  
 //-----省略前面的解析步骤  
 if (parsingRequestLinePhase == 4) {  
 // Mark the current buffer position  
   
 int end = 0;  
 //  
 // Reading the URI  
 //  
 boolean space = false;  
 while (!space) {  
 // Read new bytes if needed  
 if (pos >= lastValid) {  
 if (!fill(true, false)) //request line parsing  
 return false;  
 }  
 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {  
 space = true;  
 end = pos;  
 } else if ((buf[pos] == Constants.CR)  
 || (buf[pos] == Constants.LF)) {  
 // HTTP/0.9 style request  
 parsingRequestLineEol = true;  
 space = true;  
 end = pos;  
 } else if ((buf[pos] == Constants.QUESTION)  
 && (parsingRequestLineQPos == -1)) {  
 parsingRequestLineQPos = pos;  
 }  
 pos++;  
 }  
 request.unparsedURI().setBytes(buf, parsingRequestLineStart, end - parsingRequestLineStart);  
 if (parsingRequestLineQPos >= 0) {  
 request.queryString().setBytes(buf, parsingRequestLineQPos + 1,   
 end - parsingRequestLineQPos - 1);  
 request.requestURI().setBytes(buf, parsingRequestLineStart, parsingRequestLineQPos - parsingRequestLineStart);  
 } else {  
 // URL 当解析的时候之前个请求方法执行完之后会找到对应的空格  
 // 请求行的开始就就是parseRequestLineStart 开始位置  
 //  之后向下寻找空格 并将他标记为end  
 // setBytes 的时候只要把开始的位置和长度设置进去就行了  
 request.requestURI().setBytes(buf, parsingRequestLineStart, end - parsingRequestLineStart);  
 }  
 System.out.println("解析出来的URI为: " +request.requestURI().toString());  
 parsingRequestLinePhase = 5;  
 }  
 }  
}

这里主要要了解的是几个变量

  • buf 整条请求头的 byte[]

  • parsingRequestLineStart URI 开始位置

  • end URI 结束位置

上面代码的大致意识是 将 parsingRequestLineStart 的位置设置为上次解析(解析请求方法)的位置 +1

然后通过遍历 buf 寻找从 parsingRequestLineStart 开始的第一个空格。

并且为了避免多余的编码,tomcat 将 空格 CR LF 也转换为字节,只要比较字节就能判断是否相同,期间没有任何编码。

/**  
* CR.  
*/  
public static final byte CR = (byte) '\r';  
/**  
* LF.  
*/  
public static final byte LF = (byte) '\n';  
/**  
* SP.  
*/  
public static final byte SP = (byte) ' ';

将这些字节流通过 setBytes 打标,记住是 offset/offset+ 长度。

总结

还是开头那句话:

Tomcat 采用延时编码的方式来提升性能,解析完一个 Request 后,如果没有被利用,变量存储的只是这个字节流的打标,只有在使用的时候才会去编码或者去取缓存。这样有个好处,就是 Request 中的信息不是全部要使用的,有时候我们只需要取一部分就行了,所以就可以降低编码的性能消耗。

参考:

深入理解 Tomcat(12)拾遗-MessageBytes

消息字节——MessageBytes

  • Tomcat

    Tomcat 最早是由 Sun Microsystems 开发的一个 Servlet 容器,在 1999 年被捐献给 ASF(Apache Software Foundation),隶属于 Jakarta 项目,现在已经独立为一个顶级项目。Tomcat 主要实现了 JavaEE 中的 Servlet、JSP 规范,同时也提供 HTTP 服务,是市场上非常流行的 Java Web 容器。

    162 引用 • 529 回帖 • 1 关注
  • 代码
    467 引用 • 586 回帖 • 9 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...