1. 程式人生 > 其它 >細說Http中的Keep-Alive和Java Http中的Keep-Alive機制

細說Http中的Keep-Alive和Java Http中的Keep-Alive機制

參考:https://blog.csdn.net/qq826654664jx/article/details/100864080

參考:https://blog.csdn.net/weixin_37672169/article/details/80283935

什麼是 Keep-Alive

這個詞看著有點熟,很多地方好像都見過。

TCP 的 KeepAlive,Http 的 KeepAlive,現在就連一些前端框架都有類似 KeepAlive 的東西了(比如 VUE.js,保持路由)。

本文介紹 HTTP 和 TCP 中的 KeepAlive 機制,其他方面不在本文討論範圍。

Http 中的 Keep-Alive

HTTP 持久連線(HTTP persistent connection,也稱作 HTTP keep-alive 或 HTTP connection reuse,翻譯過來可以是保持連線或者連線複用)是使用同一個 TCP 連線來發送和接收多個 HTTP 請求 / 應答,而不是為每一個新的請求 / 應答開啟新的連線的方式。

HTTP 協議採用 “請求 - 應答” 模式,當使用普通模式,即非 KeepAlive 模式時,每個請求 / 應答客戶和伺服器都要新建一個連線,完成 之後立即斷開連線(HTTP 協議為無連線的協議),每次請求都會經過三次握手四次揮手過程,效率較低;當使用Keep-Alive模式時,客戶端到伺服器端的連線不會斷開,當出現對伺服器的後繼請求時,客戶端就會複用已建立的連線。

下圖是每次新建連線和連線複用在通訊模型上的區別:

在 Http 1.0 中,Keep-Alive是沒有官方支援的,但是也有一些 Server 端支援,這個年代比較久遠就不用考慮了。

Http1.1 以後,Keep-Alive已經預設支援並開啟。客戶端(包括但不限於瀏覽器)傳送請求時會在 Header 中增加一個請求頭Connection: Keep-Alive,當伺服器收到附帶有Connection: Keep-Alive的請求時,也會在響應頭中新增 Keep-Alive。這樣一來,客戶端和伺服器之間的 HTTP 連線就會被保持,不會斷開(斷開方式下面介紹),當客戶端傳送另外一個請求時,就可以複用已建立的連線。

現在的 Http 協議基本都是 Http 1.1 版本了,不太需要考慮 1.0 的相容問題

Keep-Alive 真的就這麼完美嗎

當然不是,Keep-Alive 也有自己的優缺點,並不是所有場景下都適用

優點

  • 節省了服務端 CPU 和記憶體適用量
  • 降低擁塞控制 (TCP 連線減少)
  • 減少了後續請求的延遲(無需再進行握手)

缺點

對於某些低頻訪問的資源 / 服務,比如一個冷門的圖片伺服器,一年下不了幾次,每下一次連線還保持就比較浪費了(這個場景舉的不是很恰當)。Keep-Alive 可能會非常影響效能,因為它在檔案被請求之後還保持了不必要的連線很長時間,額外佔用了服務端的連線數。

連線複用後會有什麼問題

在沒有連線複用時,Http 接收端(注意這裡是接收端,並沒有特指 Client/Server,因為 Client/Server 都同是傳送端和接收端)只需要讀取 Socket 中所有的資料就可以了,解決 “拆包” 問題即可;但是連線複用後,無法區分單次 Http 報文的邊界,所以還需要額外處理報文邊界問題。當然這個通過 Http 中 Header 的長度欄位,按需讀取即可解決。

粘包拆包的介紹可以參考另一篇文章細說 Netty 中的粘包和拆包

Http 連線複用後包邊界問題處理

由於 Http 中 Header 的存在,通過定義一些報文長度的首部欄位,可以很方便的處理包邊界問題。

在 Http 中,有兩種方式處理包邊界問題:

Content-Length 處理包邊界

這個是最通常的處理方式,接收端處理報文時首先讀取完整首部(Header),然後通過 Header 中的Content-Length來確認報文大小,讀取報文時按此長度讀取即可,超出長度的報文(“粘包”)不讀取,不夠長度的報文快取等待繼續讀取(“拆包”)。

Chunked 處理包邊界

對於無法確認總報文大小的情況,可以使用 Chunked 的方式來對報文進行分塊傳輸,每一塊內標示報文大小。比如 Nginx,開啟 Gzip 壓縮後,就會開啟 Chunked 的傳輸方式。

通過 Wireshark 抓包,可以很直觀的看初 Chunked 的原理:

注意,這裡的 chunk 包,和 tcp segment 不是一回事,chunk 只是應用層的一個分包,而 tcp 的 segment 是對應用層報文再次進行分組

每個 chunk 報文前,會攜帶當前 chunk 的大小。

Http 連線複用後怎樣斷開連線

通過 Keep-Alive 已經做到連線複用了,但複用之後什麼時候斷開連線呢,不然一直保持連線,造成資源的浪費。

Http 協議規定了兩種關閉複用連線的方式:

通過 Keep-Alive Timeout 標識

如果服務端 Response Header 設定了Keep-Alive:timeout={timeout},客戶端會就會保持此連線 timeout(單位秒)時間,超時之後關閉連線。

現在在服務端設定響應 Header:

Keep-Alive:timeout=5

通過 Wireshark 來看下配置了 timeout 的效果:

從上圖可以看出,客戶端傳送請求後,在 15S 內(圖上沒有體現時間,就當 15S 吧)保持了連線不銷燬,超時後經過了 4 次揮手,斷開連線

但是如果在 15S 內再次請求,連線是可以複用的,不會重新 3 次握手。

下圖是 15S 內再次請求的效果:

通過 Connection close 標識

還有一種方式是接收端通在 Response Header 中增加Connection close標識,來主動告訴傳送端,連線已經斷開了,不能再複用了;客戶端接收到此標示後,會銷燬連線,再次請求時會重新建立連線。

注意:配置 close 配置後,並不是說每次都新建連線,而是約定此連線可以用幾次,達到這個最大次數時,接收端就會返回 close 標識(服務端配置方法下面會介紹)

下面來測試下效果,客戶端傳送兩次請求:

通過 wireshark 截圖可以發現,配置了 Connection:close 之後 (服務端設定了請求只可以用 1 此,所所以請求完成就銷燬連線),兩次請求都重新建立了連線。

Nginx 中設定 Keep-Alive(服務端)

Keep-Alive timeout 配置:

Syntax:     keepalive_timeout timeout [header_timeout];
Default:    keepalive_timeout 75s;
Context:    http, server, location

第一個引數設定一個超時,在此期間保持活動的客戶機連線將在伺服器端保持開啟狀態。如果為0則禁用保Keep-Alive。第二個可選引數在“Keep-Alive: timeout=time”響應頭欄位中設定一個值。

“Keep-Alive: timeout=time”報頭欄位被Mozilla和Konqueror識別。MSIE在大約60秒內自動關閉保持連線。

Keep-Alive requests(連線可用次數)配置:

Syntax:     keepalive_requests number;
Default:    keepalive_requests 100;
Context:    http, server, location

設定通過一個保持活動連線可以服務的請求的最大數量。在發出最大數量的請求之後,連線關閉。

Tomcat 中設定 Keep-Alive(服務端)

<Connector>標籤中配置屬性:

Keep-Alive timeout 配置:

keepAliveTimeout="超時時間",預設值是使用為 connectionTimeout 屬性設定的值 。值為 - 1 表示沒有(即無限)超時。

Keep-Alive requests(連線可用次數)配置:

maxKeepAliveRequests="連線可用次數",-1 為永不失效。如果未指定,預設為 100。

例如:

<Connector port="8080" 
    protocol="HTTP/1.1" 
    connectionTimeout="20000" 
    redirectPort="8443" 
    keepAliveTimeout="超時時間(單位秒)"
    maxKeepAliveRequests="連線可用次數" />

Apache HttpClient 設定 Keep-Alive(客戶端)

Apache HttpClient 算是 Java 中最強的 HttpClient 了,也是最主流的(後端方向),功能強大。
Apache HttpClient 在處理 KeepAlive 的地方設計的比較靈活,提供了可配置的介面,使用者可以使用 Http 標準的策略,也自定定製策略。

1 HttpClients.custom()
2                 //連線是否複用策略,通過此策略返回是否複用
3                 //DefaultClientConnectionReuseStrategy是預設的Http策略,不設定也可以
4                 .setConnectionReuseStrategy(new DefaultClientConnectionReuseStrategy())
5                 //連線複用後有效期(持久時間)策略,複用後通過此策略判斷複用超時時間
6                 //DefaultConnectionKeepAliveStrategy是預設的判斷超時時間策略,讀取的是Keep-Alive:timeout=超時時間
7                 .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
8                 .build();

這裡順帶說一下 Apache HttpClient 的使用,希望能幫助到有需要的人。(版本 Apache HttpClient 4.x)

 1 //建立客戶端,此客戶端最好保持單例,這是個執行緒安全的類,併發下也沒有問題。
 2 //HttpClient中的連線池等元件都包含在內,如果每次都新建的話,
 3 //效率低,佔用資源大,連線複用當然也不會生效了。
 4 HttpClients.custom()
 5                 //禁用自動重試,預設有3次的重試策略
 6                 .disableAutomaticRetries()
 7                 //不用預設的重試策略,自定義
 8                 .setRetryHandler()
 9                 //設定預設請求配置,這裡可以配置一些超時時間等引數    
10                 .setDefaultRequestConfig(requestConfig())
11                 //全域性Header,每次請求都會攜帶
12                 .setDefaultHeaders()
13                 //當Https證書不受信任的時候,記得自定義此項
14                 .setSSLHostnameVerifier()
15                 //設定UA
16                 .setUserAgent()
17                 //設定代理
18                 .setProxy()
19                 //...還有很多配置,可以自行查閱文件
20                 .build();

TCP 中的 Keep-Alive

TCP 中的 KeepAlive 和 Http 的 Keep-Alive 可不是一回事,HTTP 中是做連線複用的,而 TCP 中的 KeepAlive 是 “心跳監測”,定時傳送一個空的 TCP Segment,來監測連線是否存活。下面介紹下 Java 中設定 TCP KeepAive 的一些方式。

Netty 中設定 Keep-Alive

1 bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

NIO(New NetWorking IO Lib)中設定 Keep-Alive

1 channel.setOption(StandardSocketOptions.SO_KEEPALIVE,true);

BIO 中設定 Keep-Alive

1 Socket socket = serverSocket.accept();
2 socket.setKeepAlive(true);

HTTP和TCP的Keep-Alive timeout區別

Keep-Alive timeout:

Httpd守護程序,一般都提供了keep-alive timeout時間設定引數。比如nginx的keepalive_timeout,和Apache的KeepAliveTimeout。這個keepalive_timout時間值意味著:一個http產生的tcp連線在傳送完最後一個響應後,還需要hold住keepalive_timeout秒後,才開始關閉這個連線。
當httpd守護程序傳送完一個響應後,理應馬上主動關閉相應的tcp連線,設定 keepalive_timeout後,httpd守護程序會想說:”再等等吧,看看瀏覽器還有沒有請求過來”,這一等,便是keepalive_timeout時間。如果守護程序在這個等待的時間裡,一直沒有收到瀏覽器發過來http請求,則關閉這個http連線。

Tcp的Keepalive:

連線建立之後,如果客戶端一直不傳送資料,或者隔很長時間才傳送一次資料,當連線很久沒有資料報文傳輸時如何去確定對方還線上,到底是掉線了還是確實沒有資料傳輸,連線還需不需要保持,這種情況在TCP協議設計中是需要考慮到的。
TCP協議通過一種巧妙的方式去解決這個問題,當超過一段時間之後,TCP自動傳送一個數據為空的報文(偵測包)給對方,如果對方迴應了這個報文,說明對方還線上,連線可以繼續保持,如果對方沒有報文返回,並且重試了多次之後則認為連結丟失,沒有必要保持連線。

tcp keep-alive是TCP的一種檢測TCP連線狀況的保鮮機制。tcp keep-alive保鮮定時器,支援三個系統核心配置引數:
net.ipv4.tcp_keepalive_intvl = 15
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_time = 1800
keepalive是TCP保鮮定時器,當網路兩端建立了TCP連線之後,閒置(雙方沒有任何資料流傳送往來)了tcp_keepalive_time後,伺服器就會嘗試向客戶端傳送偵測包,來判斷TCP連線狀況(有可能客戶端崩潰、強制關閉了應用、主機不可達等等)。如果沒有收到對方的回答(ack包),則會在 tcp_keepalive_intvl後再次嘗試傳送偵測包,直到收到對方的ack,如果一直沒有收到對方的ack,一共會嘗試 tcp_keepalive_probes次,每次的間隔時間在這裡分別是15s, 30s, 45s, 60s, 75s。如果嘗試tcp_keepalive_probes,依然沒有收到對方的ack包,則會丟棄該TCP連線。TCP連線預設閒置時間是2小時,一般設定為30分鐘足夠了。