Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> Android 中 HttpURLConnection 使用詳解

Android 中 HttpURLConnection 使用詳解

編輯:Android資訊

認識Http協議

Android中發送http網絡請求是很常見的,要有GET請求和POST請求。一個完整的http請求需要經歷兩個過程:客戶端發送請求到服務器,然後服務器將結果返回給客戶端,如下圖所示:

這裡寫圖片描述

  • 客戶端->服務器
    客戶端向服務器發送請求主要包含以下信息:請求的Url地址、請求頭以及可選的請求體,打開百度首頁,客戶端向服務器發送的信息如下所示:這裡寫圖片描述

    • 請求URL(Request URL)
      上圖中的Request URL就是請求的Url地址,即https://www.baidu.com,該Url沒有附加其他的參數。其實可以通過?和&符向URL地址後面追加一系列的鍵值對參數,比如地址https://www.baidu.com/s?ie=utf-8&wd=Android,該Url包含兩個鍵值對,ie=utf-8,以及wd=Android,ie和wd是key,utf-8和Android分別是其對應的value,服務端可以獲取ie和wd所對應的value的值。由此我們可以看出,Url可以攜帶額外的數據信息。一般情況下,URL的長度不能超過2048個字符,即2KB,超過此限制的話服務器可能就不識別。
    • 請求頭(Request Headers)
      上圖中Request Headers部分就是請求頭,請求頭其實也是一些鍵值對,不過這些鍵值通常都是W3C定義了的一些標准的Http請求頭的名稱,請求頭包含了客戶端想告訴服務端的一些元數據信息,注意是元數據,而不是數據,比如請求頭User-Agent會告訴服務器這條請求來自於什麼浏覽器,再比如請求頭Accept-Encoding會告訴服務器客戶端支持的壓縮格式。除了這些標准的請求頭,我們還可以添加自定義的請求頭。
    • 請求體(Request Body)
      之前我們提到,URL的最大長度就是2048個字符,如果我們發送的數據很大,超過了2KB怎麼辦?我們可以將很大的數據放到請求體中,GET請求不支持請求體,只有POST請求才能設置請求體。請求體中可以放置任意的字節流,從而可以很方便地發送任意格式的數據,服務端只需要讀取該輸入流即可。
  • 服務器->客戶端
    服務器接收到客戶端發來的請求後,會進行相應的處理,並向客戶端輸出信息,輸出的信息包括響應頭和響應體。

    • 響應頭 (Response Headers)
      響應頭也是一些鍵值對,如下所示:
      這裡寫圖片描述響應頭包含了服務器想要告訴客戶端的一些元數據信息,注意不是數據,是元數據,比如通過響應頭Content-Encoding告訴客戶端服務器所采用的壓縮格式,響應頭Content-Type告訴客戶端響應體是什麼格式的數據,再比如服務端可以通過多個Set-Cookie響應頭向客戶端寫入多條Cookie信息,等等。剛剛提到的幾個請求頭都是W3C規定的標准的請求頭名稱,我們也可以在服務端向客戶端寫入自定義的響應頭。
    • 響應體 (Response Body)
      響應體是服務端向客戶端傳輸的實際的數據信息,本質就是一堆字節流,可以表示文本,也可以表示圖片或者其他格式的信息,如下所示:
      這裡寫圖片描述

GET vs POST

Http協議支持的操作有GET、POST、HEAD、PUT、TRACE、OPTIONS、DELETE,其中最最常用的還是GET和POST操作,下面我們看一下GET和POST的區別。

GET:

  • GET請求可以被緩存。
  • 我們之前提到,當發送鍵值對信息時,可以在URL上面直接追加鍵值對參數。當用GET請求發送鍵值對時,鍵值對會隨著URL一起發送的。
  • 由於GET請求發送的鍵值對時隨著URL一起發送的,所以一旦該URL被黑客截獲,那麼就能看到發送的鍵值對信息,所以GET請求的安全性很低,不能用GET請求發送敏感的信息(比如用戶名密碼)。
  • 由於URL不能超過2048個字符,所以GET請求發送數據是有長度限制的。
  • 由於GET請求較低的安全性,我們不應該用GET請求去執行增加、刪除、修改等的操作,應該只用它獲取數據。

POST:

  • POST請求從不會被緩存。
  • POST請求的URL中追加鍵值對參數,不過這些鍵值對參數不是隨著URL發送的,而是被放入到請求體中發送的,這樣安全性稍微好一些。
  • 應該用POST請求發送敏感信息,而不是用GET。
  • 由於可以在請求體中發送任意的數據,所以理論上POST請求不存在發送數據大小的限制。
  • 當執行增減、刪除、修改等操作時,應該使用POST請求,而不應該使用GET請求。

HttpURLConnection vs DefaultHttpClient

在Android API Level 9(Android 2.2)之前之能使用DefaultHttpClient類發送http請求。DefaultHttpClient是Apache用於發送http請求的客戶端,其提供了強大的API支持,而且基本沒有什麼bug,但是由於其太過復雜,Android團隊在保持向後兼容的情況下,很難對DefaultHttpClient進行增強。為此,Android團隊從Android API Level 9開始自己實現了一個發送http請求的客戶端類——–HttpURLConnection。

相比於DefaultHttpClient,HttpURLConnection比較輕量級,雖然功能沒有DefaultHttpClient那麼強大,但是能夠滿足大部分的需求,所以Android推薦使用HttpURLConnection代替DefaultHttpClient,並不強制使用HttpURLConnection。

但從Android API Level 23(Android 6.0)開始,不能再在Android中使用DefaultHttpClient,強制使用HttpURLConnection。

Demo介紹

為了演示HttpURLConnection的常見用法,我做了一個App,界面如下所示:

這裡寫圖片描述

主界面MainActivity有四個按鈕,分別表示用GET發送請求、用POST發送鍵值對數據、用POST發送XML數據以及用POST發送JSON數據,點擊對應的按鈕會啟動NetworkActivity並執行相應的操作。

NetworkActivity的源碼如下所示,此處先貼出代碼,後面會詳細說明。

package com.ispring.httpurlconnection;

import android.content.Intent;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class NetworkActivity extends AppCompatActivity {

    private NetworkAsyncTask networkAsyncTask = new NetworkAsyncTask();

    private TextView tvUrl = null;
    private TextView tvRequestHeader = null;
    private TextView tvRequestBody = null;
    private TextView tvResponseHeader = null;
    private TextView tvResponseBody = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_network);
        tvUrl = (TextView) findViewById(R.id.tvUrl);
        tvRequestHeader = (TextView) findViewById(R.id.tvRequestHeader);
        tvRequestBody = (TextView) findViewById(R.id.tvRequestBody);
        tvResponseHeader = (TextView) findViewById(R.id.tvResponseHeader);
        tvResponseBody = (TextView) findViewById(R.id.tvResponseBody);
        Intent intent = getIntent();
        if (intent != null && intent.getExtras() != null) {
            String networkAction = intent.getStringExtra("action");
            networkAsyncTask.execute(networkAction);
        }
    }

    //用於進行網絡請求的AsyncTask
    class NetworkAsyncTask extends AsyncTask<String, Integer, Map<String, Object>> {
        //NETWORK_GET表示發送GET請求
        public static final String NETWORK_GET = "NETWORK_GET";
        //NETWORK_POST_KEY_VALUE表示用POST發送鍵值對數據
        public static final String NETWORK_POST_KEY_VALUE = "NETWORK_POST_KEY_VALUE";
        //NETWORK_POST_XML表示用POST發送XML數據
        public static final String NETWORK_POST_XML = "NETWORK_POST_XML";
        //NETWORK_POST_JSON表示用POST發送JSON數據
        public static final String NETWORK_POST_JSON = "NETWORK_POST_JSON";

        @Override
        protected Map<String, Object> doInBackground(String... params) {
            Map<String,Object> result = new HashMap<>();
            URL url = null;//請求的URL地址
            HttpURLConnection conn = null;
            String requestHeader = null;//請求頭
            byte[] requestBody = null;//請求體
            String responseHeader = null;//響應頭
            byte[] responseBody = null;//響應體
            String action = params[0];//http請求的操作類型

            try {
                if (NETWORK_GET.equals(action)) {
                    //發送GET請求
                    url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet?name=孫群&age=27");
                    conn = (HttpURLConnection) url.openConnection();
                    //HttpURLConnection默認就是用GET發送請求,所以下面的setRequestMethod可以省略
                    conn.setRequestMethod("GET");
                    //HttpURLConnection默認也支持從服務端讀取結果流,所以下面的setDoInput也可以省略
                    conn.setDoInput(true);
                    //用setRequestProperty方法設置一個自定義的請求頭:action,由於後端判斷
                    conn.setRequestProperty("action", NETWORK_GET);
                    //禁用網絡緩存
                    conn.setUseCaches(false);
                    //獲取請求頭
                    requestHeader = getReqeustHeader(conn);
                    //在對各種參數配置完成後,通過調用connect方法建立TCP連接,但是並未真正獲取數據
                    //conn.connect()方法不必顯式調用,當調用conn.getInputStream()方法時內部也會自動調用connect方法
                    conn.connect();
                    //調用getInputStream方法後,服務端才會收到請求,並阻塞式地接收服務端返回的數據
                    InputStream is = conn.getInputStream();
                    //將InputStream轉換成byte數組,getBytesByInputStream會關閉輸入流
                    responseBody = getBytesByInputStream(is);
                    //獲取響應頭
                    responseHeader = getResponseHeader(conn);
                } else if (NETWORK_POST_KEY_VALUE.equals(action)) {
                    //用POST發送鍵值對數據
                    url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet");
                    conn = (HttpURLConnection) url.openConnection();
                    //通過setRequestMethod將conn設置成POST方法
                    conn.setRequestMethod("POST");
                    //調用conn.setDoOutput()方法以顯式開啟請求體
                    conn.setDoOutput(true);
                    //用setRequestProperty方法設置一個自定義的請求頭:action,由於後端判斷
                    conn.setRequestProperty("action", NETWORK_POST_KEY_VALUE);
                    //獲取請求頭
                    requestHeader = getReqeustHeader(conn);
                    //獲取conn的輸出流
                    OutputStream os = conn.getOutputStream();
                    //獲取兩個鍵值對name=孫群和age=27的字節數組,將該字節數組作為請求體
                    requestBody = new String("name=孫群&age=27").getBytes("UTF-8");
                    //將請求體寫入到conn的輸出流中
                    os.write(requestBody);
                    //記得調用輸出流的flush方法
                    os.flush();
                    //關閉輸出流
                    os.close();
                    //當調用getInputStream方法時才真正將請求體數據上傳至服務器
                    InputStream is = conn.getInputStream();
                    //獲得響應體的字節數組
                    responseBody = getBytesByInputStream(is);
                    //獲得響應頭
                    responseHeader = getResponseHeader(conn);
                } else if (NETWORK_POST_XML.equals(action)) {
                    //用POST發送XML數據
                    url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet");
                    conn = (HttpURLConnection) url.openConnection();
                    //通過setRequestMethod將conn設置成POST方法
                    conn.setRequestMethod("POST");
                    //調用conn.setDoOutput()方法以顯式開啟請求體
                    conn.setDoOutput(true);
                    //用setRequestProperty方法設置一個自定義的請求頭:action,由於後端判斷
                    conn.setRequestProperty("action", NETWORK_POST_XML);
                    //獲取請求頭
                    requestHeader = getReqeustHeader(conn);
                    //獲取conn的輸出流
                    OutputStream os = conn.getOutputStream();
                    //讀取assets目錄下的person.xml文件,將其字節數組作為請求體
                    requestBody = getBytesFromAssets("person.xml");
                    //將請求體寫入到conn的輸出流中
                    os.write(requestBody);
                    //記得調用輸出流的flush方法
                    os.flush();
                    //關閉輸出流
                    os.close();
                    //當調用getInputStream方法時才真正將請求體數據上傳至服務器
                    InputStream is = conn.getInputStream();
                    //獲得響應體的字節數組
                    responseBody = getBytesByInputStream(is);
                    //獲得響應頭
                    responseHeader = getResponseHeader(conn);
                } else if (NETWORK_POST_JSON.equals(action)) {
                    //用POST發送JSON數據
                    url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet");
                    conn = (HttpURLConnection) url.openConnection();
                    //通過setRequestMethod將conn設置成POST方法
                    conn.setRequestMethod("POST");
                    //調用conn.setDoOutput()方法以顯式開啟請求體
                    conn.setDoOutput(true);
                    //用setRequestProperty方法設置一個自定義的請求頭:action,由於後端判斷
                    conn.setRequestProperty("action", NETWORK_POST_JSON);
                    //獲取請求頭
                    requestHeader = getReqeustHeader(conn);
                    //獲取conn的輸出流
                    OutputStream os = conn.getOutputStream();
                    //讀取assets目錄下的person.json文件,將其字節數組作為請求體
                    requestBody = getBytesFromAssets("person.json");
                    //將請求體寫入到conn的輸出流中
                    os.write(requestBody);
                    //記得調用輸出流的flush方法
                    os.flush();
                    //關閉輸出流
                    os.close();
                    //當調用getInputStream方法時才真正將請求體數據上傳至服務器
                    InputStream is = conn.getInputStream();
                    //獲得響應體的字節數組
                    responseBody = getBytesByInputStream(is);
                    //獲得響應頭
                    responseHeader = getResponseHeader(conn);
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //最後將conn斷開連接
                if (conn != null) {
                    conn.disconnect();
                }
            }

            result.put("url", url.toString());
            result.put("action", action);
            result.put("requestHeader", requestHeader);
            result.put("requestBody", requestBody);
            result.put("responseHeader", responseHeader);
            result.put("responseBody", responseBody);
            return result;
        }

        @Override
        protected void onPostExecute(Map<String, Object> result) {
            super.onPostExecute(result);
            String url = (String)result.get("url");//請求的URL地址
            String action = (String) result.get("action");//http請求的操作類型
            String requestHeader = (String) result.get("requestHeader");//請求頭
            byte[] requestBody = (byte[]) result.get("requestBody");//請求體
            String responseHeader = (String) result.get("responseHeader");//響應頭
            byte[] responseBody = (byte[]) result.get("responseBody");//響應體

            //更新tvUrl,顯示Url
            tvUrl.setText(url);

            //更新tvRequestHeader,顯示請求頭
            if (requestHeader != null) {
                tvRequestHeader.setText(requestHeader);
            }

            //更新tvRequestBody,顯示請求體
            if(requestBody != null){
                try{
                    String request = new String(requestBody, "UTF-8");
                    tvRequestBody.setText(request);
                }catch (UnsupportedEncodingException e){
                    e.printStackTrace();
                }
            }

            //更新tvResponseHeader,顯示響應頭
            if (responseHeader != null) {
                tvResponseHeader.setText(responseHeader);
            }

            //更新tvResponseBody,顯示響應體
            if (NETWORK_GET.equals(action)) {
                String response = getStringByBytes(responseBody);
                tvResponseBody.setText(response);
            } else if (NETWORK_POST_KEY_VALUE.equals(action)) {
                String response = getStringByBytes(responseBody);
                tvResponseBody.setText(response);
            } else if (NETWORK_POST_XML.equals(action)) {
                //將表示xml的字節數組進行解析
                String response = parseXmlResultByBytes(responseBody);
                tvResponseBody.setText(response);
            } else if (NETWORK_POST_JSON.equals(action)) {
                //將表示json的字節數組進行解析
                String response = parseJsonResultByBytes(responseBody);
                tvResponseBody.setText(response);
            }
        }

        //讀取請求頭
        private String getReqeustHeader(HttpURLConnection conn) {
            //https://github.com/square/okhttp/blob/master/okhttp-urlconnection/src/main/java/okhttp3/internal/huc/HttpURLConnectionImpl.java#L236
            Map<String, List<String>> requestHeaderMap = conn.getRequestProperties();
            Iterator<String> requestHeaderIterator = requestHeaderMap.keySet().iterator();
            StringBuilder sbRequestHeader = new StringBuilder();
            while (requestHeaderIterator.hasNext()) {
                String requestHeaderKey = requestHeaderIterator.next();
                String requestHeaderValue = conn.getRequestProperty(requestHeaderKey);
                sbRequestHeader.append(requestHeaderKey);
                sbRequestHeader.append(":");
                sbRequestHeader.append(requestHeaderValue);
                sbRequestHeader.append("\n");
            }
            return sbRequestHeader.toString();
        }

        //讀取響應頭
        private String getResponseHeader(HttpURLConnection conn) {
            Map<String, List<String>> responseHeaderMap = conn.getHeaderFields();
            int size = responseHeaderMap.size();
            StringBuilder sbResponseHeader = new StringBuilder();
            for(int i = 0; i < size; i++){
                String responseHeaderKey = conn.getHeaderFieldKey(i);
                String responseHeaderValue = conn.getHeaderField(i);
                sbResponseHeader.append(responseHeaderKey);
                sbResponseHeader.append(":");
                sbResponseHeader.append(responseHeaderValue);
                sbResponseHeader.append("\n");
            }
            return sbResponseHeader.toString();
        }

        //根據字節數組構建UTF-8字符串
        private String getStringByBytes(byte[] bytes) {
            String str = "";
            try {
                str = new String(bytes, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return str;
        }

        //從InputStream中讀取數據,轉換成byte數組,最後關閉InputStream
        private byte[] getBytesByInputStream(InputStream is) {
            byte[] bytes = null;
            BufferedInputStream bis = new BufferedInputStream(is);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            BufferedOutputStream bos = new BufferedOutputStream(baos);
            byte[] buffer = new byte[1024 * 8];
            int length = 0;
            try {
                while ((length = bis.read(buffer)) > 0) {
                    bos.write(buffer, 0, length);
                }
                bos.flush();
                bytes = baos.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            return bytes;
        }

        //根據文件名,從asserts目錄中讀取文件的字節數組
        private byte[] getBytesFromAssets(String fileName){
            byte[] bytes = null;
            AssetManager assetManager = getAssets();
            InputStream is = null;
            try{
                is = assetManager.open(fileName);
                bytes = getBytesByInputStream(is);
            }catch (IOException e){
                e.printStackTrace();
            }
            return bytes;
        }

        //將表示xml的字節數組進行解析
        private String parseXmlResultByBytes(byte[] bytes) {
            InputStream is = new ByteArrayInputStream(bytes);
            StringBuilder sb = new StringBuilder();
            List<Person> persons = XmlParser.parse(is);
            for (Person person : persons) {
                sb.append(person.toString()).append("\n");
            }
            return sb.toString();
        }

        //將表示json的字節數組進行解析
        private String parseJsonResultByBytes(byte[] bytes){
            String jsonString = getStringByBytes(bytes);
            List<Person> persons = JsonParser.parse(jsonString);
            StringBuilder sb = new StringBuilder();
            for (Person person : persons) {
                sb.append(person.toString()).append("\n");
            }
            return sb.toString();
        }

    }
}

這個App是用來發送http請求的客戶端,除此之外,我還創建了一個JSP的WebProject作為服務端,用Servlet對客戶端發來的請求進行處理,Servlet的代碼如下所示:

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Enumeration;

@WebServlet(name = "MyServlet")
public class MyServlet extends HttpServlet {
    //GET請求
    private static final String NETWORK_GET = "NETWORK_GET";
    //用POST發送鍵值對
    private static final String NETWORK_POST_KEY_VALUE = "NETWORK_POST_KEY_VALUE";
    //用POST發送XML數據
    private static final String NETWORK_POST_XML = "NETWORK_POST_XML";
    //用POST發送JSON數據
    private static final String NETWORK_POST_JSON = "NETWORK_POST_JSON";

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String action = request.getHeader("action");
        //將輸入與輸出都設置為UTF-8編碼
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/plain;charset=UTF-8");
        //response.setHeader("content-type","text/plain;charset=UTF-8");
        if(NETWORK_GET.equals(action) || NETWORK_POST_KEY_VALUE.equals(action)){
            //對於NETWORK_GET和NETWORK_POST_KEY_VALUE,遍歷鍵值對,並將鍵值對重新寫回到輸出結果中
            Enumeration<String> parameterNames = request.getParameterNames();
            PrintWriter writer = response.getWriter();
            while(parameterNames.hasMoreElements()){
                String name = parameterNames.nextElement();
                String value = request.getParameter(name);
                if(request.getMethod().toUpperCase().equals("GET")){
                    //GET請求需要進行編碼轉換,POST不需要
                    value = new String(value.getBytes("ISO-8859-1"), "UTF-8");
                }
                writer.write(name + "=" + value + "\n");
            }
            writer.flush();
            writer.close();
        }else if(NETWORK_POST_XML.equals(action) || NETWORK_POST_JSON.equals(action)){
            //對於NETWORK_POST_XML和NETWORK_POST_JSON,將請求體重新寫入到響應體的輸出流中
            //通過request.getInputStream()得到http請求的請求體
            BufferedInputStream bis  = new BufferedInputStream(request.getInputStream());
            //通過response.getOutputStream()得到http請求的響應體
            BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
            byte[] buffer = new byte[1024 * 8];
            int length = 0;
            while ( (length = bis.read(buffer)) > 0){
                bos.write(buffer, 0, length);
            }
            bos.flush();
            bos.close();
            bis.close();
        }else{
            PrintWriter writer = response.getWriter();
            writer.write("非法的請求頭: action");
            writer.flush();
            writer.close();
        }
    }
}

發送GET請求

由於網絡請求耗時而且會阻塞當前線程,所以我們將發送http請求的操作都放到NetworkAsyncTask中,NetworkAsyncTask是繼承自AsyncTask。

點擊”GET”按鈕後,界面如下所示:

這裡寫圖片描述

GET請求是最簡單的http請求,其發送請求的代碼如下所示:

if (NETWORK_GET.equals(action)) {
    //發送GET請求
    url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet?name=孫群&age=27");
    conn = (HttpURLConnection) url.openConnection();
    //HttpURLConnection默認就是用GET發送請求,所以下面的setRequestMethod可以省略
    conn.setRequestMethod("GET");
    //HttpURLConnection默認也支持從服務端讀取結果流,所以下面的setDoInput也可以省略
    conn.setDoInput(true);
    //用setRequestProperty方法設置一個自定義的請求頭:action,由於後端判斷
    conn.setRequestProperty("action", NETWORK_GET);
    //禁用網絡緩存
    conn.setUseCaches(false);
    //獲取請求頭
    requestHeader = getReqeustHeader(conn);
    //在對各種參數配置完成後,通過調用connect方法建立TCP連接,但是並未真正獲取數據
    //conn.connect()方法不必顯式調用,當調用conn.getInputStream()方法時內部也會自動調用connect方法
    conn.connect();
    //調用getInputStream方法後,服務端才會收到請求,並阻塞式地接收服務端返回的數據
    InputStream is = conn.getInputStream();
    //將InputStream轉換成byte數組,getBytesByInputStream會關閉輸入流
    responseBody = getBytesByInputStream(is);
    //獲取響應頭
    responseHeader = getResponseHeader(conn);
}

上面的注釋寫的比較詳細了,此處對代碼進行一下簡單說明。

  • 我們在URL的後面添加了?name=孫群&age=27,這樣就相當於添加了兩個鍵值對,其形式如?key1=value1&key2=value2&key3=value3,在?後面添加鍵值對,鍵和值之間用=相連,鍵值對之間用&分隔,服務端可以讀取這些鍵值對信息。
  • HttpURLConnection默認就是用GET發送請求,當然也可以用conn.setRequestMethod(“GET”)將其顯式地設置為GET請求。
  • 通過setRequestProperty方法可以設置請求頭,既可以是標准的請求頭,也可以是自定義的請求頭,此處我們設置了自定義的請求頭action,用於服務端判斷請求的類型。
  • GET請求容易被緩存,我們可以用conn.setUseCaches(false)禁用緩存。
  • 通過調用connect方法可以讓客戶端和服務器之間建立TCP連接,建立連接之後不會立即傳輸數據,只是表示處於connected狀態了。該方法不必顯式調用,因為在之後的getInputStream方法中會隱式地調用該方法。
  • 調用HttpURLConnection的getInputStream()方法可以獲得響應結果的輸入流,即服務器向客戶端輸出的信息,需要注意的是,getInputStream()方法的調用必須在一系列的set方法之後進行。
  • 然後在方法getBytesByInputStream中,通過輸入流的read方法得到字節數組,read方法是阻塞式的,每read一次,其實就是從服務器上下載一部分數據,直到將服務器的輸出全部下載完成,這樣就得到響應體responseBody了。
  • 我們可以通過getReqeustHeader方法讀取請求頭,代碼如下所示:
    //讀取請求頭
    private String getReqeustHeader(HttpURLConnection conn) {            
        Map<String, List<String>> requestHeaderMap = conn.getRequestProperties();
        Iterator<String> requestHeaderIterator = requestHeaderMap.keySet().iterator();
        StringBuilder sbRequestHeader = new StringBuilder();
        while (requestHeaderIterator.hasNext()) {
            String requestHeaderKey = requestHeaderIterator.next();
            String requestHeaderValue = conn.getRequestProperty(requestHeaderKey);
            sbRequestHeader.append(requestHeaderKey);
            sbRequestHeader.append(":");
            sbRequestHeader.append(requestHeaderValue);
            sbRequestHeader.append("\n");
        }
        return sbRequestHeader.toString();
    }
  • 由上可以看出,以上方法主要還是調用了HttpURLConnection的方法getRequestProperty獲取請求頭,需要注意的是getRequestProperty方法執行時,客戶端和服務器之間必須還未建立TCP連接,即還沒有調用connect方法,在connected之後執行getRequestProperty會拋出異常,詳見https://github.com/square/okhttp/blob/master/okhttp-urlconnection/src/main/java/okhttp3/internal/huc/HttpURLConnectionImpl.java#L236

我們還可以通過getResponseHeader方法獲取響應頭,代碼如下所示:

    //讀取響應頭
    private String getResponseHeader(HttpURLConnection conn) {
        Map<String, List<String>> responseHeaderMap = conn.getHeaderFields();
        int size = responseHeaderMap.size();
        StringBuilder sbResponseHeader = new StringBuilder();
        for(int i = 0; i < size; i++){
            String responseHeaderKey = conn.getHeaderFieldKey(i);
            String responseHeaderValue = conn.getHeaderField(i);
            sbResponseHeader.append(responseHeaderKey);
            sbResponseHeader.append(":");
            sbResponseHeader.append(responseHeaderValue);
            sbResponseHeader.append("\n");
        }
        return sbResponseHeader.toString();
    }

通過方法getHeaderFieldKey可以獲得響應頭的key值,通過方法getHeaderField可以獲得響應頭的value值。

在後台的Servelt中將鍵值對信息重新原樣寫入到客戶端,服務端的代碼如下所示:

if(NETWORK_GET.equals(action) || NETWORK_POST_KEY_VALUE.equals(action)){
            //對於NETWORK_GET和NETWORK_POST_KEY_VALUE,遍歷鍵值對,並將鍵值對重新寫回到輸出結果中
            Enumeration<String> parameterNames = request.getParameterNames();
            PrintWriter writer = response.getWriter();
            while(parameterNames.hasMoreElements()){
                String name = parameterNames.nextElement();
                String value = request.getParameter(name);
                if(request.getMethod().toUpperCase().equals("GET")){
                    //GET請求需要進行編碼轉換,POST不需要
                    value = new String(value.getBytes("ISO-8859-1"), "UTF-8");
                }
                writer.write(name + "=" + value + "\n");
            }
            writer.flush();
            writer.close();
        }

在接收到服務端返回的數據後,在AsyncTask的onPostExecute方法中會將Url、請求頭、響應頭、響應體的值展示在UI上。

用POST發送鍵值對數據

點擊”POST KEY VALUE”按鈕,可以用POST發送鍵值對數據,界面如下所示:

這裡寫圖片描述

代碼如下所示:

if (NETWORK_POST_KEY_VALUE.equals(action)) {
    //用POST發送鍵值對數據
    url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet");
    conn = (HttpURLConnection) url.openConnection();
    //通過setRequestMethod將conn設置成POST方法
    conn.setRequestMethod("POST");
    //調用conn.setDoOutput()方法以顯式開啟請求體
    conn.setDoOutput(true);
    //用setRequestProperty方法設置一個自定義的請求頭:action,由於後端判斷
    conn.setRequestProperty("action", NETWORK_POST_KEY_VALUE);
    //獲取請求頭
    requestHeader = getReqeustHeader(conn);
    //獲取conn的輸出流
    OutputStream os = conn.getOutputStream();
    //獲取兩個鍵值對name=孫群和age=27的字節數組,將該字節數組作為請求體
    requestBody = new String("name=孫群&age=27").getBytes("UTF-8");
    //將請求體寫入到conn的輸出流中
    os.write(requestBody);
    //記得調用輸出流的flush方法
    os.flush();
    //關閉輸出流
    os.close();
    //當調用getInputStream方法時才真正將請求體數據上傳至服務器
    InputStream is = conn.getInputStream();
    //獲得響應體的字節數組
    responseBody = getBytesByInputStream(is);
    //獲得響應頭
    responseHeader = getResponseHeader(conn);
}

使用POST發送請求的代碼與用GET發送請求的代碼大部分類似,我們只對其中不同的地方做下說明。

  • 需要通過setRequestMethod將conn設置成POST方法。
  • 如果想用POST發送請求體,那麼需要調用setDoOutput方法,將其設置為true。
  • 通過conn.getOutputStream()獲得輸出流,可以向輸出流中寫入請求體,最後記得調用輸出流的flush方法,注意此時並沒有真正將請求體發送到服務器端。
  • 當調用getInputStream方法後,才真正將請求體的內容發送到服務器。

在我們的服務器端的Servlet中,在接收到POST請求發送的鍵值對數據後,也只是簡單地將鍵值對數據原樣寫入給客戶端,具體代碼參見上文,不再贅述。

用POST發送XML數據

點擊”POST XML”按鈕,可以用POST發送XML數據,界面如下所示:

這裡寫圖片描述

代碼如下所示:

if (NETWORK_POST_XML.equals(action)) {
    //用POST發送XML數據
    url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet");
    conn = (HttpURLConnection) url.openConnection();
    //通過setRequestMethod將conn設置成POST方法
    conn.setRequestMethod("POST");
    //調用conn.setDoOutput()方法以顯式開啟請求體
    conn.setDoOutput(true);
    //用setRequestProperty方法設置一個自定義的請求頭:action,由於後端判斷
    conn.setRequestProperty("action", NETWORK_POST_XML);
    //獲取請求頭
    requestHeader = getReqeustHeader(conn);
    //獲取conn的輸出流
    OutputStream os = conn.getOutputStream();
    //讀取assets目錄下的person.xml文件,將其字節數組作為請求體
    requestBody = getBytesFromAssets("person.xml");
    //將請求體寫入到conn的輸出流中
    os.write(requestBody);
    //記得調用輸出流的flush方法
    os.flush();
    //關閉輸出流
    os.close();
    //當調用getInputStream方法時才真正將請求體數據上傳至服務器
    InputStream is = conn.getInputStream();
    //獲得響應體的字節數組
    responseBody = getBytesByInputStream(is);
    //獲得響應頭
    responseHeader = getResponseHeader(conn);
}

上面的代碼與用POST發送鍵值對的代碼很相似,對其進行簡單說明。

  • 上述代碼通過getBytesFromAssets方法讀取了assets目錄下的person.xml文件,將xml文件的字節流作為請求體requestBody,然後將該請求體發送到服務器。
  • person.xml文件如下所示:
<?xml version="1.0" encoding="utf-8"?>
<persons>
    <person id="101">
        <name>張三</name>
        <age>27</age>
    </person>
    <person id="102">
        <name>李四</name>
        <age>28</age>
    </person>
</persons>

<person>標簽對應著Person類,Person類代碼如下所示:

package com.ispring.httpurlconnection;

public class Person {
    private String id = "";
    private String name = "";
    private int age = 0;

    public String getId(){
        return id;
    }

    public void setId(String id){
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return new StringBuilder().append("name:").append(getName()).append(", age:").append(getAge()).toString();
    }
}

服務端接收到客戶端發送來的XML數據之後,只是簡單的將其原樣寫回到客戶端,即客戶端接收到的響應體還是原來的XML數據,然後客戶端通過parseXmlResultByBytes方法對該XML數據進行解析,將字節數組轉換成List<Person>,parseXmlResultByBytes代碼如下所示:

    //將表示xml的字節數組進行解析
    private String parseXmlResultByBytes(byte[] bytes) {
        InputStream is = new ByteArrayInputStream(bytes);
        StringBuilder sb = new StringBuilder();
        List<Person> persons = XmlParser.parse(is);
        for (Person person : persons) {
            sb.append(person.toString()).append("\n");
        }
        return sb.toString();
    }

該方法使用了自定義的XmlParser類對XML數據進行解析,其源碼如下所示:

package com.ispring.httpurlconnection;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

public class XmlParser {

    public static List<Person> parse(InputStream is) {
        List<Person> persons = new ArrayList<>();
        try{
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser parser = factory.newSAXParser();
            PersonHandler personHandler = new PersonHandler();
            parser.parse(is, personHandler);
            persons = personHandler.getPersons();
        }catch (Exception e){
            e.printStackTrace();
        }
        return persons;
    }

    static class PersonHandler extends DefaultHandler {
        private List<Person> persons;
        private Person temp;
        private StringBuilder sb;

        public List<Person> getPersons(){
            return persons;
        }

        @Override
        public void startDocument() throws SAXException {
            super.startDocument();
            persons = new ArrayList<>();
            sb = new StringBuilder();
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            super.startElement(uri, localName, qName, attributes);
            sb.setLength(0);
            if(localName.equals("person")){
                temp = new Person();
                int length = attributes.getLength();
                for(int i = 0; i < length; i++){
                    String name = attributes.getLocalName(i);
                    if(name.equals("id")){
                        String value = attributes.getValue(i);
                        temp.setId(value);
                    }
                }
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            super.characters(ch, start, length);
            sb.append(ch, start, length);
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            super.endElement(uri, localName, qName);
            if(localName.equals("name")){
                String name = sb.toString();
                temp.setName(name);
            }else if(localName.equals("age")){
                int age = Integer.parseInt(sb.toString());
                temp.setAge(age);
            }else if(localName.equals("person")){
                persons.add(temp);
            }
        }
    }
}

用POST發送JSON數據

點擊”POST JSON”按鈕,可以用POST發送JSON數據,界面如下所示:

這裡寫圖片描述

代碼如下所示:

if (NETWORK_POST_JSON.equals(action)) {
     //用POST發送JSON數據
     url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet");
     conn = (HttpURLConnection) url.openConnection();
     //通過setRequestMethod將conn設置成POST方法
     conn.setRequestMethod("POST");
     //調用conn.setDoOutput()方法以顯式開啟請求體
     conn.setDoOutput(true);
     //用setRequestProperty方法設置一個自定義的請求頭:action,由於後端判斷
     conn.setRequestProperty("action", NETWORK_POST_JSON);
     //獲取請求頭
     requestHeader = getReqeustHeader(conn);
     //獲取conn的輸出流
     OutputStream os = conn.getOutputStream();
     //讀取assets目錄下的person.json文件,將其字節數組作為請求體
     requestBody = getBytesFromAssets("person.json");
     //將請求體寫入到conn的輸出流中
     os.write(requestBody);
     //記得調用輸出流的flush方法
     os.flush();
     //關閉輸出流
     os.close();
     //當調用getInputStream方法時才真正將請求體數據上傳至服務器
     InputStream is = conn.getInputStream();
     //獲得響應體的字節數組
     responseBody = getBytesByInputStream(is);
     //獲得響應頭
     responseHeader = getResponseHeader(conn);
}

上面的代碼與用POST發送XML的代碼很相似,對其進行簡單說明。

  • 上述代碼通過getBytesFromAssets方法讀取了assets目錄下的person.json文件,將json文件的字節流作為請求體requestBody,然後將該請求體發送到服務器。
  • person.json文件如下所示:
{
  "persons": [{
    "id": "101",
    "name":"張三",
    "age":27
  }, {
    "id": "102",
    "name":"李四",
    "age":28
  }]
}
  • persons數組中的每個元素都對應著一個Person對象。
  • 服務端接收到客戶端發送來的JSON數據之後,只是簡單的將其原樣寫回到客戶端,即客戶端接收到的響應體還是原來的JSON數據,然後客戶端通過parseJsonResultByBytes方法對該XML數據進行解析,將字節數組轉換成List,parseXmlResultByBytes代碼如下所示:
    //將表示json的字節數組進行解析
    private String parseJsonResultByBytes(byte[] bytes){
        String jsonString = getStringByBytes(bytes);
        List<Person> persons = JsonParser.parse(jsonString);
        StringBuilder sb = new StringBuilder();
        for (Person person : persons) {
            sb.append(person.toString()).append("\n");
        }
        return sb.toString();
    }

該方法又使用了自定義的JsonParset類,源碼如下所示:

package com.ispring.httpurlconnection;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;

public class JsonParser {
    public static List<Person> parse(String jsonString){
        List<Person> persons = new ArrayList<>();

        try{
            JSONObject jsonObject = new JSONObject(jsonString);
            JSONArray jsonArray = jsonObject.getJSONArray("persons");
            int length = jsonArray.length();
            for(int i = 0; i < length; i++){
                JSONObject personObject = jsonArray.getJSONObject(i);
                String id = personObject.getString("id");
                String name = personObject.getString("name");
                int age = personObject.getInt("age");
                Person person = new Person();
                person.setId(id);
                person.setName(name);
                person.setAge(age);
                persons.add(person);
            }
        }catch (JSONException e){
            e.printStackTrace();
        }

        return persons;
    }
}

其他

  • 如果Http請求體的數據很大,就可以認為該請求主要是完成數據上傳的作用;如果響應體的數據很大,就可以認為該請求主要完成數據下載的作用。
  • 上面我們通過demo演示了如何上傳XML文件和JSON文件,並對二者進行解析。在上傳的過程中,Android要寫入Content-Length這個請求頭,Content-Length就是請求體的字節長度,注意是字節長度,而不是字符長度(漢字等會占用兩個字節)。默認情況下,Android為了得到Content-Length的長度,Android會把請求體放到內存中的,直到輸出流調用了close方法後,才會讀取內存中請求體的字節長度,將其作為請求頭Content-Length。當要上傳的請求體很大時,這會非常占用內存,為此Android提供了兩個方法來解決這個問題。
    • setFixedLengthStreamingMode (int contentLength)
      如果請求體的大小是知道的,那麼可以調用HttpURLConnection的setFixedLengthStreamingMode (int contentLength) 方法,該方法會告訴Android要傳輸的請求頭Content-Length的大小,這樣Android就無需讀取整個請求體的大小,從而不必一下將請求體全部放到內存中,這樣就避免了請求體占用巨大內存的問題。
    • setChunkedStreamingMode (int chunkLength)
      如果請求體的大小不知道,那麼可以調用setChunkedStreamingMode (int chunkLength)方法。該方法將傳輸的請求體分塊傳輸,即將原始的數據分成多個數據塊,chunkLength表示每塊傳輸的字節大小。比如我們要傳輸的請求體大小是10M,我們將chunkLength設置為1024 * 1024 byte,即1M,那麼Android會將請求體分10次傳輸,每次傳輸1M,具體的傳輸規則是:每次傳輸一個數據塊時,首先在一行中寫明該數據塊的長度,比如1024 * 1024,然後在後面的一行中寫入要傳輸的數據塊的字節數組,再然後是一個空白行,這樣第一數據塊就這樣傳輸,在空白行之後就是第二個數據塊的傳輸,與第一個數據塊的格式一樣,直到最後沒有數據塊要傳輸了,就在用一行寫明要傳輸的字節為0,這樣在服務器端就知道讀取完了整個請求體了。如果設置的chunkLength的值為0,那麼表示Android會使用默認的一個值作為實際的chunkLength。使用setChunkedStreamingMode方法的前提是服務器支持分塊數據傳輸,分塊數據傳輸是從HTTP 1.1開始支持的,所以如果你的服務器只支持HTTP 1.0的話,那麼不能使用setChunkedStreamingMode方法。

希望本文對大家使用HttpURLConnection有所幫助!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved