漫談:Http網(wǎng)絡(luò)協(xié)議中的Proxy-Connection
平時用 Chrome 開發(fā)者工具抓包時,經(jīng)常會見到 Proxy-Connection 這個請求頭。之前一直沒去了解什么情況下會產(chǎn)生它,也沒去了解它有什么含義。最近看完《HTTP 權(quán)威指南》第四章「連接管理」和第六章「代理」之后,終于搞明白了這是因為給瀏覽器設(shè)置了代理(Proxy)。而神器 Fiddler 的抓包原理就是讓瀏覽器請求走它開的本地代理,所以開了 Fiddler 必然會產(chǎn)生這個請求頭。
代理改變了什么?
為了徹底弄清這個問題,我們先來看下設(shè)置瀏覽器代理之后,HTTP 請求報文有那些變化。下面分別是設(shè)置代理前后訪問同一 URL 的請求報文(省略了無關(guān)內(nèi)容):
GET / HTTP/1.1 Host: www.example.com Connection: keep-alive GET http://www.example.com/ HTTP/1.1 Host: www.example.com Proxy-Connection: keep-alive
設(shè)置代理之后,瀏覽器連接的是代理服務(wù)器,不再是目標服務(wù)器,這個變化單純從請求報文中無法看出。請求報文中的變化有兩點:第一行中的 request-URL 變成了完整路徑;Connection 請求頭被替換成了 Proxy-Connection。我們分別來看這兩個變化。
為什么需要完整路徑?
早期的 HTTP 設(shè)計中,瀏覽器直接與單個服務(wù)器進行對話,不存在虛擬主機。單個服務(wù)器總是知道自己的主機名和對應(yīng)端口,為了避免冗余,瀏覽器只需要發(fā)送主機名之外的那部分 URI 就行了。代理出現(xiàn)之后,部分 URI 徹底杯具,代理服務(wù)器無法得知用戶想要訪問的URI在什么主機上。為此,HTTP/1.0 要求瀏覽器為代理請求發(fā)送完整的 URI,也就是說規(guī)范告訴瀏覽器的實現(xiàn)者必須這么做。
顯式地給瀏覽器配置代理后,瀏覽器會為之后的請求使用完整 URI,解決了代理無法定位資源的問題。但是代理可以出現(xiàn)在連接的任何位置,很多代理對瀏覽器來說不可見,如反向代理或路由器代理。所以實際上,幾乎所有的瀏覽器都會為每個請求加上內(nèi)容為主機名的 HOST 請求頭,來徹底解決虛擬主機問題。對于 HTTP/1.1 請求,HOST 請求頭必須存在,否則會收到 400 錯誤;對于 HTTP/1.0 請求,如果連接的是代理服務(wù)器,使用相對 URI,并且沒有 HOST 請求頭,會發(fā)生錯誤。
Proxy-Connection 是什么?
HTTP 中的 Connection,用來對 HTTP 連接進行說明,多個說明使用英文逗號隔開,如:
GET / HTTP/1.1 Host: www.example.com Connection: my-header, close, my-connection My-Header: xxx
其中,「my-header」是本次請求中其它 Header 的名字(不區(qū)分大小寫),表示這個 Header 只與當前連接有關(guān)。實際上,Connection 本身也只有當前連接有關(guān)。當客戶端和服務(wù)端存在一個或多個中間實體(如代理)時,每個請求報文都會從客戶端(通常是瀏覽器)開始,逐跳發(fā)給服務(wù)器;服務(wù)器的響應(yīng)報文,也會逐跳返回給客戶端。通常,即使通過了重重代理,請求頭都會原封不動的發(fā)給服務(wù)器,響應(yīng)頭也會原樣被客戶端收到。但 Connection,以及 Connection 定義的其它 Header,只是對上個節(jié)點和當前節(jié)點之間的連接進行說明,必須在報文轉(zhuǎn)給下個節(jié)點之前刪除,否則可能會引發(fā)后面要提到的問題。其它不能傳遞的 Header 還有Prxoy-Authenticate、Proxy-Connection、Transfer-Encoding 和 Upgrade。
「close」表示操作完成后需要關(guān)閉當前連接;Connection 還允許任何字符串作為它的值,如「my-connection」,用來存放自定義的連接說明。HTTP/1.0 默認不支持持久連接,很多 HTTP/1.0 的瀏覽器和服務(wù)器使用「Keep-Alive」這個自定義說明來協(xié)商持久連接:瀏覽器在請求頭里加上 Connection: Keep-Alive,服務(wù)端返回同樣的內(nèi)容,這個連接就會被保持供后續(xù)使用。對于 HTTP/1.1,Connection: Keep-Alive 已經(jīng)失去意義了,因為 HTTP/1.1 除了顯式地將 Connection 指定為 close,默認都是持久連接。
有了上面的背景知識,我們來看問題?;ヂ?lián)網(wǎng)上,存在著大量簡陋并過時的代理服務(wù)器在繼續(xù)工作,它們很可能無法理解 Connection——無論是請求報文還是響應(yīng)報文中的 Connection。而代理服務(wù)器在遇到不認識的 Header 時,往往都會選擇繼續(xù)轉(zhuǎn)發(fā)。大部分情況下這樣做是對的,很多使用 HTTP 協(xié)議的應(yīng)用軟件擴展了 HTTP 頭部,如果代理不傳輸擴展字段,這些軟件將無法工作。
如果瀏覽器對這樣的代理發(fā)送了 Connection: Keep-Alive,那么結(jié)果會變得很復(fù)雜。這個 Header 會被不理解它的代理原封不動的轉(zhuǎn)給服務(wù)端,如果服務(wù)器也不能理解就還好,能理解就徹底杯具了。服務(wù)器并不知道 Keep-Alive 是由代理錯誤地轉(zhuǎn)發(fā)而來,它會認為代理希望建立持久連接,服務(wù)端同意之后也返回一個 Keep-Alive。同樣,響應(yīng)中的 Keep-Alive 也會被代理原樣返給瀏覽器,同時代理還會傻等服務(wù)器關(guān)閉連接——實際上,服務(wù)端已經(jīng)按照 Keep-Alive 指示保持了連接,即使數(shù)據(jù)回傳完成,也不會關(guān)閉連接。另一方面,瀏覽器收到 Keep-Alive 之后,會復(fù)用之前的連接發(fā)送剩下的請求,但代理不認為這個連接上還會有其他請求,請求被忽略。這樣,瀏覽器會一直處于掛起狀態(tài),直到連接超時。
這個問題最根本的原因是代理服務(wù)器轉(zhuǎn)發(fā)了禁止轉(zhuǎn)發(fā)的 Header。但是要升級所有老舊的代理也不是件簡單的事,所以瀏覽器廠商和代理實現(xiàn)者協(xié)商了一個變通的方案:首先,顯式給瀏覽器設(shè)置代理后,瀏覽器會把請求頭中的 Connection 替換為 Proxy-Connetion。這樣,對于老舊的代理,它不認識這個 Header,會繼續(xù)發(fā)給服務(wù)器,服務(wù)器也不認識,代理和服務(wù)器之間不會建立持久連接(不能正確處理 Connection 的都是 HTTP/1.0 代理),服務(wù)器不返回 Keep-Alive,代理和瀏覽器之間也不會建立持久連接。而對于新代理,它可以理解 Proxy-Connetion,會用 Connection 取代無意義的 Proxy-Connection,并將其發(fā)送給服務(wù)器,以收到預(yù)期的效果。
顯然,如果瀏覽器并不知道連接中有老舊代理的存在,或者在老舊代理任意一側(cè)有新代理的情況下,這種方案仍然無濟于事。所以有時候服務(wù)器也會選擇徹底忽略 HTTP/1.0 的 Keep-Alive 特性:對于 HTTP/1.0 請求,從不使用持久連接,也從不返回 Keep-Alive。
最后
通過上面的內(nèi)容可以看到,瀏覽器對代理請求頭的修改,都是為了盡可能的兼容網(wǎng)絡(luò)中各種不規(guī)范的中轉(zhuǎn)設(shè)備,使網(wǎng)絡(luò)更健壯。
最后再提一句,用 Fiddler 和其它工具查看同一個請求頭,會發(fā)現(xiàn) Fiddler 顯示的是 Connection,而其它工具顯示的是 Proxy-Connection。這是因為大部分情況下,F(xiàn)iddler 會把 Proxy-Connection 換回 Connection 來顯示,只是展現(xiàn)上的差別而已。