##15.1 持久连接 在HTTP1.0版本或之前,浏览器每建立一条TCP请求,只能发起一条HTTP请求,完成响应后就将TCP连接关闭。假设一个页面有100个资源,那么浏览器就需要为此建立100次TCP连接,这其实是__高消耗低性能__的做法。原因:
- TCP端口消耗过多;
- TCP重复建立握手造成时延和性能消耗;
从HTTP1.0增强版和HTTP1.1开始支持持久连接,所谓__持久连接__就是__使用同一条TCP连接建立多次HTTP请求。__假设每条TCP请求最多可以处理10个HTTP请求,那么原来的100次TCP连接则可减少为10次。那么就可以大大降低TCP的消耗。
如果要使用持久连接,则需要在HTTP头域使用Connection: keep-alive告诉服务器建立持久连接。假设要在一个TCP请求中发起10次HTTP请求,则需要在前9次的HTTP请求头域加入Connection: keep-alive,同时第十个HTTP头域使用Connection: close告诉服务器在响应这个HTTP请求后自动关闭TCP连接。(如果不写,TCP会在客户端或浏览器超时或自主断开后释放连接)
##15.2 示例
以下代码演示了一个TCP请求发起两个HTTP请求,第一个HTTP请求使用了Connection: keep-alive告诉服务器建立持久连接,而第二个请求则使用Connection: close告诉服务器在处理该HTTP请求后自动断开TCP连接。
public static void main(String[] args) throws UnknownHostException, IOException {
Socket socket = new Socket("localhost", 8080);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
new Thread(() -> {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
while ((line = br.readLine()) != null) { // 迭代输出的文本
System.out.println(line);
}
br.close();
bw.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
bw.write("GET /springmvc/ HTTP/1.1\r\n");
bw.write("HOST: localhost:8080\r\n");
bw.write("Connection: keep-alive\r\n");
bw.write("\r\n");
bw.flush();
bw.write("GET /springmvc/ HTTP/1.1\r\n");
bw.write("HOST: localhost:8080\r\n");
bw.write("Connection: close\r\n");
bw.write("\r\n");
bw.flush();
}响应的报文如下:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=C1CC998B1DF491A2AA2DFC874624E247; Path=/springmvc/; HttpOnly
Content-Type: text/html;charset=ISO-8859-1
Content-Language: zh-CN
Content-Length: 89
Date: Mon, 28 Apr 2014 10:25:25 GMT
<html>
<head>
<title>Home</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=3E421B3CB47418054E0DDDA6BCB4CBD0; Path=/springmvc/; HttpOnly
Content-Type: text/html;charset=ISO-8859-1
Content-Language: zh-CN
Content-Length: 89
Date: Mon, 28 Apr 2014 10:25:25 GMT
Connection: close
<html>
<head>
<title>Home</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
可以看到,由于使用了持久连接,服务器将两个HTTP的报文都写在了一个TCP连接中,那么我们如何分割报文,以及找到HTTP请求对应的报文呢。
- 首先,在服务器响应的报文中,会有一个
Content-Length的头域,该头域用于指定对应的HTTP报文的Message Body大小,那么我们可以从头域后开始读取Content-Length指定大小的内容,这就是一个完整的HTTP报文了,依次类推就可以分割各个不同的HTTP请求的报文。 - 在持久连接中,HTTP响应的报文顺序就是发起的HTTP请求顺序,所以浏览器只要把持久连接的HTTP请求顺序记录下来则可以找到对应的HTTP响应报文了。
- 大概处理如此,考虑到各种异常情况,实际上处理要复杂的多。
##15.3 More
- 在15.2的示例中,将第一个请求改为
Connection: close则服务器在处理第一个HTTP请求后就会关闭TCP连接而不会处理其余的HTTP请求。 - 如果将第二个请求改为
Connection: keep-alive,则客户端在打印完两个HTTP报文后,并不会即使断开TCP连接,由于服务器以为客户端还会持续发送HTTP请求而不会主动断开TCP请求,所以line并不会读到null值。这时候,要么客户端或服务器主动断开TCP请求,或等待客户端或服务器任意一端超时。
##15.4 参考资料
- 《HTTP权威指南》
- 以上代码使用Java8编写