Android 網路程式設計系列(4)使用 HttpUrlConnection

顧小魚發表於2019-02-23

前言

在我們的應用中支援網路功能是絕對有必要的,大部分的應用程式都需要從伺服器獲取網路資料然後顯示在介面中。前兩篇文章我們介紹了 WebView 的一些用法和知識點。但是並非所有的網路功能都能通過 Webview 來實現,比如我們從伺服器獲取一段 json 資料,其中包含了我們想要的資訊,這時候,我們就不能使用Webview 了,而是需要直接獲取到一個 Http 請求的響應資料。我們可以使用 HttpUrlConnection 或者其他的第三方網路框架來實現網路訪問。

在 Android 6.0 之前,原生的有兩種方式可以進行網路請求,HttpClient 和 HttpUrlConnection,HttpClient 的 API 多而複雜,擴充困難,因此這種方式在 Android 6.0 之後就被官方移除了。HttpUrlConnection 的 API 簡單,體積較小,非常適合 Android 開發,也是官方推薦的網路請求方式。我們這篇文章就來看看 HttpUrlConnection 的相關知識。

HttpUrlConnection 用法

使用 HttpUrlConnection 來進行網路請求大致上可以分為4個步驟:

  1. 獲取到 HttpUrlConnection 物件
  2. 進行全域性的網路設定並建立 Http 連線
  3. 進行資料處理
  4. 關閉連線

我們依次來看看這些步驟中需要做哪些工作:

獲取到 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);
        }
    });
}複製程式碼

程式執行的結果如圖所示:

pic
pic

例項二:下載檔案

在上篇 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…

相關文章