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 中的信息不是全部要使用的,有时候我们只需要取一部分就行了,所以就可以降低编码的性能消耗。
参考:
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于