引子
在HTTP/1.0 時代,HTTP與TCP默認傳輸?shù)年P(guān)系如圖1所示,每次HTTP請求和響應(yīng)都發(fā)生一次TCP三次握手和四次揮手。以HTTP/1.0 時代的網(wǎng)絡(luò)通信量來看,都是些容量很小的文本傳輸,所以圖1的傳輸方式性能沒有較大問題。

可是隨著互聯(lián)網(wǎng)普及,Web網(wǎng)頁的圖片逐漸多起來。比如,使用瀏覽器瀏覽一個包含多張圖片的HTML頁面時,在發(fā)送HTTP請求訪問HTML頁面資源的同時,也會請求該HTML頁面里包含的其他資源,比如圖片等靜態(tài)文件。因此,每次的請求都會造成無謂的TCP連接建立和斷開,增加網(wǎng)絡(luò)通信量的開銷,如圖2所示。

為了解決上述問題,HTTP/1.1和一部分的HTTP/1.0想出了持久連接的辦法,即只要任意一端沒有明確提出斷開連接,則保持TCP連接狀態(tài),如圖3所示。

HTTP持久連接不是絕對高性能
HTTP持久連接允許在事務(wù)處理結(jié)束之后將TCP連接保持在打開狀態(tài),以便為未來的HTTP請求重用現(xiàn)存的連接。在事務(wù)處理結(jié)束之后仍然保持在打開狀態(tài)的TCP連接被稱為持久連接。持久連接會在不同事務(wù)之間保持打開狀態(tài),直到客戶端或服務(wù)器決定將其關(guān)閉為止。
優(yōu)點:重用已對目標(biāo)服務(wù)器打開的空閑持久連接,可以避開緩慢的連接建立階段,更快速地進行數(shù)據(jù)的傳輸。
缺點:管理不當(dāng)可能會積累出大量的空閑連接,耗費本地客戶端以及遠程服務(wù)器上的資源。
非持久連接會在每個事務(wù)處理結(jié)束之后關(guān)閉。
HTTP持久連接實現(xiàn)手段是HTTP首部添加Connection字段
- Connection: keep-alive , 開啟HTTP持久連接,HTTP 1.1默認值
- Connection: close , 關(guān)閉HTTP持久連接,HTTP 1.0默認值
HTTP keep-alive與TCP keep-alive區(qū)別
- HTTP keep-alive參數(shù)為了減少TCP連接和斷開而提出的一種解決方案,HTTP持久連接即TCP長連接。
- TCP keep-alive參數(shù)主要為探測長連接的存活狀況,即TCP?;罟δ堋?/li>
本文將對HTTP首部Connction實踐,對比keep-alive/close兩個值在HTTP和TCP的表現(xiàn)情況。后端使用Spring boot+Java,前端使用HTML+CSS。
HTTP Request首部Connection
如果Client希望HTTP使用持久連接,在Request首部指定Connection: keep-alive,否則指定Connection: close。
后端Java代碼如下:
package com.demo.web.http;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("http")
public class ConnectionController {
@RequestMapping("/connection")
public String connection(@RequestHeader(value="Connection") String connection) {
System.out.println("Connection: " + connection);
return "http/connection";
}
}
前端HTML代碼:
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP Connection Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h3>實踐HTTP Connection</h3>
<a href="connection">刷新頁面</a>
</body>
</html>
后端服務(wù)啟動端口8080,執(zhí)行nc 127.0.0.1 8080,輸入如下兩行命令,兩次回車,HTTP Request首部Connection: keep-alive,執(zhí)行情況如圖4第一個紅色方框,緊接著返回Web頁面,后端日志打印Connection: keep-alive。從圖5抓包紅色方框看,TCP沒有發(fā)起四次揮手釋放連接,HTTP請求保持TCP長連接。
GET http://127.0.0.1:8080/http/connection HTTP/1.1
Connection: keep-alive


再次輸入如下兩行命令,兩次回車,HTTP Request首部Connection: keep-alive,執(zhí)行情況如圖4第二個紅色方框,緊接著返回Web頁面,后端日志打印Connection: keep-alive。從圖5綠色方框看,TCP沒有發(fā)起四次揮手釋放連接,HTTP請求保持TCP長連接。
GET http://127.0.0.1:8080/http/connection HTTP/1.1
Connection: keep-alive
再次輸入如下兩行命令,兩次回車,HTTP Request首部Connection: close,執(zhí)行情況如圖6綠色方框,緊接著返回Web頁面,后端日志打印Connection: close。從圖5藍色色方框看,TCP發(fā)起四次揮手釋放連接,HTTP請求的TCP連接斷開。
GET http://127.0.0.1:8080/http/connection HTTP/1.1
Connection: close

HTTP Response首部Connection
如果Server希望HTTP使用長連接,在Response首部指定Connection: keep-alive,否則指定Connection: close。
后端Java代碼如下:
package com.demo.web.http;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletResponse;
@Controller
@RequestMapping("http")
public class ConnectionController {
@RequestMapping("/connection")
public String connection(HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value="Connection") String connection) {
System.out.println("Connection: " + connection);
response.addHeader("Connection", "keep-alive");
return "http/connection";
}
}
前端HTML代碼:
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP Connection Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h3>實踐HTTP Connection</h3>
<a href="connection">刷新頁面</a>
</body>
</html>
HTTP/1.1的Request首部Connection默認為keep-alive,當(dāng)后端返回Response首部Connection: keep-alive,訪問http://127.0.0.1:8080/http/connection,如圖6所示,點擊按鈕刷新頁面。從圖7綠色方框和紅色方框看,TCP沒有發(fā)起四次揮手釋放連接,HTTP請求保持TCP長連接。超時后TCP連接會自動斷開,從四次揮手的開始時間21:31:40.000676與Web頁面請求結(jié)束時間21:30:39.945438看,TCP長連接60s超時。


HTTP/1.0與HTTP/1.1 首部Connection默認值對比
后端Java代碼:
package com.demo.web.http;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("http")
public class ConnectionController {
@RequestMapping("/connection")
public String connection() {
return "http/connection";
}
}
后端服務(wù)啟動端口8080,執(zhí)行nc 127.0.0.1 8080,輸入如下命令,兩次回車,重復(fù)執(zhí)行如下命令,如圖8所示,TCP沒有發(fā)起四次揮手釋放連接,HTTP請求保持TCP長連接,所以HTTP/1.1 首部Connection默認值為keep-alive。
GET http://127.0.0.1:8080/http/connection HTTP/1.1

后端服務(wù)啟動端口8080,執(zhí)行nc 127.0.0.1 8080,輸入如下命令,兩次回車,如圖9所示,TCP釋放連接,HTTP請求也結(jié)束,所以HTTP/1.0 首部Connection默認值為close。
GET http://127.0.0.1:8080/http/connection HTTP/1.0

再進一步,后端服務(wù)啟動端口8080,執(zhí)行nc 127.0.0.1 8080,輸入如下命令,兩次回車,如圖9所示,TCP釋放連接,HTTP請求也結(jié)束,所以HTTP默認使用1.0版本。
GET http://127.0.0.1:8080/http/connection

HTTP持久連接的數(shù)據(jù)傳輸完成識別
HTTP首部定義Connection: keep-alive后,客戶端、服務(wù)端怎么知道本次傳輸結(jié)束呢?兩部分:
- 靜態(tài)頁面通過Content-Length提前告知對方數(shù)據(jù)傳輸大小,具體可以參考拙作HTTP Content-Length深入實踐。
- 動態(tài)頁面不能通過Content-Length提前告知對方數(shù)據(jù)傳輸大小,它是分塊傳輸(chunked),這時候就要根據(jù)chunked編碼來判斷,chunked編碼的數(shù)據(jù)在最后有一個空chunked塊,表明本次傳輸數(shù)據(jù)結(jié)束,HTTP頭部使用
Transfer-Encoding: chunked來代替Content-Length。