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

什麼是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模式時,客戶端到服務器端的連接不會斷開,當出現對服務器的後繼請求時,客戶端就會複用已建立的連接。

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

640px-HTTP_persistent_connection.svg.png?1568213219388

在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

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

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

chunk detail

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

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

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

通過Keep-Alive Timeout標識

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

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

Keep-Alive:timeout=5

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

keep-alive timeout

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

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

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

15s resend

通過Connection close標識

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

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

Connection : 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標準的策略,也自定定製策略。

HttpClients.custom()
                //連接是否複用策略,通過此策略返回是否複用
                //DefaultClientConnectionReuseStrategy是默認的Http策略,不設置也可以
                .setConnectionReuseStrategy(new DefaultClientConnectionReuseStrategy())
                //連接複用後有效期(持久時間)策略,複用後通過此策略判斷複用超時時間
                //DefaultConnectionKeepAliveStrategy是默認的判斷超時時間策略,讀取的是Keep-Alive:timeout=超時時間
                .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
                .build();

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

//創建客戶端,此客戶端最好保持單例,這是個線程安全的類,併發下也沒有問題。
//HttpClient中的連接池等組件都包含在內,如果每次都新建的話,
//效率低,佔用資源大,連接複用當然也不會生效了。
HttpClients.custom()
                //禁用自動重試,默認有3次的重試策略
                .disableAutomaticRetries()
                //不用默認的重試策略,自定義
                .setRetryHandler()
                //設置默認請求配置,這裏可以配置一些超時時間等參數    
                .setDefaultRequestConfig(requestConfig())
                //全局Header,每次請求都會攜帶
                .setDefaultHeaders()
                //當Https證書不受信任的時候,記得自定義此項
                .setSSLHostnameVerifier()
                //設置UA
                .setUserAgent()
                //設置代理
                .setProxy()
                //...還有很多配置,可以自行查閱文檔
                .build();

TCP中的Keep-Alive

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

Netty中設置Keep-Alive

bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

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

channel.setOption(StandardSocketOptions.SO_KEEPALIVE,true);

BIO中設置Keep-Alive

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

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章