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