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 是个什么样的东西?
public class MessageBytesTest {
public static void main(Str ing[] args) {
MessageBytes mb = MessageBytes.newInstance();
// 等待测试的byte 对象
byte[] bytes = "abcdefg".getBytes(Charset.defaultCharset());
// 调用`setBytes`对bytes 进行标记
mb.setBytes(bytes, 2, 3);
System.out.println(mb.toString());
}
}
这个例子用来提取字节流中的子子节,并将它转换为 String
下面我们继续来阅读这个 MessageBytes 到底是何方神圣?
MessageByte 主要有四种类型:
public static final int T_NULL = 0;
/** getType() is T_STR if the the object used to create the MessageBytes
was a String */
// 表示消息为字符串
public static final int T_STR = 1;
/** getType() is T_STR if the the object used to create the MessageBytes
was a byte[] */
// 表示消息为字节数组类型
public static final int T_BYTES = 2;
/** getType() is T_STR if the the object used to create the MessageBytes
was a char[] */
// 表示消息为字符数组
public static final int T_CHARS = 3;
-
T_NULL
表示空消息,即消息为null
-
T_STR
表示消息为字符串类型 -
T_BYTES
表示消息为字节数组类型 -
T_CHARS
表示消息为字符数组类型
接着我们查看一下构造方法:
/**
* Creates a new, uninitialized MessageBytes object.
* Use static newInstance() in order to allow
* future hooks.
*/
// 使用工厂方法来创建实例
private MessageBytes() {
}
它的构造方法是私有的,我们只能通过工厂方法来获取实例
接着我们查看我们 demo 中使用的方法 setBytes
这个是一个关键方法,它负责对 bytes 打标。
/**
* Sets the content to the specified subarray of bytes.
*
* @param b the bytes
* @param off the start offset of the bytes
* @param len the length of the bytes
*/
public void setBytes(byte[] b, int off, int len) {
//private final ByteChunk byteC=new ByteChunk();
//private final CharChunk charC=new CharChunk();
byteC.setBytes( b, off, len );
type=T_BYTES;
hasStrValue=false;
hasHashCode=false;
hasIntValue=false;
hasLongValue=false;
}
它内部调用了 ByteChunk
的 setBytes
方法,同时设置了 type字段
。
我们继续向里面走!
发现内部十分简单只是对数组进行了标识。
//非常简单,就是设置一下待标识的字节数组、开始位置、结束位置。
public void setBytes(byte[] b, int off, int len) {
buff = b;
start = off;
end = start+ len;
isSet=true;
}
同时也印证了我们开头所说,打标记但是没有转码。
i
// -------------------- MessageBytes --------------------
/** Compute the string value
* 首先判断是否有缓存的字符串,有的话就直接返回,
* 这也是提高性能的一种方式。其次是根据type来选择不同的*Chunk,
* 然后调用其toString()方法。那么我们这儿选择ByteChunk.toString()来分析。
*/
@Override
public String toString() {
// 先取缓存
if( hasStrValue ) {
return strValue;
}
// 判断缓存类型
// 设置缓存
switch (type) {
case T_CHARS:
strValue=charC.toString();
hasStrValue=true;
return strValue;
case T_BYTES:
strValue=byteC.toString();
hasStrValue=true;
return strValue;
}
return null;
}
// -------------------- ByteChunk --------------------
@Override
public String toString() {
if (null == buff) {
return null;
} else if (end-start == 0) {
return "";
}
return StringCache.toString(this);
}
public String toStringInternal() {
if (charset == null) {
charset = DEFAULT_CHARSET;
}
// 如果我们只有少部分要使用
// 通过打标记+延时提取的方式
// new String(byte[], int, int, Charset) takes a defensive copy of the
// entire byte array. This is expensive if only a small subset of the
// bytes will be used. The code below is from Apache Harmony.
CharBuffer cb;
cb = charset.decode(ByteBuffer.wrap(buff, start, end-start));
// reuturn new String(buff, start, end - start, charset);
return new String(cb.array(), cb.arrayOffset(), cb.length());
}
需要关注的主要是这三个方法
MB 调用 toString 方法的时候首先会从当前实例中取出缓存,如果没有缓存就调用 ByteChunk 的 toString 方法,设置缓存并且返回。
ByteChunk 的 toString 方法是使用 StringCache 的 toString 方法 但是其中的主要调用仍然是 StringCache.toStringInternal()
我们来讲解一下这个方法吧!
他使用的是 NIO 的 ByteBuffer 根据 偏移量
和 待提取长度
进行 编码提取转换
。
需要注意的是该注释已经给出了使用 java.nio.charset.CharSet.decode()
代替直接使用 new String(byte[], int, int, Charset)
的原因。
如果是用默认的 new String(byte[], int, int, Charset)
会对整个 byte 进行拷贝,对于一个巨大的 byte[] 中我们只需要提取一些些数据,就会带来严重的性能损耗。
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 中的信息不是全部要使用的,有时候我们只需要取一部分就行了,所以就可以降低编码的性能消耗。
参考:
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于