Android 網絡編程系列(4)使用 HttpUrlConnection

前言

在咱們的應用中支持網絡功能是絕對有必要的,大部分的應用程序都須要從服務器獲取網絡數據而後顯示在界面中。前兩篇文章咱們介紹了 WebView 的一些用法和知識點。可是並不是全部的網絡功能都能經過 Webview 來實現,好比咱們從服務器獲取一段 json 數據,其中包含了咱們想要的信息,這時候,咱們就不能使用Webview 了,而是須要直接獲取到一個 Http 請求的響應數據。咱們可使用 HttpUrlConnection 或者其餘的第三方網絡框架來實現網絡訪問。html

在 Android 6.0 以前,原生的有兩種方式能夠進行網絡請求,HttpClient 和 HttpUrlConnection,HttpClient 的 API 多而複雜,拓展困難,所以這種方式在 Android 6.0 以後就被官方移除了。HttpUrlConnection 的 API 簡單,體積較小,很是適合 Android 開發,也是官方推薦的網絡請求方式。咱們這篇文章就來看看 HttpUrlConnection 的相關知識。android

HttpUrlConnection 用法

使用 HttpUrlConnection 來進行網絡請求大體上能夠分爲4個步驟:web

  1. 獲取到 HttpUrlConnection 對象
  2. 進行全局的網絡設置並創建 Http 鏈接
  3. 進行數據處理
  4. 關閉鏈接

咱們依次來看看這些步驟中須要作哪些工做:編程

獲取到 HttpUrlConnection 對象

使用 URL 對象的 openConnection()方法獲取到 HttpUrlConnection 對象,這個對象是咱們進行網絡請求的核心。json

網絡請求在響應時間上具備很大的不肯定性,若是將網絡請求放在主線程中執行時,過長的耗時操做會阻塞主線程,致使程序卡死。所以,網絡請求都應該放在子線程中執行。數組

如如下示例代碼:瀏覽器

URL url = new URL("http://lixiaoyu.cc");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();複製代碼

進行全局的網絡設置並創建 Http 鏈接

獲取到 HttpUrlConnection 對象後,就能夠調用這個對象的一些方法,進行一些網絡設置,好比設置鏈接超時時間,讀取超時時間,網絡請求方式等。如如下代碼所示:bash

//設置網絡請求方式,如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…

相關文章
相關標籤/搜索