1. 程式人生 > >Android OkHttp相關解析 實踐篇

Android OkHttp相關解析 實踐篇

概述

OkHttp是一個處理網路請求的開源框架,由知名移動支付公司square開發,是android當下最熱門的輕量級框架之一。相比較google自帶的網路請求API(HttpURLConnetion、HttpClient不推薦),OkHttp能提交更簡易、更方便的網路請求。
本篇部落格想通過程式碼實踐的方式來和大家一起學習okhttp的使用,再多的理論都不如直接編碼,寫些demo有用,只有通過不斷的實踐,才能更好地掌握理解理論。主要包括以下內容:

  • 如何使用okhttp
  • okhttp get請求
  • okhttp post請求
  • okhttp 提取響應頭
  • okhttp 解析json
  • okhttp 提交表單
  • okhttp 提交檔案
  • okhttp 下載檔案
  • okhttp https請求相關

主要內容

如何使用okhttp

在工程對應的build.gradle檔案中新增一句依賴,然後sync project即可:

compile 'com.squareup.okhttp3:okhttp:3.7.0'

那怎麼知道當前okhttp的最新版本呢?
很簡單,直接通過AS查詢新增即可。
右鍵project->open module setting->Dependencies 點選右上角+號,新增library dependencies,搜尋okhttp,找到square公司釋出的最新版本okhttp新增。

okhttp get 請求

添加了okhttp依賴庫後,就可以通過程式碼去使用http了,以下是通過get請求去訪問一個網站:

 public static String doGet(Context context, String url, boolean needVerify) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .build();
        OkHttpClient client = getOkHttpClient(context, request
.isHttps(), needVerify); Response response = client.newCall(request).execute(); if (response.isSuccessful()) { return response.body().string(); } else { throw new IOException("Unexpected code: " + response); } } private static OkHttpClient httpClient = new OkHttpClient();

Get請求過程包括:

  1. 首先,通過Request.Builder例項化一個request物件
  2. 然後,例項化一個okhttpclient物件
  3. 最後,通過call.excute()方法發起一個get請求

TIP: 這裡的請求過程是同步的,且只適用於普通的請求,大檔案的下載和上傳需要使用另一個非同步請求介面call.enqueue()。

 //非同步請求
                try {
                    OkHttpHelper.doAsyncGet(mContext, "http://publicobject.com/helloworld.txt",
                            new Callback() {
                                @Override
                                public void onFailure(Call call, IOException e) {
                                    e.printStackTrace();
                                }

                                @Override
                                public void onResponse(Call call, Response response) throws IOException {
                                    System.out.println("onResponse() 當前執行緒:" + Thread.currentThread().getName());
                                    if (response.isSuccessful()) {
                                        Headers respHeaders = response.headers();
                                        for (int i = 0; i < respHeaders.size(); i++) {
                                            System.out.println(respHeaders.name(i) + ": " + respHeaders.value(i));
                                        }
                                        String respContent = response.body().string();
                                        System.out.println(respContent);
                                        sendRespMessage(MSG_HTTP_RESPONCE, true, respContent);
                                    } else {
                                        throw new IOException("Unexpected code: " + response);
                                    }
                                }
                            });
                } catch (Exception e) {
                    e.printStackTrace();
                    sendRespMessage(MSG_HTTP_RESPONCE, false, "請求出錯");
                }

 public static void doAsyncGet(Context context, String url, Callback callback) throws Exception {
        Request request = new Request.Builder()
                .url(url)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        client.newCall(request).enqueue(callback);
    }

TIP: 非同步請求和同步的區別是網路請求是由okhttp新開一個執行緒來執行的,需要注意請求完成後的callback是在子執行緒上,不是主執行緒
因此,若回撥後需要改變介面內容,則需要用handler來跳轉到主執行緒進行操作。
為什麼不直接回調到主執行緒上呢?我的理解是為了讓框架更靈活,適應更多不同的需求,很多專案在做完網路請求後,可能還有很多的業務需要在子執行緒上處理,這時候直接回調主執行緒就不是明智的選擇。

okhttp post請求

 public static String doPostString(Context context) throws IOException {
        String postBody = ""
                + "Releases\n"
                + "--------\n"
                + "\n"
                + " * _1.0_ May 6, 2013\n"
                + " * _1.1_ June 15, 2013\n"
                + " * _1.2_ August 11, 2013\n";
        RequestBody body = RequestBody.create(MARKDOWN, postBody);
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(body)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

    private static final MediaType MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");

這個例子是提交一串string到伺服器上,和get 請求相比多了一個requestBody物件,用來儲存要上傳的body(也就是string)。

okhttp 提取響應頭

在實際專案開發中,經常會碰到需要自定義請求頭,或者獲取自定義響應頭,那在okhttp中該如何實現呢?

 public static String doGetHeader(Context context) throws IOException {
        Request request = new Request.Builder()
                .url("https://api.github.com/repos/square/okhttp/issues")
                .header("User-Agent", "OkHttp Headers.java")
                .addHeader("Accept", "application/json; q=0.5") //可新增自定義header
                .addHeader("Accept", "application/vnd.github.v3+json")
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            System.out.println("Server: " + response.header("Server")); //獲取自定義響應頭
            System.out.println("User-Agent: " + response.header("User-Agent"));
            System.out.println("Date: " + response.header("Date"));
            System.out.println("Vary: " + response.headers("Vary"));
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

只要通過Request.Builder().addHeader就可以新增自定義header;通過responce.header(“”)來獲取響應頭。

okhttp 使用gson解析json

現在的很多專案,對於只需要簡單的資料互動時,這時候json格式的資料傳輸就是一個很好的選擇。接下來介紹如何通過google的gson來解析json。
第一步:配置google gson依賴庫,具體方法和配置okhttp一樣,不再做具體介紹

 compile 'com.google.code.gson:gson:2.8.0'

第二步:安裝GsonFormat外掛(此步驟主要用於方便自動生成json實體類,可不做)

找到setting->Plugins 
搜尋 gsonformat 安裝
重啟android studio

第三步:發起okhttp json請求

 public static String doGetJson(Context context) throws Exception {
        Request request = new Request.Builder()
                .url("https://api.github.com/gists/c2a7c39532239ff261be")
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
            StringBuffer buffer = new StringBuffer();
            for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
                System.out.println(entry.getKey());
                System.out.println(entry.getValue().content);
                buffer.append(entry.getKey() + ":");
                buffer.append(entry.getValue() + "\n");
            }
            return buffer.toString();
        } else {
            throw new Exception("Unexpected code: " + response);
        }
    }

    static class Gist {
        Map<String, GistFile> files;
    }

    static class GistFile {
        String content;
    }

前面的請求和get一樣,獲得響應後,通過response.body.charStream()方法獲取body的char stream,然後再用gson.fromJson()方法解析stream,獲取json實體類。json實體類的構造要和返回的json內容相對應,不同請求得到的json實體是不一樣的。

okhttp 提交表單

public static String doPostForm(Context context) throws IOException {
        RequestBody body = new FormBody.Builder()
                .add("search", "China")
                .build();
        Request request = new Request.Builder()
                .url("https://en.wikipedia.org/w/index.php")
                .post(body)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

通過FormBody.buider() 去構造一個request body,然後post(body)即可。

okhttp 提交檔案

這個和post string是類似的,只是從檔案當中去獲取要post的body內容

 public static String doPostFile(Context context) throws IOException {
        File file = new File("assets/readme.txt");
        RequestBody body = RequestBody.create(MARKDOWN, file);
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(body)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

TIP: 本例子中的檔案上傳是有問題的,因為assets下的檔案不能這麼讀取,會提示找不到。
當檔案儲存在assets目錄中,可以通過post stream方式來上傳檔案。

public static String doPostStream(final Context context) throws IOException {
        RequestBody body = new RequestBody() {
            @Override
            public MediaType contentType() {
                return MARKDOWN;
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                InputStream is = context.getAssets().open("readme.txt");
                byte[] buffer = new byte[1024];
                int byteCount=0;
                while((byteCount = is.read(buffer)) != -1) {//迴圈從輸入流讀取 buffer位元組
                    sink.write(buffer, 0, byteCount);//將讀取的輸入流寫入到輸出流
                }
                is.close();
            }
        };
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(body)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            return response.body().string();
        } else {
            throw new IOException("Unexpected code: " + response);
        }
    }

    private static final MediaType MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");

這裡的buffersink可以理解為記憶體和網路請求之間的管道,當上傳檔案readme.txt時,首先通過讀取檔案內容到記憶體中;接著通過buffersink把檔案內容上傳到網路中;最後伺服器接收,返回請求結果。

okhttp 檔案下載

這裡對檔案下載功能做了一個簡單的擴充套件,增加下載過程進度的展示。
1、定義一個介面,當正在下載和下載完成時,都會觸發回撥

  public interface DownloadStatusListener {
        void onProgress(long totalSize, long currSize);
        void onFinish(boolean success, String msg);
    }

2、通過responce.body.byteStream流讀取檔案,並儲存到指定位置

 /**
     * 下載檔案
     * @param context
     * @param url
     * @param destPath
     */
    public static void downloadFile(Context context, String url, String destPath, final DownloadStatusListener listener) {
        String fileName = url.substring(url.lastIndexOf("/"));
        File dirPath = new File(destPath);
        if (!dirPath.exists()) {
            dirPath.mkdirs();
        }
        final File file = new File(dirPath, fileName);
        Request request = new Request.Builder().url(url).build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps());
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "onFailure()...");
                listener.onFinish(false, e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "onResponse() ...");
                InputStream is = null;
                byte[] buf = new byte[2048];
                int len = 0;
                FileOutputStream fos = null;
                try {
                    long total = response.body().contentLength();
                    Log.e(TAG, "total------>" + total);
                    long current = 0;
                    is = response.body().byteStream();
                    fos = new FileOutputStream(file);
                    while ((len = is.read(buf)) != -1) {
                        current += len;
                        fos.write(buf, 0, len);
                        Log.e(TAG, "current------>" + current);
                        listener.onProgress(total, current);
                    }
                    fos.flush();
                    listener.onFinish(true, file.getAbsolutePath());
                } catch (IOException e) {
                    Log.e(TAG, e.toString());
                    listener.onFinish(false, "下載失敗");
                } finally {
                    try {
                        if (is != null) {
                            is.close();
                        }
                        if (fos != null) {
                            fos.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

3、呼叫downloadFile方法,實現回撥,更新主介面。

mTvRespContent.setText("開始檔案下載...");
                try {
                    OkHttpHelper.downloadFile(mContext,
                            "https://hhap.bjyada.com:443/downloads_sit/2/000011/FJCT/K9/BSA/com.boc.spos.client/1.apk",
                            DEST_PATH,
                            new OkHttpHelper.DownloadStatusListener() {
                                @Override
                                public void onProgress(long totalSize, long currSize) {
                                    String msg = "總大小:" + formatFileSize(totalSize) + "   已下載:" + formatFileSize(currSize);
                                    sendRespMessage(MSG_DOWNLOAD_FILE, true, msg);
                                }

                                @Override
                                public void onFinish(boolean success, String msg) {
                                    sendRespMessage(MSG_DOWNLOAD_FILE, success, msg);
                                }
                            });
                } catch (Exception e) {
                    e.printStackTrace();
                }

 private void sendRespMessage(int msgWhat, boolean success, String content) {
        String status = (success ? "成功:\n" : "失敗:\n");
        Message msg = Message.obtain();
        msg.what = msgWhat;
        msg.obj = status + content;
        mHandler.sendMessage(msg);
    }

 private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            String content = (String) msg.obj;
            switch (msg.what) {
                case MSG_HTTP_RESPONCE:
                    mTvRespContent.setText(Html.fromHtml(content));
                    break;
                case MSG_DOWNLOAD_FILE:
                    mTvRespContent.setTextSize(16);
                    mTvRespContent.setText(content);
                    break;
            }
        }
    };

https相關請求

okhttp預設是支援https請求的,比如訪問https://baidu.com等,但需要注意的是,okhttp支援的網站是由CA機構頒發的證書,預設支援。但對於使用自簽名的證書的網站比如12306網站https://kyfw.12306.cn/otn/會直接報錯:

javax.net.ssl.SSLHandshakeException: 
    java.security.cert.CertPathValidatorException: 
        Trust anchor for certification path not found.

那為什麼會報這個錯誤,https請求和http有什麼區別呢?

https相關知識

https是個安全版的http,它是在http的基礎加一層ssl/tsl
這裡寫圖片描述

SSL (Secure Sockets Layer) 是一種在客戶端跟伺服器端建立一個加密連線的安全標準. 一般用來加密網路伺服器跟瀏覽器, 或者是郵件伺服器跟郵件客戶端(如: Outlook)之間傳輸的資料。
它能夠:

  • 認證使用者和伺服器,確保資料傳送到正確的客戶機和伺服器;(驗證證書)
  • 加密資料以防止資料中途被竊取;(加密)
  • 維護資料的完整性,確保資料在傳輸過程中不被改變。(摘要演算法)

下面我們簡單描述下HTTPS的工作原理:

  1. 客戶端向伺服器端索要並驗證公鑰。
  2. 雙方協商生成“對話金鑰”。
  3. 雙方採用“對話金鑰”進行加密通訊。

上面過程的前兩步,又稱為“握手階段”。
這裡寫圖片描述
TTPS在傳輸資料之前需要客戶端(瀏覽器)與服務端(網站)之間進行一次握手,在握手過程中將確立雙方加密傳輸資料的密碼資訊。握手過程的簡單描述如下:

  1. 瀏覽器將自己支援的一套加密演算法、HASH演算法傳送給網站。
  2. 網站從中選出一組加密演算法與HASH演算法,並將自己的身份資訊以證書的形式發回給瀏覽器。證書裡面包含了網站地址,加密公鑰,以及證書的頒發機構等資訊。
  3. 瀏覽器獲得網站證書之後,開始驗證證書的合法性,如果證書信任,則生成一串隨機數字作為通訊過程中對稱加密的祕鑰。然後取出證書中的公鑰,將這串數字以及HASH的結果進行加密,然後發給網站。
  4. 網站接收瀏覽器發來的資料之後,通過私鑰進行解密,然後HASH校驗,如果一致,則使用瀏覽器發來的數字串使加密一段握手訊息發給瀏覽器。
  5. 瀏覽器解密,並HASH校驗,沒有問題,則握手結束。接下來的傳輸過程將由之前瀏覽器生成的隨機密碼並利用對稱加密演算法進行加密。

至此,整個握手階段全部結束。接下來,客戶端與伺服器進入加密通訊,就完全是使用普通的HTTP協議,只不過用“會話金鑰”加密內容。

訪問自簽名的網站

瞭解了https基礎知識後,我們來看看如何訪問自簽名的網站。

第一種方式:直接忽略證書,不進行認證。

 /**
     * 忽略Https證書
     * @return
     */
    public static OkHttpClient getUnSafedOkHttpClient() {
        // Create a trust manager that does not validate certificate chains
        TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            @Override
            public void checkClientTrusted(java.security.cert.X509Certificate[] chain,
                                           String authType) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(java.security.cert.X509Certificate[] chain,
                                           String authType) throws CertificateException {
            }

            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                X509Certificate[] x509Certificates = new X509Certificate[0];
                return x509Certificates;
            }
        }};

        // Install the all-trusting trust manager
        try {
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
            // Create an ssl socket factory with our all-trusting manager
            javax.net.ssl.SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .sslSocketFactory(sslSocketFactory)
                    .hostnameVerifier(new HostnameVerifier() {

                        @Override
                        public boolean verify(String hostname, SSLSession session) {
                            return true;

                        }
                    })
                    .build();
            return okHttpClient;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

第二種方式: 證書認證
首先,匯出自簽名網站的證書,以12306網站為例。
這裡寫圖片描述

然後,程式碼中新增證書

  /**
     * 新增https證書(多個)
     * @param certificates
     */
    public static OkHttpClient getOkHttpClientByCertificate(InputStream... certificates) {
        try {
            //構造CertificateFactory物件,通過它的generateCertificate(is)方法得到Certificate
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates) {
                String certificateAlias = Integer.toString(index++);
                //將得到的Certificate放入到keyStore中
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
                try {
                    if (certificate != null) {
                        certificate.close();
                    }
                } catch (IOException e) {
                }
            }
            SSLContext sslContext = SSLContext.getInstance("TLS");
            final TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            //通過keyStore初始化TrustManagerFactory
            trustManagerFactory.init(keyStore);
            //由trustManagerFactory.getTrustManagers獲得TrustManager[]初始化SSLContext
            sslContext.init(null,
                    trustManagerFactory.getTrustManagers(),
                    new SecureRandom());
            javax.net.ssl.SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

            //設定sslSocketFactory
            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .sslSocketFactory(sslSocketFactory)
                    .hostnameVerifier(new HostnameVerifier() {

                        @Override
                        public boolean verify(String hostname, SSLSession session) {
                            return true;

                        }
                    })
                    .build();
            return okHttpClient;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

最後,用得到的okhttpclient 例項發起https請求。

 Request request = new Request.Builder()
                .url(url)
                .build();
        OkHttpClient client = getOkHttpClient(context, request.isHttps(), needVerify);
        Response response = client.newCall(request).execute();

參考資料

本篇部落格參考: