Cookie
— 作者 sunggsun @ 20:26
8、Cookies
HttpClient能自動管理cookie,包括允許伺服器設定cookie並在需要的時候自動將cookie返回伺服器,它也支援手工設定cookie後發送到伺服器端。不幸的是,對如何處理cookie,有幾個規範互相衝突:Netscape Cookie 草案, RFC2109, RFC2965,而且還有很大數量的軟體商的cookie實現不遵循任何規範. 為了處理這種狀況,HttpClient提供了策略驅動的cookie管理方式。HttpClient支援的cookie規範有:
Netscape cookie草案,是最早的cookie規範,基於rfc2109。儘管這個規範與rc2109有較大的差別,這樣做可以與一些伺服器相容。
rfc2109,是w3c發布的第一個官方cookie規範。理論上講,所有的伺服器在處理cookie(版本1)時,都要遵循此規範,正因如此,HttpClient將其設為預設的規範。遺憾的是,這個規範太嚴格了,以致很多伺服器不正確的實施了該規範或仍在作用Netscape規範。在這種情況下,應使用相容規範。
相容性規範,設計用來相容儘可能多的伺服器,即使它們並沒有遵循標準規範。當解析cookie出現問題時,應考慮採用相容性規範。
RFC2965規範暫時沒有被HttpClient支援(在以後的版本為會加上),它定義了cookie版本2,並說明了版本1cookie的不足,RFC2965有意有久取代rfc2109.
在HttpClient中,有兩種方法來指定cookie規範的使用,
HttpClient client = new HttpClient();
client.getState().setCookiePolicy(CookiePolicy.COMPATIBILITY);
這種方法設定的規範只對當前的HttpState有效,參數可取值CookiePolicy.COMPATIBILITY,CookiePolicy.NETSCAPE_DRAFT或CookiePolicy.RFC2109。
System.setProperty("apache.commons.httpclient.cookiespec", "COMPATIBILITY");
此法指的規範,對以後每個建立立的HttpState對象都有效,參數可取值"COMPATIBILITY","NETSCAPE_DRAFT"或"RFC2109"。
常有不能解析cookie的問題,但更換到相容規範大都能解決。
9、使用HttpClient遇到問題怎麼辦?
用一個瀏覽器訪問伺服器,以確認伺服器應答正常
如果在使代理,關掉代理試試
另找一個伺服器來試試(如果運行著不同的伺服器軟體更好)
檢查代碼是否按教程中講的思路編寫
設定log層級為debug,找出問題出現的原因
開啟wiretrace,來追蹤用戶端與伺服器的通訊,以確實問題出現在什麼地方
用telnet或netcat手工將資訊發送到伺服器,適合於猜測已經找到了原因而進行實驗時
將netcat以監聽方式運行,用作伺服器以檢查httpclient如何處理應答的。
利用最新的httpclient試試,bug可能在最新的版本中修複了
向郵件清單求協助
向bugzilla報告bug.
10、SSL
藉助Java Secure Socket Extension (JSSE),HttpClient全面支援Secure Sockets Layer (SSL)或IETF Transport Layer Security (TLS)協議上的HTTP。JSSE已經jre1.4及以後的版本中,以前的版本則需要手工安裝設定,具體過程參見Sun網站或本學習筆記。
HttpClient中使用SSL非常簡單,參考下面兩個例子:
HttpClient httpclient = new HttpClient();
GetMethod httpget = new GetMethod("https://www.verisign.com/");
httpclient.executeMethod(httpget);
System.out.println(httpget.getStatusLine().toString());
,如果通過需要授權的代理,則如下:
HttpClient httpclient = new HttpClient();
httpclient.getHostConfiguration().setProxy("myproxyhost", 8080);
httpclient.getState().setProxyCredentials("my-proxy-realm", " myproxyhost",
new UsernamePasswordCredentials("my-proxy-username", "my-proxy-password"));
GetMethod httpget = new GetMethod("https://www.verisign.com/");
httpclient.executeMethod(httpget);
System.out.println(httpget.getStatusLine().toString());
在HttpClient中定製SSL的步驟如下:
提供了一個實現了org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory介面的socket factory。這個 socket factory負責打一個到伺服器的連接埠,使用標準的或第三方的SSL函數庫,並進行象串連握手等初始化操作。通常情況下,這個初始化操作在連接埠被建立時自動進行的。
執行個體化一個org.apache.commons.httpclient.protocol.Protocol對象。建立這個執行個體時,需要一個合法的協議類型(如https),一個定製的socket factory,和一個預設的端中號(如https的443連接埠).
Protocol myhttps = new Protocol("https", new MySSLSocketFactory(), 443);
然後,這個執行個體可被設定為協議的處理器。
HttpClient httpclient = new HttpClient();
httpclient.getHostConfiguration().setHost("www.whatever.com", 443, myhttps);
GetMethod httpget = new GetMethod("/");
httpclient.executeMethod(httpget);
通過調用Protocol.registerProtocol方法,將此定製的執行個體,註冊為某一特定協議的預設的處理器。由此,可以很方便地定製自己的協議類型(如myhttps)。
Protocol.registerProtocol("myhttps",
new Protocol("https", new MySSLSocketFactory(), 9443));
...
HttpClient httpclient = new HttpClient();
GetMethod httpget = new GetMethod("myhttps://www.whatever.com/");
httpclient.executeMethod(httpget);
如果想用自己定製的處理器取代https預設的處理器,只需要將其註冊為"https"即可。
Protocol.registerProtocol("https",
new Protocol("https", new MySSLSocketFactory(), 443));
HttpClient httpclient = new HttpClient();
GetMethod httpget = new GetMethod("https://www.whatever.com/");
httpclient.executeMethod(httpget);
已知的限制和問題
持續的SSL串連在Sun的低於1.4JVM上不能工作,這是由於JVM的bug造成。
通過代理訪問伺服器時,非搶先認證( Non-preemptive authentication)會失敗,這是由於HttpClient的設計缺陷造成的,以後的版本中會修改。
遇到問題的處理
很多問題,特別是在jvm低於1.4時,是由jsse的安裝造成的。
下面的代碼,可作為最終的檢測手段。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import javax.net.ssl.SSLSocketFactory;
public class Test {
public static final String TARGET_HTTPS_SERVER = "www.verisign.com";
public static final int TARGET_HTTPS_PORT = 443;
public static void main(String[] args) throws Exception {
Socket socket = SSLSocketFactory.getDefault().
createSocket(TARGET_HTTPS_SERVER, TARGET_HTTPS_PORT);
try {
Writer out = new OutputStreamWriter(
socket.getOutputStream(), "ISO-8859-1");
out.write("GET / HTTP/1.1rn");
out.write("Host: " + TARGET_HTTPS_SERVER + ":" +
TARGET_HTTPS_PORT + "rn");
out.write("Agent: SSL-TESTrn");
out.write("rn");
out.flush();
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(), "ISO-8859-1"));
String line = null;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
} finally {
socket.close();
}
}
}
11、httpclient的多執行緒
使用多線程的主要目的,是為了實現並行的下載。在httpclient啟動並執行過程中,每個http協議的方法,使用一個HttpConnection執行個體。由於串連是一種有限的資源,每個串連在某一時刻只能供一個線程和方法使用,所以需要確保在需要時正確地分配串連。HttpClient採用了一種類似jdbc串連池的方法來管理串連,這個管理工作由 MultiThreadedHttpConnectionManager完成。
MultiThreadedHttpConnectionManager connectionManager =
new MultiThreadedHttpConnectionManager();
HttpClient client = new HttpClient(connectionManager);
此是,client可以在多個線程中被用來執行多個方法。每次調用HttpClient.executeMethod() 方法,都會去連結管理器申請一個串連執行個體,申請成功這個連結執行個體被簽出(checkout),隨之在連結使用完後必須歸還管理器。管理器支援兩個設定: maxConnectionsPerHost 每個主機的最大並行連結數,預設為2
maxTotalConnections 用戶端總並行連結最大數,預設為20
管理器重新利用連結時,採取早歸還者先重用的方式(least recently used approach)。
由於是使用HttpClient的程式而不是HttpClient本身來讀取應答包的主體,所以HttpClient無法決定什麼時間串連不再使用了,這也就要求在讀完應答包的主體後必須手工顯式地調用releaseConnection()來釋放申請的連結。
MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
HttpClient client = new HttpClient(connectionManager);
...
// 在某個線程中。
GetMethod get = new GetMethod("http://jakarta.apache.org/");
try {
client.executeMethod(get);
// print response to stdout
System.out.println(get.getResponseBodyAsStream());
} finally {
// be sure the connection is released back to the connection
// manager
get.releaseConnection();
}
對每一個HttpClient.executeMethod須有一個method.releaseConnection()與之匹配.
12、HTTP方法
HttpClient支援的HTTP方法有8種,下面分述之。
1、Options
HTTP方法Options用來向伺服器發送請求,希望獲得針對由請求URL(request url)標誌的資源在請求/應答的通訊過程可以使用的功能選項。通過這個方法,用戶端可以在採取具體行動之前,就可對某一資源決定採取什麼動作和/或以及一些必要條件,或者瞭解伺服器提供的功能。這個方法最典型的應用,就是用來擷取伺服器支援哪些HTTP方法。
HttpClient中有一個類叫OptionsMethod,來支援這個HTTP方法,利用這個類的getAllowedMethods方法,就可以很簡單地實現上述的典型應用。
OptionsMethod options = new OptionsMethod("http://jakarta.apache.org");
// 執行方法並做相應的異常處理
...
Enumeration allowedMethods = options.getAllowedMethods();
options.releaseConnection();
2、Get
HTTP方法GET用來取回請求URI(request-URI)標誌的任何資訊(以實體(entity)的形式),"get"這個單詞本意就是”擷取“的意思。如果請求URI指向的一個資料處理過程,那這個過程產生的資料,在應答中以實體的形式被返回,而不是將這個過程的代碼的返回。
如果HTTP包中含有If-ModifiedSince, If-Unmodified-Since, If-Match, If-None-Match, 或 If-Range等頭欄位,則GET也就變成了”條件GET“,即只有滿足上述欄位描述的條件的實體才被取回,這樣可以減少一些非必需的網路傳輸,或者減少為擷取某一資源的多次請求(如第一次檢查,第二次下載)。(一般的瀏覽器,都有一個臨時目錄,用來緩衝一些網頁資訊,當再次瀏覽某個頁面的時候,只下載那些修改過的內容,以加快瀏覽速度,就是這個道理。至於檢查,則常用比GET更好的方法HEAD來實現。)如果HTTP包中含有Range頭欄位,那麼請求URI指定的實體中,只有決定範圍條件的那部分才被取回來。(用過多線程下載工具的朋友,可能比較容易理解這一點)
這個方法的典型應用,用來從web伺服器下載文檔。HttpClient定義了一個類叫GetMethod來支援這個方法,用GetMethod類中getResponseBody, getResponseBodyAsStream 或 getResponseBodyAsString函數就可以取到應答包包體中的文檔(如HTML頁面)資訊。這這三個函數中,getResponseBodyAsStream通常是最好的方法,主要是因為它可以避免在處理下載的文檔之前緩衝所有的下載的資料。
GetMethod get = new GetMethod("http://jakarta.apache.org");
// 執行方法,並處理失敗的請求.
...
InputStream in = get.getResponseBodyAsStream();
// 利用輸入資料流來處理資訊。
get.releaseConnection();
對GetMethod的最常見的不正確的使用,是沒有將全部的應答主體的資料讀出來。還有,必須注意要手工明確地將連結釋放。
3、Head
HTTP的Head方法,與Get方法完全一致,唯一的差別是伺服器不能在應答包中包含主體(message-body),而且一定不能包含主體。使用這個方法,可以使得客戶無需將資源下載回就可就以得到一些關於它的基本資料。這個方法常用來檢查超鏈的可訪問性以及資源最近有沒有被修改。
HTTP的head方法最典型的應用,是擷取資源的基本資料。HttpClient定義了HeadMethod類支援這個方法,HeadMethod類與其它*Method類一樣,用 getResponseHeaders()取回頭部資訊,而沒有自己的特殊方法。
HeadMethod head = new HeadMethod("http://jakarta.apache.org");
// 執行方法,並處理失敗的請求.
...
// 取回應答包的頭欄位資訊.
Header[] headers = head.getResponseHeaders();
// 只取回最後修改日期欄位的資訊.
String lastModified = head.getResponseHeader("last-modified").getValue();
4、Post
Post在英文有“派駐”的意思,HTTP方法POST就是要求伺服器接受請求包中的實體,並將其作為請求URI的下屬資源。從本質上說,這意味著伺服器要儲存這個實體資訊,而且通常由伺服器端的程式進行處理。Post方法的設計意圖,是要以一種統一的方式實現下列功能:
對已有的資源做評註
將資訊發布到BBS、新聞群組、郵件清單,或類似的文章組中
將一塊資料,提交給資料處理進程
通過追加操作,來擴充一個資料庫
這些都操作期待著在伺服器端產生一定的“副作用”,如修改了資料庫等。
HttpClient定義PostMethod類以支援該HTTP方法,在httpclient中,使用post方法有兩個基本的步驟:為請求包準備資料,然後讀取伺服器來的應答包的資訊。通過調用 setRequestBody()函數,來為請求包提供資料,它可以接收三類參數:輸入資料流、名值對數組或字串。至於讀取應答包需要調用 getResponseBody* 那一系列的方法,與GET方法處理應答包的方法相同。
常見問題是,沒有將全部應答讀取(無論它對程式是否有用),或沒有釋放連結資源。