Android 中 HttpURLConnection 使用詳解
認識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請求才能設定請求體。請求體中可以放置任意的位元組流,從而可以很方便地傳送任意格式的資料,服務端只需要讀取該輸入流即可。
- 請求URL(Request URL)
- 伺服器->客戶端
伺服器接收到客戶端發來的請求後,會進行相應的處理,並向客戶端輸出資訊,輸出的資訊包括響應頭和響應體。- 響應頭 (Response Headers)
響應頭也是一些鍵值對,如下所示:
響應頭包含了伺服器想要告訴客戶端的一些後設資料資訊,注意不是資料,是後設資料,比如通過響應頭Content-Encoding告訴客戶端伺服器所採用的壓縮格式,響應頭Content-Type告訴客戶端響應體是什麼格式的資料,再比如服務端可以通過多個Set-Cookie響應頭向客戶端寫入多條Cookie資訊,等等。剛剛提到的幾個請求頭都是W3C規定的標準的請求頭名稱,我們也可以在服務端向客戶端寫入自定義的響應頭。 - 響應體 (Response Body)
響應體是服務端向客戶端傳輸的實際的資料資訊,本質就是一堆位元組流,可以表示文字,也可以表示圖片或者其他格式的資訊,如下所示:
- 響應頭 (Response Headers)
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方法。
- setFixedLengthStreamingMode (int contentLength)
希望本文對大家使用HttpURLConnection有所幫助!
相關文章
- Android中HttpURLConnection使用詳解AndroidHTTP
- Android HttpURLConnection詳解AndroidHTTP
- Android中PopupWindow使用詳解Android
- Android中AsyncTask使用詳解Android
- 詳解Android中AsyncTask的使用Android
- Android 網路程式設計系列(4)使用 HttpUrlConnectionAndroid程式設計HTTP
- Android應用中Clean架構使用詳解Android架構
- Android Gson使用詳解Android
- Android Paint 使用詳解AndroidAI
- Android AsyncTask使用詳解Android
- Android webview使用詳解AndroidWebView
- Android ViewPager使用詳解AndroidViewpager
- HttpURLConnection和HttpClient的使用HTTPclient
- Android BroadcastReceiver使用詳解AndroidAST
- Android AIDL使用詳解AndroidAI
- 詳解Android RxJava的使用AndroidRxJava
- Android Support Annotations 使用詳解Android
- Android每週一輪子:HttpURLConnectionAndroidHTTP
- Android中的onWindowFocusChanged()方法詳解Android
- Android中SQLite應用詳解AndroidSQLite
- Android中的ANR用法詳解Android
- Android中Context用法詳解AndroidContext
- Android中的Context詳解AndroidContext
- Android 中的 Checkbox 詳解Android
- Android 中的 HandlerThread 詳解Androidthread
- Android-SharedPreferences 使用詳解Android
- Android FragmentTabHost 使用方法詳解AndroidFragment
- Android shape的使用詳解Android
- Android taskAffinity屬性使用詳解Android
- Android 中 Canvas 繪圖之 Shader 使用圖文詳解AndroidCanvas繪圖
- Android 中 Canvas 繪圖之 PorterDuffXfermode 使用及工作原理詳解AndroidCanvas繪圖
- Android中Canvas繪圖之PorterDuffXfermode使用及工作原理詳解AndroidCanvas繪圖
- Visual Studio中的Android模擬器使用詳解Android
- Android中Canvas繪圖之Shader使用圖文詳解AndroidCanvas繪圖
- maven中profiles使用詳解Maven
- Android探索之HttpURLConnection網路請求AndroidHTTP
- Java 中 HttpURLConnection 與 PoLA 法則JavaHTTP
- Android 中 XML 資料解析詳解AndroidXML