前言
在我們的應用中支援網路功能是絕對有必要的,大部分的應用程式都需要從伺服器獲取網路資料然後顯示在介面中。前兩篇文章我們介紹了 WebView 的一些用法和知識點。但是並非所有的網路功能都能通過 Webview 來實現,比如我們從伺服器獲取一段 json 資料,其中包含了我們想要的資訊,這時候,我們就不能使用Webview 了,而是需要直接獲取到一個 Http 請求的響應資料。我們可以使用 HttpUrlConnection 或者其他的第三方網路框架來實現網路訪問。
在 Android 6.0 之前,原生的有兩種方式可以進行網路請求,HttpClient 和 HttpUrlConnection,HttpClient 的 API 多而複雜,擴充困難,因此這種方式在 Android 6.0 之後就被官方移除了。HttpUrlConnection 的 API 簡單,體積較小,非常適合 Android 開發,也是官方推薦的網路請求方式。我們這篇文章就來看看 HttpUrlConnection 的相關知識。
HttpUrlConnection 用法
使用 HttpUrlConnection 來進行網路請求大致上可以分為4個步驟:
- 獲取到 HttpUrlConnection 物件
- 進行全域性的網路設定並建立 Http 連線
- 進行資料處理
- 關閉連線
我們依次來看看這些步驟中需要做哪些工作:
獲取到 HttpUrlConnection 物件
使用 URL 物件的 openConnection()方法獲取到 HttpUrlConnection 物件,這個物件是我們進行網路請求的核心。
網路請求在響應時間上具有很大的不確定性,如果將網路請求放在主執行緒中執行時,過長的耗時操作會阻塞主執行緒,導致程式卡死。因此,網路請求都應該放在子執行緒中執行。
如以下示例程式碼:
URL url = new URL("http://lixiaoyu.cc");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();複製程式碼
進行全域性的網路設定並建立 Http 連線
獲取到 HttpUrlConnection 物件後,就可以呼叫這個物件的一些方法,進行一些網路設定,比如設定連線超時時間,讀取超時時間,網路請求方式等。如以下程式碼所示:
//設定網路請求方式,如GET、POST、HEAD等
conn.setRequestMethod("GET");
//設定連線超時時間
conn.setConnectTimeout(8000);
//設定讀取超時時間
conn.setReadTimeout(8000);
//設定Http請求頭部
conn.setRequestProperty("Accept-Encoding", "identity");
//設定可以讀取輸入流
conn.setDoInput(true);
//設定可以讀取輸出流,在使用POST向伺服器提交資料時必須要將該方法設定為true
conn.setDoOutput(true);
//進行Http連線,必須寫在setDoInput()方法後面
conn.connect();複製程式碼
進行資料處理
進行資料處理包括兩個方面,一個是從伺服器讀取相應資料,一個是向伺服器傳送資料(POST 方法會用到),分別對應之前的 setDoInput() 和 setDoOutput() 方法。
從伺服器讀取資料
先來看看從伺服器讀取資料,通過呼叫 HttpUrlConnection 物件的一些方法可以獲取到伺服器傳送給客戶端的相應資訊,如狀態碼、響應內容長度、包含了響應內容的輸入流等等。如以下示例程式碼:
//獲取響應狀態碼,如 200 表示成功等
int responseCode = conn.getResponseCode();
//獲取包含響應內容的輸入流
InputStream in = conn.getInputStream();
//獲取響應內容長度
int contentLength = conn.getContentLength();複製程式碼
在獲取輸入流之後,就可以利用 Java 中的 IO 流的知識對該輸入流進行流處理,從而得到我們想要的資料。(這部分程式碼在完整示例程式碼中給出)
向伺服器提交資料
我們常常使用 POST 方法向伺服器提交一個表單,在向伺服器提交資料時,需要先通過 HttpUrlConnection 物件的 getOutputStream() 方法獲取到輸出流物件,在通過輸出流物件的 write() 方法向伺服器寫資料。POST 方法的每條資料都以鍵值對的形式提交,資料之間用 “&” 進行分隔。如以下示例程式碼:
//將網路請求方法改為 POST
conn.setRequestMethod("POST");
//設定支援輸出流
conn.setDoOutput(true);
//獲取 HttpUrlConnection 的輸出流物件
OutputStream out = conn.getOutputStream();
//給這個輸出流新增一個處理流,方便操作
DataOutputStream dos = new DataOutputStream(out);
//使用 writeBytes() 方法將資料提交到伺服器
dos.writeBytes("username=admin&password=123456");
//進行 Http 連線
conn.connect();複製程式碼
關閉連線
在我們完成了所有資料寫入和讀取的流操作後,應該呼叫 disconnect() 方法關閉 Http 連線。
//關閉 Http 連線
conn.disconnect();複製程式碼
接下來,通過兩個例項加深對 HttpUrlConnection 的理解。
例項一:獲取網站原始碼
在 layout 檔案中放置一個 Button 和一個 TextView,我們希望點選 Button 後,獲取到某個網站的 HTML 原始碼,並以文字的形式展示在 TextView 中。這個例項較為簡單,就直接將程式碼貼出,關鍵部分會有註釋。
layout 檔案中,Button 指定了一個名為 getCode 的 onClick()方法,可以直接在 Activity 中實現這個方法,進行 Button 的點選事件監聽。
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="getCode"
android:text="獲取網頁原始碼"/>
<TextView
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>複製程式碼
在 Activity 中:
//處理 Button 的點選事件
public void getCode(View view) {
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL("http://lixiaoyu.cc");
//獲取 HttpURLConnection 物件
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//設定請求方法為 GET
conn.setRequestMethod("GET");
//設定連線超時時間為 8 秒
conn.setConnectTimeout(8000);
//設定讀取超時時間為 8 秒
conn.setReadTimeout(8000);
//支援輸入流
conn.setDoInput(true);
//獲取響應狀態碼
int responseCode = conn.getResponseCode();
Log.i(TAG, "responseCode=" + responseCode);
//獲取輸入流
InputStream in = conn.getInputStream();
//將輸入流封裝成 BufferedReader
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuffer sb = new StringBuffer();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
//將 StringBuffer 的資料轉化成 String,在主執行緒中設定到 TextView 上
showCode(sb.toString());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private void showCode(final String s) {
//必須在主執行緒中操作 UI
runOnUiThread(new Runnable() {
@Override
public void run() {
tvCode.setText(s);
}
});
}複製程式碼
程式執行的結果如圖所示:
例項二:下載檔案
在上篇 WebView 的文章中講到在 Webview 下載檔案可以有兩種方式,一時通過隱式 Intent 呼叫系統瀏覽器進行下載,一種是拿到檔案的 URL 後自己建立執行緒進行下載,上篇文章中只介紹了第一種方法,這裡就介紹第二種方法的實現。其實原理非常簡單,就是在獲取到檔案 URL 後,使用 HttpUrlConnection 進行網路請求,通過其物件的輸入流讀取到該檔案的二進位制資料,將二進位制資料儲存為相應格式的檔案即可。
完整程式碼如下:
public class WebActivity extends AppCompatActivity {
private WebView mWebView;
private String mUrl = null;
private static final String TAG = "WebActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_haha);
mWebView = (WebView) findViewById(R.id.webview);
mWebView.getSettings().setJavaScriptEnabled(true);
//載入豌豆莢應用市場的網頁
mWebView.loadUrl("http://wandoujia.com");
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
//設定下載監聽器
mWebView.setDownloadListener(new DownloadListener() {
@Override
public void onDownloadStart(String url, String s1, String s2, String s3, long l) {
//如果URL以“.apk”結尾,就進行下載
if (url.endsWith(".apk")) {
mUrl = url;
//程式執行在Android 6.0以上的系統中,所以在讀寫SD卡時需要動態申請許可權
if(ContextCompat.checkSelfPermission(WebActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(WebActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}else{
//如果已經申請該許可權,則直接下載
downloadApk(mUrl);
}
}
}
});
}
/**
* 下載Apk檔案的方法
* @param url
*/
private void downloadApk(final String url) {
new Thread(new Runnable() {
@Override
public void run() {
//獲取SD卡的目錄
File sdCard = Environment.getExternalStorageDirectory();
//通過URL拿到apk檔名
String apkName = url.substring(url.lastIndexOf("/"));
//在SD卡的根目錄下新建一個檔案
File apkFile = new File(sdCard, apkName);
try {
if (!apkFile.exists()){
apkFile.createNewFile();
}else{
apkFile.delete();
apkFile.createNewFile();
}
FileOutputStream fos = new FileOutputStream(apkFile);
HttpURLConnection conn = (HttpURLConnection) (new URL(url)).openConnection();
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.setConnectTimeout(80000);
conn.setReadTimeout(80000);
conn.connect();
InputStream in = conn.getInputStream();
//新建一個byte陣列buffer,將輸入流中讀到的資料寫入buffer中
byte [] buffer = new byte[1024 * 1024];
//每次讀到的資料長度
int len;
while ((len = in.read(buffer)) != -1) {
//將每次讀取到的資料寫入SD卡中的檔案裡
fos.write(buffer,0,len);
}
Log.i("TAG", "download success");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
/**
* 許可權申請的回撥函式
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode){
case 1:
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED){
//使用者已同意該許可權的申請
if (mUrl != null){
downloadApk(mUrl);
}
}else{
//使用者拒絕了許可權的申請
Toast.makeText(this, "你拒絕了許可權申請", Toast.LENGTH_SHORT).show();
}
break;
}
}
}複製程式碼
將 HttpUrlConnection 封裝成工具類
每次有網路請求時,如果都使用上面的方式來實現,效率顯然是極低的,因為每次我們都要把所有的程式碼都再寫一遍。更好的想法就是將網路請求封裝成一個工具類,每次要用的時候直接呼叫這個工具類的相關方法。我對 HttpUrlConnection 進行了一個簡單的封裝。
在 HttpUtils 這個工具類中,提供了四個 public 的靜態方法:
String httpGet(String url)
String httpGet(String url, HttpCallback callback)
String httpPost(String url, List< PostParam > paramList)
String httpGet(String url, List< PostParam > paramList, HttpCallback callback)
前兩個是 GET 方法,後兩個是 POST 方法。具體的區別請看程式碼以及註釋。
HttpUtils 類:
public class HttpUtils {
/**
* GET 方法,返回字串型別的響應內容,返回值為空表示失敗,不為空表示成功了
* @param url 網址
* @return
*/
@Nullable
public static String httpGet(String url){
HttpResponse response = baseGet(url);
if(response.getCode() == 200){
//成功
Log.i(TAG, "httpGet: 成功");
return response.getContent();
}else{
//失敗
Log.i(TAG, "httpGet: 失敗---" + response.getCode());
return null;
}
}
/**
* GET方法,使用一個回撥介面,成功則回撥onSuccess方法,失敗則回撥onError方法
* @param url 網址
* @param callback 回撥介面
* @return
*/
@Nullable
public static String httpGet(String url, HttpCallback callback){
HttpResponse response = baseGet(url);
if(response.getCode() == 200){
//成功
Log.i(TAG, "httpGet: 成功");
callback.onSuccess(response.getContent());
}else{
//失敗
Log.i(TAG, "httpGet: 失敗---" + response.getCode());
callback.onError(response.getCode(), new Exception());
}
return null;
}
/**
* 基礎的GET實現,不對外公佈此方法,僅僅是被上面兩個方法呼叫
* 返回的HttpResponse類,包含狀態碼和響應內容。
* @param url 網址
* @return
*/
private static HttpResponse baseGet(String url){
HttpURLConnection conn = getHttpUrlConnection(url);
HttpResponse response = new HttpResponse();
BufferedReader reader;
try {
//設定請求方式
conn.setRequestMethod("GET");
//建立連線
conn.connect();
//獲取狀態碼
response.setCode(conn.getResponseCode());
reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = reader.readLine()) != null){
sb.append(line);
}
//獲取響應內容
response.setContent(sb.toString());
//關閉流
reader.close();
//關閉連線
conn.disconnect();
} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
/**
* POST 方法,返回字串型別的響應內容,返回值為空表示失敗,不為空表示成功了
* @param url 網址
* @param paramList Post提交的引數列表,鍵值對形式
* @return
*/
@Nullable
public static String httpPost(String url, List<PostParam> paramList){
HttpResponse response = basePost(url, paramList);
if(response.getCode() == 200){
//成功
Log.i(TAG, "httpPost: 成功");
return response.getContent();
}else{
//失敗
Log.i(TAG, "httpPost: 失敗---" + response.getCode());
return null;
}
}
/**
* POST方法,使用一個回撥介面,成功則回撥onSuccess方法,失敗則回撥onError方法
* @param url 網址
* @param paramList post提交的引數列表
* @param callback 回撥介面
* @return 返回值無意義
*/
@Nullable
public static String httpPost(String url, List<PostParam> paramList, HttpCallback callback){
HttpResponse response = basePost(url, paramList);
if(response.getCode() == 200){
//成功
Log.i(TAG, "httpPost: 成功");
callback.onSuccess(response.getContent());
}else{
//失敗
Log.i(TAG, "httpPost: 失敗---" + response.getCode());
callback.onError(response.getCode(), new Exception());
}
return null;
}
/**
* 基礎的POST實現,不對外公佈此方法,只被上面兩個POST方法呼叫
* @param url 網址
* @param paramList 提交的引數列表
* @return
*/
private static HttpResponse basePost(String url, List<PostParam> paramList){
HttpURLConnection conn = getHttpUrlConnection(url);
HttpResponse response = new HttpResponse();
String post = parseParamList(paramList);
BufferedReader reader;
try {
//設定請求方式
conn.setRequestMethod("POST");
//獲取輸出流並轉化為處理流
DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
//寫入引數
dos.writeUTF(post);
//建立連線
conn.connect();
//獲取狀態碼
response.setCode(conn.getResponseCode());
reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = reader.readLine()) != null){
sb.append(line);
}
//獲取響應內容
response.setContent(sb.toString());
//關閉流
reader.close();
//關閉連線
conn.disconnect();
} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
/**
* 將引數列表轉化成一段字串
* @param paramList
* @return
*/
@NonNull
private static String parseParamList(@NonNull List<PostParam> paramList){
StringBuffer sb = new StringBuffer();
for (PostParam param :
paramList) {
if(sb == null){
sb.append(param.toString());
}else{
sb.append("&"+param.toString());
}
}
return sb.toString();
}
/**
* 獲取HttpUrlConnection物件,並進行基礎網路設定
* @param url
* @return
*/
private static HttpURLConnection getHttpUrlConnection(String url){
HttpURLConnection conn = null;
try {
//獲取HttpURLConnection物件
URL mUrl = new URL(url);
conn = (HttpURLConnection) mUrl.openConnection();
//進行一些通用設定
conn.setConnectTimeout(80000);
conn.setReadTimeout(80000);
conn.setRequestProperty("Conection","Keep-Alive");
conn.setDoInput(true);
conn.setDoOutput(true);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return conn;
}
/**
* 回撥介面,有兩個方法,分別在成功和失敗時回撥
*/
public interface HttpCallback{
void onSuccess(String response);
void onError(int responseCode, Exception e);
}
/**
* Post提交的引數類
* 包含String型別的name和String型別的value
*/
public class PostParam{
private String name;
private String value;
public PostParam(String name, String value){
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return name+"="+value;
}
}
}複製程式碼
HttpResponse 類
/**
* 包含網路請求的響應狀態碼和響應內容
*/
public class HttpResponse {
private int code;
private String content;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}複製程式碼
有了這個工具類,我們實現例項一的功能,就可以這樣來寫:
public void getCode(View view) {
new Thread(new Runnable() {
@Override
public void run() {
String url = "http://www.cnmooc.org";
String response = HttpUtils.httpGet(url);
if(response != null){
showCode(response);
}
}
}).start();
}
private void showCode(final String s) {
//必須在主執行緒中操作UI
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.i(TAG, "run: code--->"+s);
tvCode.setText(s);
}
});
}複製程式碼
或者使用帶回撥介面的GET方法:
public void getCode(View view) {
new Thread(new Runnable() {
@Override
public void run() {
String url = "http://www.cnmooc.org";
HttpUtils.httpGet(url, new HttpUtils.HttpCallback() {
@Override
public void onSuccess(String response) {
showCode(response);
}
@Override
public void onError(int responseCode, Exception e) {
e.printStackTrace();
}
});
}
}).start();
}
private void showCode(final String s) {
//必須在主執行緒中操作UI
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.i(TAG, "run: code--->"+s);
tvCode.setText(s);
}
});
}複製程式碼
結束語
這篇文章中對 HttpUrlConnection 的用法、使用示例以及如何封裝一個簡單的 Http 工具類做了一個介紹,對於瞭解 HttpUrlConnection 的相關內容還是有所幫助的。關於封裝部分,由於水平有限,其實封裝的並不好,還是需要先建立執行緒,在子執行緒中進行網路操作,完了後也需要手動切換回主執行緒來操作 UI,在接下來學習第三方網路載入框架時,會重點留意這個問題,學習如何更好地封裝,還請繼續支援。感恩。
再見。
參考資料
這篇文章的參考資料主要有:
郭霖《第一行程式碼(第二版)》
郭霖:Android 訪問網路,使用 HttpURLConnection 還是 HttpClient?
blog.csdn.net/guolin_blog…劉望舒:Android 網路程式設計(二)HttpClient 與 HttpURLConnection
liuwangshu.cn/application…