最近在研究AHACHE对HTTP1.1的实现,写了下面一些心得给大家分享,也给自己备个份。
大家没事的时候看看,希望对大家有帮助,如果发现有写错了的地方,一定告诉我,谢谢!
HTTP 1.1的三个新特性及APACHE的实现
HTTP1.1比HTTP1.0多了三个非常重要的特性:
1.支持长连接(connection: keep-alive)
HTTP1.1之前,Server处理完浏览器的请求后会立即断开连接。每次浏览器申请一个资源的时候都需要建立一个连接,而一个页面往往包含了很多资源,图片、CSS文件、脚本文件,Flash文件等,所以每次用户打开一个页面需要浏览器和Server建立很多连接,进而耗费很多资源,速度慢。HTTP1.1引入了长连接的机制,在长连接模式下浏览器下载一个页面时,服务器段并不断开链接直到页面的所有资源都下载完毕。这种机制的引入减少很多用户浏览网页时建立连接和关闭连接的损耗,增加了互联网的浏览效率。
浏览器请求没有发送完毕前,所有HTTP请求的头部都带有这个标识:
connection: keep-alive
这个时候Server应该一直接收这个流,不断处理流中的HTTP请求,不能断开连接。
直到最后一个请求,浏览器发送的HTTP请求的头部带有这个标识:
connection: close
Server读取完本请求后,不再读取这个连接中请求流中的数据,断开连接。
2.分块模式传输数据(transfer-encoding:chunked )
在长链接上,服务器可能会给浏览器发送一个包含多个资源的字节流应答,浏览器也有可能在请求流中携带多个资源。请求和应答时,必须在包头中说明本包的字节数,这样接收者能够根据这个值截断流读取有效数据。而实际情况是,服务器和浏览器在组包的时候并不能确切得知道本包的字节数。比如,Servlet在应答请求的时候,并不必须等到所有事务处理都完成,而是可以先返回准备好的几个字节,等其他事务处理完了再返回这些迟到的事务产生的数据。这种情况需要让接收者知道如何截取数据,如果包头没有包的字节数数据的话。在HTTP1.0时代,服务器端只好把 content-length 字段留空,然后继续返回字节流。数据发送完成后,简单关闭连接,在结尾处加一个-1表示文件流结束了。HTTP1.1使用了一个特殊的头字段 transfer-encoding:chunked 来标识本次流传输是分块进行的,每一块的长度以16进制的形式写在块头,回车符之前。每次以分块模式传输的数据,最后一块的的字节数为0。下面是一个例子,该流总长度为38字节,分两块发送,第一块长度是29字节,第二块长度是9字节:
"I'm as helpless as a kitten up a tree." ,发送的流是下面这个样子:
1D\r\n I'm as helpless as a kitten u 9\r\n p a tree. 0\r\n
3.使用100-continue请求服务器能力反馈
HTTP1.1允许客户端在发送请求包的报文体之前发送报文头使用100-continue来等待服务器的反馈,确认是否允许发送该请求报文的报文体。这种情况通常发生在客户端准备发送一个冗长的请求给服务器,但是不确认服务器是否有能力接收。如果没有得到确认,而将一个冗长的请求包发送给服务器,然后包被服务器给抛弃了,这种情况挺浪费资源的。一旦服务器收到100-continue的请求头,如果此时能够处理该请求,立即给客户端返回 HTTP/1.1 100 Continue 加两个回车符,这时服务器端可以继续读取该请求流了。
APACHE的实现:
代码区
package org.apache.catalina.connector.http;
final class HttpProcessor
.......
private boolean stopped = false;
private void process(Socket socket) {
boolean ok = true;
......
keepAlive = true;
while (!stopped && ok && keepAlive) {
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;
}
// Parse the incoming request
try {
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);
}
}
} catch (EOFException e) {
// It's very likely to be a socket disconnect on either the
// client or the server
ok = false;
finishResponse = false;
}
.......
// Ask our Container to process this request
try {
((HttpServletResponse) response).setHeader
("Date", FastHttpDateFormat.getCurrentDate());
if (ok) {
connector.getContainer().invoke(request, response);
}
} catch (ServletException e) {
log("process.invoke", e);
try {
((HttpServletResponse) response.getResponse()).sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (Exception f) {
;
}
ok = false;
}
........
try {
request.finishRequest();
} catch (IOException e) {
ok = false;
} catch (Throwable e) {
log("process.invoke", e);
ok = false;
}
try {
if (output != null)
output.flush();
} catch (IOException e) {
ok = false;
}
}
// We have to check if the connection closure has been requested
// by the application or the response stream (in case of HTTP/1.0
// and keep-alive).
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();
}//end of while
try {
shutdownInput(input);
socket.close();
} catch (IOException e) {
;
} catch (Throwable e) {
log("process.invoke", e);
}
socket = null;
}
private static final byte[] ack =
(new String("HTTP/1.1 100 Continue\r\n\r\n")).getBytes();
private void ackRequest(OutputStream output)
throws IOException {
if (sendAck)
output.write(ack);
}
parseHeaders(){
........
} else if (header.equals(DefaultHeaders.CONNECTION_NAME)) {
if (header.valueEquals
(DefaultHeaders.CONNECTION_CLOSE_VALUE)) {
keepAlive = false;
response.setHeader("Connection", "close");
}
//request.setConnection(header);
/*
if ("keep-alive".equalsIgnoreCase(value)) {
keepAlive = true;
}
*/
} else if (header.equals(DefaultHeaders.EXPECT_NAME)) {
if (header.valueEquals(DefaultHeaders.EXPECT_100_VALUE))
sendAck = true;
else
throw new ServletException
(sm.getString("httpProcessor.parseHeaders.unknownExpectation"));
........
}
代码解析区
这次要解析的关键代码都在同一个类里,比较集中,也比较好理解。这个类是命名空间为org.apache.catalina.connector.http的HttpProcessor类,是APACHE处理HTTP请求线程池里面负责具体执行的类。该类最重要的方法就是process方法,这个方法接收socket对象作为参数,进行请求解析、调用container容器进行处理和返回应答。
第一段代码及解析
private boolean stopped = false;
private void process(Socket socket) {
boolean ok = true;
......
keepAlive = true;
while (!stopped && ok && keepAlive) {
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;
}
首先APACHE设置三个内部变量:
stopped:服务是否被请求停止
ok:内部是否出现错误
keepAlive:是否允许长连接
一开始设置keepAlive为true,ok和stopped也设置为正常值,线程就能进入while循环了。
进入循环后,先初始化request和response对象,把他们需要的流对象都给他们。
第二段代码及解析
private void process(Socket socket) {
boolean ok = true;
......
// Parse the incoming request
try {
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);
}
}
} catch (EOFException e) {
// It's very likely to be a socket disconnect on either the
// client or the server
ok = false;
finishResponse = false;
}
.......
}end of function process
parseHeaders(){
........
} else if (header.equals(DefaultHeaders.CONNECTION_NAME)) {
if (header.valueEquals
(DefaultHeaders.CONNECTION_CLOSE_VALUE)) {
keepAlive = false;
response.setHeader("Connection", "close");
}
//request.setConnection(header);
/*
if ("keep-alive".equalsIgnoreCase(value)) {
keepAlive = true;
}
*/
} else if (header.equals(DefaultHeaders.EXPECT_NAME)) {
if (header.valueEquals(DefaultHeaders.EXPECT_100_VALUE))
sendAck = true;
else
throw new ServletException
(sm.getString("httpProcessor.parseHeaders.unknownExpectation"));
........
}
private static final byte[] ack =
(new String("HTTP/1.1 100 Continue\r\n\r\n")).getBytes();
private void ackRequest(OutputStream output)
throws IOException {
if (sendAck)
output.write(ack);
}
APACHE解析本请求的包头数据,通过parseConnection(socket);parseRequest(input, output);parseHeaders(input);三个方法获取HTTP协议版本,是否允许分块传输数据,是否需要反馈服务器能力等数据,对与这三个方法,这儿就不解析了,就是简单的流处理,上次已经介绍过了。
接下来APACHE判断请求流是不是HTTP1.1协议的,如果是,就调用ackRequest方法按照从HTTP头中读出的数据是否有100-continue标识,来决定是否给客户端反馈数据。如果客户端标记了100-continue,那么就立即给客户端的应答流中写入"HTTP/1.1 100 Continue\r\n\r\n",然后继续对请求流的下一步处理。
第三段代码及解析:
// We have to check if the connection closure has been requested
// by the application or the response stream (in case of HTTP/1.0
// and keep-alive).
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();
}//end of while
然后APACHE调用container容器的invoke方法connector.getContainer().invoke(request, response)进入对应处理逻辑,处理完成后APACHE判断本请求的Connection的值是否是close,如果是close就设置keepAlive值为false,结束while循环,关闭本次连接。如果Connection的值不是close那么接着读取本连接上的请求流,继续处理下一个HTTP请求。不管是否结束本次连接的处理,都需要将request对象和response对象回收了。
总结:
本篇文章对HTTP1.1的三个新特性进行了描述,并且摘出了APACHE的实现关键代码进行说明:
Connection:keep-alive
1.如果HTTP协议小于1.1则设置keepAlive值为false,不进入下一个循环;
2.如果HTTP请求包中的Connection值为Close就则设置keepAlive值为false,不进入下一个循环;
3.否则继续读取本链接上的请求流。
transfer-encoding:chunked
根据配置值设置应答包是否允许分块传输数据模式。
本文章没有解析APACHE如何将数据分块和读取分块数据的代码。
100-continue
APACHE在接收到请求并解析了请求报文中的第一行数据后,判断客户端是否发送了100-continue请求,如果发送了,立刻在应答流中写入"HTTP/1.1 100 Continue\r\n\r\n"让客户端继续发送请求流。
大家没事的时候看看,希望对大家有帮助,如果发现有写错了的地方,一定告诉我,谢谢!
HTTP 1.1的三个新特性及APACHE的实现
HTTP1.1比HTTP1.0多了三个非常重要的特性:
1.支持长连接(connection: keep-alive)
HTTP1.1之前,Server处理完浏览器的请求后会立即断开连接。每次浏览器申请一个资源的时候都需要建立一个连接,而一个页面往往包含了很多资源,图片、CSS文件、脚本文件,Flash文件等,所以每次用户打开一个页面需要浏览器和Server建立很多连接,进而耗费很多资源,速度慢。HTTP1.1引入了长连接的机制,在长连接模式下浏览器下载一个页面时,服务器段并不断开链接直到页面的所有资源都下载完毕。这种机制的引入减少很多用户浏览网页时建立连接和关闭连接的损耗,增加了互联网的浏览效率。
浏览器请求没有发送完毕前,所有HTTP请求的头部都带有这个标识:
connection: keep-alive
这个时候Server应该一直接收这个流,不断处理流中的HTTP请求,不能断开连接。
直到最后一个请求,浏览器发送的HTTP请求的头部带有这个标识:
connection: close
Server读取完本请求后,不再读取这个连接中请求流中的数据,断开连接。
2.分块模式传输数据(transfer-encoding:chunked )
在长链接上,服务器可能会给浏览器发送一个包含多个资源的字节流应答,浏览器也有可能在请求流中携带多个资源。请求和应答时,必须在包头中说明本包的字节数,这样接收者能够根据这个值截断流读取有效数据。而实际情况是,服务器和浏览器在组包的时候并不能确切得知道本包的字节数。比如,Servlet在应答请求的时候,并不必须等到所有事务处理都完成,而是可以先返回准备好的几个字节,等其他事务处理完了再返回这些迟到的事务产生的数据。这种情况需要让接收者知道如何截取数据,如果包头没有包的字节数数据的话。在HTTP1.0时代,服务器端只好把 content-length 字段留空,然后继续返回字节流。数据发送完成后,简单关闭连接,在结尾处加一个-1表示文件流结束了。HTTP1.1使用了一个特殊的头字段 transfer-encoding:chunked 来标识本次流传输是分块进行的,每一块的长度以16进制的形式写在块头,回车符之前。每次以分块模式传输的数据,最后一块的的字节数为0。下面是一个例子,该流总长度为38字节,分两块发送,第一块长度是29字节,第二块长度是9字节:
"I'm as helpless as a kitten up a tree." ,发送的流是下面这个样子:
1D\r\n I'm as helpless as a kitten u 9\r\n p a tree. 0\r\n
3.使用100-continue请求服务器能力反馈
HTTP1.1允许客户端在发送请求包的报文体之前发送报文头使用100-continue来等待服务器的反馈,确认是否允许发送该请求报文的报文体。这种情况通常发生在客户端准备发送一个冗长的请求给服务器,但是不确认服务器是否有能力接收。如果没有得到确认,而将一个冗长的请求包发送给服务器,然后包被服务器给抛弃了,这种情况挺浪费资源的。一旦服务器收到100-continue的请求头,如果此时能够处理该请求,立即给客户端返回 HTTP/1.1 100 Continue 加两个回车符,这时服务器端可以继续读取该请求流了。
APACHE的实现:
代码区
package org.apache.catalina.connector.http;
final class HttpProcessor
.......
private boolean stopped = false;
private void process(Socket socket) {
boolean ok = true;
......
keepAlive = true;
while (!stopped && ok && keepAlive) {
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;
}
// Parse the incoming request
try {
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);
}
}
} catch (EOFException e) {
// It's very likely to be a socket disconnect on either the
// client or the server
ok = false;
finishResponse = false;
}
.......
// Ask our Container to process this request
try {
((HttpServletResponse) response).setHeader
("Date", FastHttpDateFormat.getCurrentDate());
if (ok) {
connector.getContainer().invoke(request, response);
}
} catch (ServletException e) {
log("process.invoke", e);
try {
((HttpServletResponse) response.getResponse()).sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (Exception f) {
;
}
ok = false;
}
........
try {
request.finishRequest();
} catch (IOException e) {
ok = false;
} catch (Throwable e) {
log("process.invoke", e);
ok = false;
}
try {
if (output != null)
output.flush();
} catch (IOException e) {
ok = false;
}
}
// We have to check if the connection closure has been requested
// by the application or the response stream (in case of HTTP/1.0
// and keep-alive).
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();
}//end of while
try {
shutdownInput(input);
socket.close();
} catch (IOException e) {
;
} catch (Throwable e) {
log("process.invoke", e);
}
socket = null;
}
private static final byte[] ack =
(new String("HTTP/1.1 100 Continue\r\n\r\n")).getBytes();
private void ackRequest(OutputStream output)
throws IOException {
if (sendAck)
output.write(ack);
}
parseHeaders(){
........
} else if (header.equals(DefaultHeaders.CONNECTION_NAME)) {
if (header.valueEquals
(DefaultHeaders.CONNECTION_CLOSE_VALUE)) {
keepAlive = false;
response.setHeader("Connection", "close");
}
//request.setConnection(header);
/*
if ("keep-alive".equalsIgnoreCase(value)) {
keepAlive = true;
}
*/
} else if (header.equals(DefaultHeaders.EXPECT_NAME)) {
if (header.valueEquals(DefaultHeaders.EXPECT_100_VALUE))
sendAck = true;
else
throw new ServletException
(sm.getString("httpProcessor.parseHeaders.unknownExpectation"));
........
}
代码解析区
这次要解析的关键代码都在同一个类里,比较集中,也比较好理解。这个类是命名空间为org.apache.catalina.connector.http的HttpProcessor类,是APACHE处理HTTP请求线程池里面负责具体执行的类。该类最重要的方法就是process方法,这个方法接收socket对象作为参数,进行请求解析、调用container容器进行处理和返回应答。
第一段代码及解析
private boolean stopped = false;
private void process(Socket socket) {
boolean ok = true;
......
keepAlive = true;
while (!stopped && ok && keepAlive) {
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;
}
首先APACHE设置三个内部变量:
stopped:服务是否被请求停止
ok:内部是否出现错误
keepAlive:是否允许长连接
一开始设置keepAlive为true,ok和stopped也设置为正常值,线程就能进入while循环了。
进入循环后,先初始化request和response对象,把他们需要的流对象都给他们。
第二段代码及解析
private void process(Socket socket) {
boolean ok = true;
......
// Parse the incoming request
try {
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);
}
}
} catch (EOFException e) {
// It's very likely to be a socket disconnect on either the
// client or the server
ok = false;
finishResponse = false;
}
.......
}end of function process
parseHeaders(){
........
} else if (header.equals(DefaultHeaders.CONNECTION_NAME)) {
if (header.valueEquals
(DefaultHeaders.CONNECTION_CLOSE_VALUE)) {
keepAlive = false;
response.setHeader("Connection", "close");
}
//request.setConnection(header);
/*
if ("keep-alive".equalsIgnoreCase(value)) {
keepAlive = true;
}
*/
} else if (header.equals(DefaultHeaders.EXPECT_NAME)) {
if (header.valueEquals(DefaultHeaders.EXPECT_100_VALUE))
sendAck = true;
else
throw new ServletException
(sm.getString("httpProcessor.parseHeaders.unknownExpectation"));
........
}
private static final byte[] ack =
(new String("HTTP/1.1 100 Continue\r\n\r\n")).getBytes();
private void ackRequest(OutputStream output)
throws IOException {
if (sendAck)
output.write(ack);
}
APACHE解析本请求的包头数据,通过parseConnection(socket);parseRequest(input, output);parseHeaders(input);三个方法获取HTTP协议版本,是否允许分块传输数据,是否需要反馈服务器能力等数据,对与这三个方法,这儿就不解析了,就是简单的流处理,上次已经介绍过了。
接下来APACHE判断请求流是不是HTTP1.1协议的,如果是,就调用ackRequest方法按照从HTTP头中读出的数据是否有100-continue标识,来决定是否给客户端反馈数据。如果客户端标记了100-continue,那么就立即给客户端的应答流中写入"HTTP/1.1 100 Continue\r\n\r\n",然后继续对请求流的下一步处理。
第三段代码及解析:
// We have to check if the connection closure has been requested
// by the application or the response stream (in case of HTTP/1.0
// and keep-alive).
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();
}//end of while
然后APACHE调用container容器的invoke方法connector.getContainer().invoke(request, response)进入对应处理逻辑,处理完成后APACHE判断本请求的Connection的值是否是close,如果是close就设置keepAlive值为false,结束while循环,关闭本次连接。如果Connection的值不是close那么接着读取本连接上的请求流,继续处理下一个HTTP请求。不管是否结束本次连接的处理,都需要将request对象和response对象回收了。
总结:
本篇文章对HTTP1.1的三个新特性进行了描述,并且摘出了APACHE的实现关键代码进行说明:
Connection:keep-alive
1.如果HTTP协议小于1.1则设置keepAlive值为false,不进入下一个循环;
2.如果HTTP请求包中的Connection值为Close就则设置keepAlive值为false,不进入下一个循环;
3.否则继续读取本链接上的请求流。
transfer-encoding:chunked
根据配置值设置应答包是否允许分块传输数据模式。
本文章没有解析APACHE如何将数据分块和读取分块数据的代码。
100-continue
APACHE在接收到请求并解析了请求报文中的第一行数据后,判断客户端是否发送了100-continue请求,如果发送了,立刻在应答流中写入"HTTP/1.1 100 Continue\r\n\r\n"让客户端继续发送请求流。