HttpURLConnection 使用總結

要使用 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 的使用步驟。

  1. 獲得 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();
    
  2. 設置請求屬性
    在連接到遠程資源(可以簡單理解為遠端服務器,但是這么說不準確)之前,可以設置一些 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(),后面的調用自動忽略)

  3. 調用 connect() 連接遠程資源

    connection.connect();
    

    這會與服務器建立 Socket 連接,而連接以后,連接屬性就不可以再修改;但是可以查詢服務器返回的頭信息了(header information)。
    connect 成功手機上 logcat 會打印相關信息,包括目標 IP 地址。我是用魅族做的測試,其他品牌理論上也應該會打印。

  4. 利用 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") 方法。

  5. 查詢服務器頭信息
    理論上,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()

  6. 利用 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;
    }
    
  7. 關閉 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]
    
  8. 補充一點
    在我測試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 進階之光》和下面兩個鏈接。

JDK中的URLConnection參數詳解

詳解HttpURLConnection

關于 HTTP 的 GET 方法和 POST 方法,剛開始有些疑惑,也是看了《Java 核心技術 卷II》,以及下面兩個鏈接。

99%的人都理解錯了HTTP中GET與POST的區別

GET和POST有什么區別?及為什么網上的多數答案都是錯的。

工作中經常用到的話,有必要專門學習一下 HTTP 協議和報文。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。