天天看點

HttpClient

讀者定位為具有 Java 和 Web 開發經驗的開發和設計人員。

讀者可以學習到關于 Cookie 的工作原理和 Cookie 協定的細節,以及在一個 HTTP 應用代理的場景下 Cookie 的管理和處理思想,并可以直接使用文中的代碼和思路,提高工作效率。

随着越來越多的系統移植到了 Web 上,HTTP 協定具有了比以前更廣泛的應用。不同的系統對 WEB 實作提出了不同的要求,基于 HTTP 協定的網絡應用正趨于複雜化和多元化。很多應用需要把使用者請求的頁面進行處理後再傳回給使用者,比如頁面關鍵字過濾,頁面内容緩存、内容搜尋、頁面翻譯等等。這些應用在實際效果上類似于一個 HTTP 應用代理:它們首先接受使用者的請求,根據使用者請求的 URL 去真正的目标伺服器取回目标頁面,再根據不同應用的要求做出相應處理後傳回給使用者。這樣使用者直接面對的就是這個

HTTP 應用代理,而通過它與其他頁面進行互動。Cookie 或 Session 技術的應用,解決了 HTTP 協定的一個問題 -- 無法保持客戶狀态,是以它現在被廣泛應用于各種 Web 站點中。上面提到的那些應用如果不能處理好 Cookie 和 Session 的傳遞、更新和廢除等問題,就會極大的限制它們所能處理站點的範圍,是以如何在 HTTP 應用代理中正确處理 Cookie,成為一個必須解決的問題。本文結合在頁面翻譯(Machine Translation System)項目中對于 Cookie 的處理方法,探讨一下這方面的解決方案。

<a>MTS 項目簡介及讨論前提</a>

Machine Translation System(以下簡稱 MTS)是一個線上實時頁面翻譯系統,為使用者線上提供把英文頁面翻譯成其他 9 種語言的服務。使用者通過向 MTS 系統送出一個類似下面的 URL 使用此服務,其中參數 url 指明了使用者所需要翻譯的目标位址,參數 language 指明了所需翻譯成的目智語言,www.mts.com 是假想中提供 MTS 服務的站點。

<a href="http://www.mts.com/translate?url=http://www.ibm.com/&amp;language=French">HTTP://www.mts.com/translate?url=http://www.ibm.com/&amp;language=French</a>

一個完整的 MTS 系統處理過程可以分解成以下幾個步驟:

使用者向 MTS 送出合适的 URL。

MTS 在接到使用者的請求後,解析出使用者需要翻譯的目标位址和目智語言,根據使用者請求的目标位址,把請求轉發到目标伺服器。

MTS 接受來自目标伺服器的應答,包括頁面資訊和 HTTP 頭資訊。

MTS 在确定得到正确的目标頁面後,把頁面内容送入 WebSphere Translation Server 進行翻譯。

把翻譯後的頁面連同修改後的 HTTP 頭資訊送出給使用者。

<a>MTS 邏輯圖</a>

當然,這其中涉及到很多的應用處理。比如與各種 HTTP/HTTPS 站點建立聯結、根據 HTTP 頭資訊進行頁面跳轉和錯誤處理、為始終保持使用者在翻譯模式下而對目标的 HTML 頁面進行分析和修改,根據系統設定對某些 DNT(Do Not Translate)的頁面進行過濾和跳轉,當然還有對 Cookie 的處理等等。其他問題跟這篇文章關聯不大,我們重點讨論在這種情況下的 Cookie 處理。Cookie 跟随目标伺服器的 HTTP 頭資訊被 MTS 接收到,經過

MTS 整理之後發給用戶端浏覽器。MTS 在接到下一次使用者對同一個站點的翻譯請求時,再把從用戶端得到的 Cookie 發送給目标伺服器。

在以上的場景中,MTS 充當的作用類似于一種 HTTP 應用代理伺服器,它代替使用者取得目标頁面,并在作出相應處理後再送出給使用者。當然,這種代理伺服器不需要使用者修改浏覽器的代理伺服器參數或者網絡配置,而隻是簡單的在浏覽器的位址欄中輸入一個 MTS 能夠識别的 URL 即可。此篇文章也是在這樣一個應用場景的基礎上,展開對 HTTP 應用代理伺服器如何處理 Cookie 的讨論。

<a href="http://www.ibm.com/developerworks/cn/java/j-cookie/#ibm-pcon">回頁首</a>

<a>問題的産生</a>

在 MTS 系統中,目标伺服器的 Cookie 在兩個地方會産生問題。當 MTS 接收目标伺服器應答的時候,Cookie 随着 HTTP 頭資訊被 MTS 接收到的。這時候目标伺服器認為 MTS 就是最終客戶,是以它賦予了 Cookie 與目标伺服器相符的屬性。而如果 MTS 把這些 Cookie 原封不動的儲存在 HTTP 頭資訊中,傳給真正的最終使用者的話,使用者的浏覽器會因為這些 Cookie 不合法而忽略它們。同理,當 Cookie 從浏覽器端傳回目标伺服器的時候,也會遇到相同的問題。是以有必要對

Cookie 進行一些處理,以保證使用者的浏覽器能真正識别和利用這些 Cookie。

但是為何使用者浏覽器無法識别從目标伺服器傳過來的原始 Cookie 呢?這是因為出于安全性的考慮,Cookie 規範制定的時候對 Cookie 的産生和接受設定了一些嚴格的規範,不符合這些規範的 Cookie,浏覽器和伺服器都将予以忽略。下面我們從 Cookie 規範入手進行介紹。

<a>Cookie 的規範介紹</a>

目前有以下幾種 Cookie 規範:

Netscape cookie 草案:是最早的 cookie 規範,基于 rfc2109。盡管這個規範與 rc2109 有較大的差别,但是很多伺服器都與之相容。

rfc2109, 是 w3c 釋出的第一個官方 cookie 規範。理論上講,所有的伺服器在處理 cookie( 版本 1) 時,都要遵循此規範。遺憾的是,這個規範太嚴格了,以緻很多伺服器不正确的實施了該規範或仍在使用 Netscape 規範。

rfc2965 規範定義了 cookie 版本 2,并說明了 cookie 版本 1 的不足。

rfc2965 規範的使用,目前并不多。rfc2109 規範相應要嚴格得多,在實際應用上,并不是所有的浏覽器和 Web 伺服器都嚴格遵守。是以相比較而言,Netscape cookie 草案倒是一個比較簡潔和被廣泛支援的 Cookie 規範,是以我們在這裡以 Netscape cookie 草案為基礎進行讨論,對于其他兩種規範,我們的讨論和代碼具有相同的意義。關于 Netscape cookie 草案的細節,大家可以參照 Netscape 官方站點,這裡我們列舉一些和我們讨論有關的内容。

根據 Netscape cookie 草案的描述,Cookie 是 Web 伺服器向使用者的浏覽器發送的一段 ASCII 碼文本。一旦收到 Cookie,浏覽器會把 Cookie 的資訊片斷以"名 / 值"對 (name-value pairs) 的形式儲存儲存在本地。這以後,每當向同一個 Web 伺服器請求一個新的文檔時,Web 浏覽器都會發送之站點以前存儲在本地的 Cookie。建立 Cookie 的最初目的是想讓 Web 伺服器能夠通過多個 HTTP

請求追蹤客戶。有些複雜的網絡應用需要在不同的網頁之間保持一緻,它們需要這種會話狀态的保持能力。

浏覽器與 Web 伺服器通過 HTTP 協定進行通訊,而 Cookie 就是儲存在 HTTP 協定的請求或者應答頭部(在 HTTP 協定中,資料包括兩部分,一部分是頭部,由一些名值對構成,用來描述要被傳輸資料的一些資訊。一部分是主體 (body),是真正的資料(如 HTML 頁面等))進行傳送的。

在 HTML 文檔被發送之前,Web 伺服器通過傳送 HTTP 標頭中的 Set-Cookie 消息把一個 cookie 發送到使用者的浏覽器中。下面是一個遵循 Netscape cookie 草案的完整的 Set-Cookie 頭:

Set-Cookie 的每個屬性解釋如下:

Customer=huangxp 一個"名稱=值"對,把名稱 customer 設定為值"huangxp",這個屬性在 Cookie 中必須有。

path=/foo 控制哪些通路能夠觸發 cookie 的發送。如果沒有指定 path,cookie 會在所有對此站點的 HTTP 傳送時發送。如果 path=/directory,隻有通路 /directory 下面的網頁時,cookie 才被發送。在這個例子中,使用者在通路目錄 /foo 下的内容時,浏覽器将發送此 cookie。如果指定了 path,但是 path 與目前通路的 url 不符,則此 cookie 将被忽略。

domain=.ibm.com 指定 cookie 被發送到哪台計算機上。正常情況下,cookie 隻被送回最初向使用者發送 cookie 的計算機。在這個例子中,cookie 會被發送到任何在 .ibm.com 域中的主機。如果 domain 被設為空,domain 就被設定為和提供 cookie 的 Web 伺服器相同。如果 domain 不為空,并且它的值又和提供 cookie 的 Web 伺服器域名不符,這個 Cookie 将被忽略。

expires= Wednesday, 19-OCT-05 23:12:40 GMT 指定 cookie 失效的時間。如果沒有指定失效時間,這個 cookie 就不會被寫入計算機的硬碟上,并且隻持續到這次會話結束。

secure 如果 secure 這個詞被作為 Set-Cookie 頭的一部分,那麼 cookie 隻能通過安全通道傳輸(目前即 SSL 通道)。否則,浏覽器将忽略此 Cookie。

一旦浏覽器接收了 cookie,這個 cookie 和對遠端 Web 伺服器的連續請求将一起被浏覽器發送。例如 前一個 cookie 被存入浏覽器并且浏覽器試圖請求 URL http://www.ibm.com/foo/index.html 時,下面的 HTTP 標頭就被發送到遠端的 Web 伺服器。

GET /foo/index.html HTTP/1.0

Cookie:customer=huangxp

<a>一次典型的網絡浏覽過程</a>

在了解了 Cookie 協定的一些基本内容之後,讓我們看看一次典型的網絡浏覽過程中浏覽器如何識别和處理 Cookie:

浏覽器對于 Web 伺服器應答標頭中 Cookie 的操作步驟:

1. 從 Web 伺服器的應答標頭中提取所有的 cookie。

2. 解析這些 cookie 的組成部分(名稱,值,路徑等等)。

3. 判定主機是否允許設定這些 cookie。允許的話,則把這些 Cookie 存儲在本地。

浏覽器對 Web 伺服器請求標頭中所有的 Cookie 進行篩選的步驟:

1. 根據請求的 URL 和本地存儲 cookie 的屬性,判斷那些 Cookie 能被發送給 Web 伺服器。

2. 對于多個 cookie,判定發送的順序。

3. 把需要發送的 Cookie 加入到請求 HTTP 標頭中一起發送。

<a>由 MTS 代理的網絡浏覽過程</a>

以上我們了解了在一個典型的浏覽器與 Web 伺服器互動的時候,Cookie 的傳遞過程。下面我們将看到,如果在 MTS 代理網絡浏覽的過程中,不對 Cookie 進行修改,上面的 Cookie 傳遞過程将無法實作。

1. 假設使用者希望把 http://www.ibm.com/foo/index.html 頁面翻譯成法文,應該使用如下的 url 對 MTS 送出請求

http://www.mts.com/translate?url=http://www.ibm.com/foo/index.html&amp;language=French

2. MTS 接收使用者的請求,連接配接遠端目标伺服器 http://www.ibm.com/foo/index.html。目标伺服器做出應答,傳回 HTTP 頭和 HTML 頁面内容。其中,典型的 HTTP 頭内容如下:

3. MTS 不對 Set-Cookie 後的内容作任何處理,直接把它加到使用者浏覽器的應答頭上發送給浏覽器。

4. 浏覽器将從 Set-Cookie 中解析出 domain 和 path 的值,分别是 .ibm.com 和 /foo,并與請求的 url:http://www.mts.com/translate?url=http://www.ibm.com/foo/index.html&amp;language=French 進行比較。請求 url 的 domain 是 www.mts.com,path 是 /,與 Set-Cookie 中的屬性不符,是以浏覽器将忽略此

Cookie。

另外,在浏覽器發送 Cookie 的時候也會遇到同樣的問題,同樣如上例,如果浏覽器裡本來已經存儲了 http://www.ibm.com/foo/ 的 Cookie,但由于使用者要通過 MTS 通路此站點,浏覽器經不會把已經存儲的 Cookie 上轉到 MTS 中,MTS 也就無法把之傳遞到 http://ibm.com/foo/ 上。

基于上面 Cookie 規範的介紹和例證,我們能看出,浏覽器在接受某一個站點的 Cookie 的時候,需要檢查 Cookie 的參數 domain、path、secure,看是否與目前的站點和 URL 相符,如果不符的話,就會忽略。另一方面。浏覽器在上傳 Cookie 的時候,也會根據目前所通路站點的屬性,上傳相關的 Cookie,而其他的 Cookie 則不予上傳。

至此,我們讨論了需要修改 Cookie 的根本原因在于 Cookie 規範的限制。下面我們讨論兩種解決問題的思路。

<a>解決問題的兩種思路</a>

Cookie 的存在是要解決 HTTP 協定本身先天的缺陷 - 無狀态性,它為使用者儲存了一些需要的狀态資訊。是以我們解決此問題的最本質的出發點,也就是找到一種途徑能為使用者儲存 Cookie 所提供使用者狀态資訊,實際上就是 Name/Value 對。

<a>思路一</a>

第一種思路就是修改目标伺服器取得的 Cookie,使之符合 MTS 站點的屬性,然後作為 MTS 站點的 Cookie 存儲到使用者的浏覽器中去。當然,這種修改必須保留原始 Cookie 的所有屬性值,當以後通路同一個目标伺服器的時候,MTS 能根據儲存的屬性值還原出原始 Cookie,然後進行送出。

具體到屬性值的儲存位置,沒有太多選擇的餘地,實際上,domain,path,secure,expires 這幾個屬性都無法利用,隻有利用 name=value 這一屬性對。我們的做法是創造一個新的 Cookie,把原始 Cookie 的 domain,path 的值與 name 值進行編碼,用分隔符附加在 Name 值的後面,符值給新的 Cookie。這樣做也同時避免了不同目标伺服器如果出現同名的 Cookie,将會互相覆寫的情況(Cookie 規範裡面也規定了,用戶端以

domain,path,name 作為 Cookie 的唯一标示)。而原始 Cookie 的 secure 和 expires 值,直接符給新的 Cookie,新 Cookie 的 domain 和 path 設成預設值,這樣,新 Cookie 就可以被浏覽器正常接受。由于浏覽器接受的所有 Cookie 的 domain 和 path 值都一樣,是以每次使用者對 MTS 提出請求時,浏覽器都會把所有與 MTS 站點相關的 Cookie 上傳,是以,MTS 還需要還原原始的 Cookie,過濾掉與目标伺服器不相幹的

Cookie,然後上傳有用的 Cookie。

這種思路的優點在于 Cookie 存儲在用戶端,可以做到長期存儲,浏覽器自己根據 Cookie 的 expires 值做出判斷,省掉很多開發的麻煩。缺點是轉換的過程相對較複雜。另外還有一個缺點,也是由于 Cookie 規範的限制所造成的。Cookie 規範對于一個浏覽器同時能夠存儲的 Cookie 數量作出了規定。

總共 300 個 cookie

每個 Cookie 4 K 的存儲容量

每一個 domain 或者 server 20 個 cookie。

以上是浏覽器所應達到的最小存儲數量,超出這個限制,浏覽器應該自動按照最少最近被使用的原則删除超出得 Cookie。由于使用者有可能通過 MTS 這一個網站翻譯大量的目标伺服器,是以浏覽器存儲在 MTS 的 domain 下的 cookie 數量就很有可能超過 20 個,這時候就會導緻某些 Cookie 被删除。一般這也不會造成太大問題,因為規範是要求浏覽器删除最少最近被使用的 Cookie,但我們在實際測試當中發現有些浏覽器并不遵守這樣的規範,而是删除最新的

Cookie,這就将導緻使用者很大的不便。

<a>思路二</a>

第二種思路在于把原始的 Cookie 組織成 dataBean,存儲到使用者的 Session 當中去。這樣,在使用者端隻需要存儲一個 SessionID 的 Cookie,而不需要存儲所有目标伺服器的每一個 Cookie。另外,當接收到使用者的又一次翻譯請求時,再從 Session 當中取出所有的 dataBean,逐一進行分析,找出與使用者所請求的目标伺服器相符的原始 Cookie,進行送出。

這種思路可以克服上一種思路中 Cookie 超過标準數量時的缺陷,而且不需編碼儲存原始的 Cookie 屬性值,減少了程式的複雜度。缺點是需要程式員自己處理 expires。而且由于是把 Cookie 存儲在 Session 中,一旦 Session 失效,所有 Cookie 都将被删除,是以,無法儲存那些長期的 Cookie。

總之,兩種思路各有利弊,在實際應用當中要權衡考慮。下面我們針對兩種思路進行技術實作,分别對應方案一和方案二。

由于 MTS 需要與目标伺服器連接配接,遵循 HTTP 協定讀取和傳回 Cookie,但是如果用 JDK 中的 java.net.URLConnection 處理 Cookie 将非常不友善,是以我們使用 HTTPClient 來處理與目标伺服器的連接配接。

<a>方案一:Cookie 存儲在浏覽器端</a>

使用者每發起一次新的請求,浏覽器在檢查完本地存儲 Cookie 的有效性後,會把所有由 MTS 産生的有效 Cookie 附加在請求頭裡送到 MTS。MTS 接受到用戶端的翻譯請求後,從 Request 中提取出所有的 Cookie,還原後根據目标伺服器的 domain 和 path 進行過濾。産生所有與目标伺服器相關的 Cookie。

接下來,需要把 Cookie 送到目标伺服器中。我們使用 HTTPClient 與目标伺服器連接配接。HTTPClient 在與目标伺服器連接配接以後,允許伺服器設定 Cookie 并在需要的時候自動将 Cookie 傳回伺服器,也支援手工設定 Cookie 後發送到伺服器端。但是,由于如何處理 cookie 有幾個規範互相沖突:Netscape Cookie 草案、RFC2109、RFC2965,而且還有很大數量的軟體商的 Cookie 實作不遵循任何規範。

為了處理這種狀況,需要把 HttpClient 設定成 Cookie 相容模式,這樣可以最大限度的處理好各種 Cookie。下面的代碼把 Cookie 送到目标伺服器。

MTS 把請求和 Cookie 送出後,繼續接收目标伺服器的應答,讀取傳回的原始 Cookie,并轉換成可以存儲在使用者浏覽器端的 Cookie。下面的代碼将對原始 Cookie 的内容進行變換,保留 expires 和 secure 等項,把 domain 和 path 項編碼到 name 中去。

最後一步,把這些 Cookie 儲存到 response 裡,随 HTTP 應答頭傳回使用者浏覽器。并儲存在浏覽器中。

至此,我們已經完成了接收使用者請求,轉換 Cookie,發送到目标伺服器,接收目标伺服器的原始 Cookie,并儲存在客戶浏覽器的整個處理過程。

<a>方案二:Cookie 存儲在伺服器端</a>

在此種方案中,目标伺服器傳回給 MTS 的 Cookie 将被組織成 dataBean,存儲在使用者的 Session 中。是以,我們首先生成一個用來存儲 Cookie 的類 CookiesBean,根據它的特性,它可以繼承 ArraryList 類。此對象将存儲使用者通路目标伺服器時接收到的所有 Cookie,并提供與新接收到的 Cookie 融合的功能,同時能夠删除過期的 Cookie,更新同名的 Cookie。

當 MTS 接受到用戶端的翻譯請求後,會從 Session 中提取出所有的 dataBean,并得到存儲的所有 Cookie。如以下代碼:

MTS 在所有的存儲的 Cookie 中,檢查 Cookie 的 Domain、path 和 secure 的值,篩選出符合目标伺服器的 Cookie。

把 Cookie 送到目标伺服器的代碼與方案一基本一樣,在此忽略。

最後一步,需要把 Cookie 存儲到 Session 中。下面的代碼将從目标伺服器接受 Cookie,融入到 dataBean 中,并儲存到客戶的 Session 中。

至此,我們已經完成了在 Session 中儲存個目标伺服器所産生 Cookie 的整個處理過程。

<a>關于 Session 的考慮</a>

在研究完如何管理和傳遞 Cookie 之後,我們也需要研究一下 Session 的傳遞。因為目前大部分站點都在采用 Session 機制儲存使用者狀态資料,如果不能解決 Session 的傳遞問題,HTTP 應用代理伺服器的适用範圍同樣會大打折扣。

首先我們了解一下 Session 的實作機制。Session 是一種伺服器端的機制,伺服器使用一種類似于散清單的結構來儲存資訊。當程式需要為某個用戶端的請求建立一個 session 的時候,伺服器首先檢查這個用戶端的請求裡是否已包含了一個 session 辨別 - 稱為 session id,如果已包含一個 session id 則說明以前已經為此用戶端建立過 session,伺服器就按照 session id 把這個 session 檢索出來使用(如果檢索不到,可能會建立一個),session

id 的值應該是一個既不會重複,又不容易被找到規律以仿造的字元串。

儲存這個 session id 的方式之一就是采用 Cookie。一般這個 Cookie 的名字都類似于 SESSIONID。比如 WebSphere 對于 Web 應用程式生成的 Cookie:JSESSIONID= 0001HWF4iVD94pY8Cpbx6U4CXkf:10lro0398,它的名字就是 JSESSIONID。

儲存 session id 的其他方式還包括 URL 重寫和表單隐藏字段。這兩種方式都不需要代理伺服器作特殊處理。是以實際上,我們解決了 Cookie 的管理和傳遞的問題之後,也就解決了 Session 的管理和傳遞。

<a>結束語</a>

從上面的讨論中可以看出,由于 Cookie 本身的規範限制,HTTP 應用代理所必需面對的一個問題就是如何對 Cookie 進行正确的處理。本文對此提出了兩種解決思路并列出了實作代碼。對于 MTS 項目本身,我們使用的是第二種方案。開發人員在認識好 Cookie 本身的特性之後,參照本文的思路,根據自己系統的特點,也會找出更适宜的解決方案。

<a>參考資料</a>

Netscape Cookie Specification 對 Netscape Cookie 使用的特性進行了簡要的介紹。

RFC2965:HTTP State Management Mechanism 介紹了 HTTP 狀态管理機制

RFC2109 w3c 釋出的第一個官方 cookie 規範

RFC2616:Hypertext Transfer Protocol 超文本傳輸協定

Ronald Tschalr 開發了 HTTPClient,将其作為 URLConnection 的替代品。

Jakarta Regexp Apache 的開源項目,處理正規表達式的 java 包。