要使用 HttpURLConnection,最好對一些基礎概念有所認識,比如 TCP/IP 協議,HTTP 報文, Socket 等。
先談一些我的認識,有可能不完全正確:
- Socket 應該是 TCP 協議層的概念,如果要使用 Socket 直接通信,需要使用遠程地址和端口號。其中,端口號根據具體的協議而不同,比如 HTTP 協議默認使用的端口號為 80/tcp。
- HttpURLConnection 是在底層連接上的一個請求,最終也是通過 Socket 連接網絡,所謂的 underlaying Socket。本文結尾我也會附上相關帖子連接。但是使用 HttpURLConnection 不需要我們專門去處理遠程地址和端口號。
- HttpURLConnection 只是一個抽象類,只能通過 url.openConection() 方法創建具體的實例。嚴格來說,openConection() 方法返回的是 URLConnection 的子類。根據 url 對象的不同,如可能不是 http:// 開頭的,那么 openConection() 返回的可能就不是 HttpURLConnection。
- HttpURLConnection 的 connect() 和 disconnect() 方法有必要特別強調一下,我會在下文使用到的地方詳細說明。
我在測試 HttpURLConnection 的時候,是分別使用 HTTP 的 GET 和 POST 方法發送消息到 http://ip.taobao.com//service/getIpInfo.php 查詢 IP 地址歸屬地。http://ip.taobao.com/instructions.php 是 GET 方法接口說明。
下面來具體說一下 HttpURLConnection 的使用步驟。
-
獲得 HttpURLConnection 對象
// 如果使用 POST 方法 URL url = new URL("http://ip.taobao.com//service/getIpInfo.php"); // 如果打算使用 GET 方法 //URL url = new URL("http://ip.taobao.com/service/getIpInfo.php?ip=xxx.xxx.xxx.xxx"); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-
設置請求屬性
在連接到遠程資源(可以簡單理解為遠端服務器,但是這么說不準確)之前,可以設置一些 HttpURLConnection 的屬性。// 設置連接超時時間 connection.setConnectTimeOut(15000); // 設置讀取超時時間 connection.setReadTimeOut(15000); // 設置請求參數,即具體的 HTTP 方法 connection.setRequestMethod("POST"); // 添加 HTTP HEAD 中的一些參數,可參考《Java 核心技術 卷II》 connection.setRequestProperty("Connection", "Keep-Alive"); // 設置是否向 httpUrlConnection 輸出, // 對于post請求,參數要放在 http 正文內,因此需要設為true。 // 默認情況下是false; connection.setDoOutput(true); // 設置是否從 httpUrlConnection 讀入,默認情況下是true; connection.setDoInput(true);
這些屬性的設置要在 connect() 之前完成。如果對 HTTP 包信息的結構有很好的理解,有助于理解這些方法。
setDoOutput() 方法是為了下面 getOutputStream();
setDoInput() 方法是為了下面 getInputStream()。
按照我在手機上測試,getOutputStream 和 getInputStream 內部都會隱式的調用 connect()。不過這只是我手機上的環境,嚴謹的來講,我覺得還是應該自己顯示的調用 conect()。(多次調用 connect(),后面的調用自動忽略) -
調用 connect() 連接遠程資源
connection.connect();
這會與服務器建立 Socket 連接,而連接以后,連接屬性就不可以再修改;但是可以查詢服務器返回的頭信息了(header information)。
connect 成功手機上 logcat 會打印相關信息,包括目標 IP 地址。我是用魅族做的測試,其他品牌理論上也應該會打印。 -
利用 getOutputStream() 傳輸 POST 消息
說明一下,POST 消息才需要寫數據,GET 不需要。BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8")); writer.write("ip=xxx.xxx.xxx.xxx"); writer.flush(); writer.close();
上面提到過,getOutputStream 會隱式的調用 connect()。
這里要注意的,主要是 HTTP 傳輸的消息要使用 URL UTF-8 編碼,英文字母、數字和部分符號保持不變,空格編碼成'+'。其他字符編碼成 "%XY" 形式的字節序列,特別是中文字符,不能直接傳輸。可以考慮使用
URLEncoder.encode(string, "UTF-8") 方法。 -
查詢服務器頭信息
理論上,connect() 以后就可以查詢服務器返回的頭信息了。并且,getOutputStream 里面會隱式調用 connect()。
但是,查詢服務器消息要在寫完所有要傳輸的數據以后。
如果 getResponseCode 或者 getResponseMessage 以后,是不能向 outputStream 寫消息的,報錯為:cannot write request body after response has been read
這兩個方法內部都調用了 getInputStream()。
因為有資料說,getInputStream() 的時候才會真正把 outputStream 里面的消息發出去。想想,這么做是有道理的:這樣就允許我們關閉 outputStream 后重新打開,并且補充數據。這么理解的話,getResponseCode 內部調用了 getInputStream,導致 outputStream 已經發送;而一個 HttpURLConnection 只能發送一個請求,所以就不能再向 outputStream 寫數據,否則就等于傳輸了兩個消息。
我沒有在手機上安裝抓手機報文的工具,所以沒有直接驗證。
實際使用時,肯定是先通過 outputStream 傳輸數據,然后查詢服務器的返回信息,所以 outputStream 消息到底是什么時候發送出去的,我們不需要太關心。
查詢頭信息的方法有一下幾個:
// 這兩個方法結合,可以查詢所有消息頭字段
public String getHeaderFieldKey (int n)
public String getHeaderField(int n)// 返回一個包含消息頭所有字段的標準 map 對象
public Map<String,List<String>> getHeaderFields()// 為了方便使用,以下方法可以查詢各標準字段
public String getContentType()
public int getContentLength()
public String getContentEncoding()
public long getDate()
public long getExpiration()
public long getLastModified() -
利用 getInputStream() 訪問資源數據
使用 getInputStream() 方法獲取一個輸入流用以讀取信息(這個輸入流與 URL 類中的 openStream 方法所返回的流相同)。另一個方法 getContent 在實際操作中并不是很有用。由標準內容類型(比如 text/plain 和 image/gif)所返回的對象需要使用 com.sun 層次結構中的類來進行處理。也可以注冊自己的內容處理器。
---《Java 核心技術 卷II》,CH3 網絡,使用 URLConnection 獲取信息private String convertStreamToString() { InputStream inputStream = connection.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream )); StringBuffer sb = new StringBuffer(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line + "\n"); } String reponse = sb.toString(); return reponse; }
-
關閉 HttpURLConnection
本身要 HttpURLConnection 是很簡單的,調用connection.disconnect()
就可以了。
這里是想說明一下,是否需要關閉,應該根據實際需要來。
當 HttpURLConnection 是 "Connection: close " 模式,那么關閉 inputStream 后就會自動斷開連接。
當 HttpURLConnection 是 "Connection: Keep-Alive" 模式,那么關閉 inputStream 后,并不會斷開底層的 Socket 連接。這樣的好處,是當需要連接到同一服務器地址時,可以復用該 Socket。這時如果要求斷開連接,就可以調用connection.disconnect()
了。
當然,HttpURLConnection 連接到底是不是 Keep-Alive 模式,除了 HttpURLConnection 請求設置為 Keep-Alive 外 (http 1.0中默認是關閉的,http 1.1中默認啟用Keep-Alive),也需要服務器支持 Keep-Alive,才可以真正建立 Keep-Alive 連接。// 連接 和 斷開連接 的 log,IP 地址為手機 IP I/System.out: [socket][/192.168.1.101:60330] connected I/System.out: close [socket][/192.168.1.101:60330]
-
補充一點
在我測試http://ip.taobao.com//service/getIpInfo.php 的時候,服務器一直不能正常返回 IP 地址對應的信息。最后發現,是淘寶服務器故意不響應我們這樣非瀏覽器發起的 IP 查詢請求。所以我還設置了 HttpURLConnection 的如下屬性,偽裝成瀏覽器,當然,是在 connect() 之前。connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.7 Safari/537.36");
調試聯網程序的時候,出錯有時候很難說是哪里的問題,用抓包軟件分析是很有必要的;檢查服務器的 ResponseCode 也是有必要的。
關于 HttpURLConnection 的學習,我覺得《Java 核心技術 卷II》寫的不錯。
我也參考了《Android 進階之光》和下面兩個鏈接。
關于 HTTP 的 GET 方法和 POST 方法,剛開始有些疑惑,也是看了《Java 核心技術 卷II》,以及下面兩個鏈接。
工作中經常用到的話,有必要專門學習一下 HTTP 協議和報文。