OkHttp:基本使用詳解

簡介

OkHttp是一個高效的HTTP客戶端,它有以下默認特性:

  • 支持HTTP/2,允許所有同一個主機地址的請求共享同一個socket連接
  • 連接池減少請求延時
  • 透明的GZIP壓縮減少響應數據的大小
  • 緩存響應內容,避免一些完全重複的請求

當網絡出現問題的時候OkHttp依然堅守自己的職責,它會自動恢復一般的連接問題,如果你的服務有多個IP地址,當第一個IP請求失敗時,OkHttp會交替嘗試你配置的其他IP,這對於使用IPv4+IPv6託管在冗餘數據中心中的服務是必需的,OkHttp使用現代TLS技術(TLS 1.3、ALPN、證書固定)初始化新的連接,當握手失敗時會回退到TLS 1.0。

OkHttp官網地址:http://square.github.io/okhttp/
OkHttp GitHub地址:https://github.com/square/okhttp

本文代碼下載地址:GitHub

使用

implementation 'com.squareup.okhttp3:okhttp:3.14.2'

我使用的是3.14.2版本,4.XX.XX版本使用的是Kotlin

需要在清單文件聲明訪問Internet的權限,如果使用緩存,那還得聲明寫外存的權限

同步Get請求

   /**
     * 異步GET請求:
     * new OkHttpClient;
     * 構造Request對象;
     * 通過前兩步中的對象構建Call對象;
     * 通過Call#enqueue(Callback)方法來提交異步請求;
     */
    private void asynchronousGetRequests() {
        String url = "https://wwww.baidu.com";
        OkHttpClient okHttpClient = new OkHttpClient();
        final Request request = new Request.Builder()
                .url(url)
                .get()//默認就是GET請求
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("asynchronousGetRequests onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                runOnUiThread(() -> tvContent.setText("asynchronousGetRequests onResponse: " + result));

            }
        });
    }

異步發起的請求會被加入到 Dispatcher 中的 runningAsyncCalls雙端隊列中通過線程池來執行。

響應體的 string() 方法對於小文檔來說十分方便、高效。但是如果響應體太大(超過1MB),應避免適應 string()方法,因爲他會將把整個文檔加載到內存中。對於超過1MB的響應body,應使用流的方式來處理body。

同步Get請求

在Android中應放在子線程中執行,否則有可能引起ANR異常,Android3.0 以後已經不允許在主線程訪問網絡。

    /**
     * 同步GET請求
     * new OkHttpClient;
     * 構造Request對象;
     * 通過前兩步中的對象構建Call對象;
     * 在子線程中通過Call#execute()方法來提交同步請求;
     */
    private void synchronizedGetRequests() {
        String url = "https://wwww.baidu.com";
        OkHttpClient okHttpClient = new OkHttpClient();
        final Request request = new Request.Builder()
                .url(url)
                .get()//默認就是GET請求
                .build();
        final Call call = okHttpClient.newCall(request);
        new Thread(() -> {
            try {
                //直接execute call
                Response response = call.execute();
                String result = response.body().string();
                runOnUiThread(() -> tvContent.setText("synchronizedGetRequests run: " + result));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }

POST方式提交String

RequstBody的幾種構造方式:

RequstBody的幾種構造方式:

    /**
     * POST方式提交String
     * 在構造 Request對象時,需要多構造一個RequestBody對象,攜帶要提交的數據。
     * 在構造 RequestBody 需要指定MediaType,用於描述請求/響應 body 的內容類型
     */
    private void postString() {
        MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8");
        String requestBody = "I am zza.";
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(RequestBody.create(mediaType, requestBody))
                .build();
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("postString onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                StringBuffer buffer = new StringBuffer();
                buffer.append("postString: " + "\r\n");
                buffer.append(response.protocol() + " " + response.code() + " " + response.message() + "\r\n");
                Headers headers = response.headers();
                for (int i = 0; i < headers.size(); i++) {
                    buffer.append(headers.name(i) + ":" + headers.value(i) + "\r\n");
                }
                buffer.append("onResponse: " + response.body().string());

                runOnUiThread(() -> tvContent.setText(buffer.toString()));
            }
        });
    }

POST方式提交流

    /**
     * POST方式提交流
     */
    private void postStream() {
        RequestBody requestBody = new RequestBody() {
            @Nullable
            @Override
            public MediaType contentType() {
                return MediaType.parse("text/x-markdown; charset=utf-8");
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                sink.writeUtf8("I am zza.");
            }
        };

        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(requestBody)
                .build();
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("postStream onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                StringBuffer buffer = new StringBuffer();
                buffer.append("postStream: " + "\r\n");
                buffer.append(response.protocol() + " " + response.code() + " " + response.message() + "\r\n");
                Headers headers = response.headers();
                for (int i = 0; i < headers.size(); i++) {
                    buffer.append(headers.name(i) + ":" + headers.value(i) + "\r\n");
                }
                buffer.append("onResponse: " + response.body().string());

                runOnUiThread(() -> tvContent.setText(buffer.toString()));
            }
        });
    }

POST提交文件

    /**
     * POST提交文件
     * 文件沒有的話會失敗
     * 需要在路徑下添加文件
     * 還需要權限
     */
    private void postFile() {
        MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8");
        OkHttpClient okHttpClient = new OkHttpClient();
        String path = Environment.getExternalStorageDirectory().getPath() + "/zza.md";
        File file = new File(path);
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(RequestBody.create(mediaType, file))
                .build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("postFile onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                StringBuffer buffer = new StringBuffer();
                buffer.append("postFile: " + "\r\n");
                buffer.append(response.protocol() + " " + response.code() + " " + response.message() + "\r\n");
                Headers headers = response.headers();
                for (int i = 0; i < headers.size(); i++) {
                    buffer.append(headers.name(i) + ":" + headers.value(i) + "\r\n");
                }
                buffer.append("postFile: " + response.body().string());

                runOnUiThread(() -> tvContent.setText(buffer.toString()));
            }
        });
    }

POST方式提交表單

提交表單時,使用 RequestBody 的實現類FormBody來描述請求體,它可以攜帶一些經過編碼的 key-value 請求體,鍵值對存儲在下面兩個集合中:

  private final List<String> encodedNames;
  private final List<String> encodedValues;
   /**
     * POST方式提交表單
     * 通過FormBody#Builder構造RequestBody
     */
    private void postForm() {
        //https://www.wanandroid.com/article/query/0/json
        //方法:POST
        //參數:
        //  頁碼:拼接在鏈接上,從0開始。
        //  k : 搜索關鍵詞
        RequestBody requestBody = new FormBody.Builder()
                .add("k", "okhttp")
                .build();
        Request request = new Request.Builder()
                .url("https://www.wanandroid.com/article/query/0/json")
                .post(requestBody)
                .build();
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("postForm onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                StringBuffer buffer = new StringBuffer();
                buffer.append("postForm: " + "\r\n");
                buffer.append(response.protocol() + " " + response.code() + " " + response.message() + "\r\n");
                Headers headers = response.headers();
                for (int i = 0; i < headers.size(); i++) {
                    buffer.append(headers.name(i) + ":" + headers.value(i) + "\r\n");
                }
                buffer.append("postForm: " + response.body().string());

                runOnUiThread(() -> tvContent.setText(buffer.toString()));
            }
        });
    }

POST方式提交分塊請求

MultipartBody 可以構建複雜的請求體,與HTML文件上傳形式兼容。多塊請求體中每塊請求都是一個請求體,可以定義自己的請求頭。這些請求頭可以用來描述這塊請求,例如它的 Content-Disposition。如果 Content-LengthContent-Type可用的話,他們會被自動添加到請求頭中。

    /**
     * POST方式提交分塊請求
     * <p>
     * 使用公司項目的一個接口調試通過,爲避免一些問題,接口和參數刪掉
     */
    private void postMultipartBody() {
        OkHttpClient client = new OkHttpClient();

        File file = new File(Environment.getExternalStorageDirectory().getPath() + "/logo_star_dust.png");
        MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
        RequestBody filebody = MultipartBody.create(MEDIA_TYPE_PNG, file);

        MultipartBody body = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("data", file.getName(), filebody)
                .addPart(
                        Headers.of("Content-Disposition", "form-data; name=\"vin\""),
                        RequestBody.create(null, ""))
                .addPart(
                        Headers.of("Content-Disposition", "form-data; name=\"iccid\""),
                        RequestBody.create(null, ""))
                .addPart(
                        Headers.of("Content-Disposition", "form-data; name=\"type\""),
                        RequestBody.create(null, ""))
                .addPart(
                        Headers.of("Content-Disposition", "form-data; name=\"jobId\""),
                        RequestBody.create(null, ""))
                .build();

        Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .post(body)
                .build();
        Call call = client.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("postMultipartBody onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                runOnUiThread(() -> tvContent.setText("postMultipartBody onResponse: " + result));
            }

        });
    }

攔截器

OkHttp的攔截器鏈可謂是其整個框架的精髓,用戶可傳入的 interceptor分爲兩類:

  • 應用攔截器(Interceptors)

    • addInterceptor()
    • 不必要擔心響應和重定向之間的中間響應。
    • 通常只調用一次,即使HTTP響應是通過緩存提供的。
    • 遵從應用層的最初目的。與OkHttp的注入頭部無關,如If-None-Match。
    • 允許短路而且不調用Chain.proceed()。
    • 允許重試和多次調用Chain.proceed()。
  • 網絡攔截器(networkInterceptors)

    • addNetworkInterceptor()
    • 允許像重定向和重試一樣操作中間響應。
    • 網絡發生短路時不調用緩存響應。
    • 在數據被傳遞到網絡時觀察數據。
    • 有權獲得裝載請求的連接。
    /**
     * 攔截器
     * <p>
     * 請查看打印輸出
     */
    private void testInterceptor() {
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new LoggingInterceptor())
                .build();
        Request request = new Request.Builder()
                .url("https://www.publicobject.com/helloworld.txt")
                .header("User-Agent", "OkHttp Example")
                .build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("testInterceptor onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                ResponseBody body = response.body();
                String result = response.body().string();
                if (body != null) {
                    LogUtil.d("testInterceptor: " + result);
                    body.close();
                }
                runOnUiThread(() -> tvContent.setText("testInterceptor onResponse: " + result));
            }
        });
    }

支持緩存

爲了緩存響應,你需要一個你可以讀寫的緩存目錄,和緩存大小的限制。這個緩存目錄應該是私有的,不信任的程序應不能讀取緩存內容。

一個緩存目錄同時擁有多個緩存訪問是錯誤的。大多數程序只需要調用一次new OkHttp(),在第一次調用時配置好緩存,然後其他地方只需要調用這個實例就可以了。否則兩個緩存示例互相干擾,破壞響應緩存,而且有可能會導致程序崩潰。

    /**
     * 支持緩存
     */
    private void responseCaching() {
        String url = "https://publicobject.com/helloworld.txt";
        File cacheDirectory = new File(Environment.getExternalStorageDirectory() + "/cache");
        int cacheSize = 10 * 1024 * 1024; // 10 MiB
        Cache cache = new Cache(cacheDirectory, cacheSize);

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cache(cache)
                .build();

        final Request request = new Request.Builder()
                .url(url)
                .get()//默認就是GET請求
                .build();
        final Call call1 = okHttpClient.newCall(request);
        final Call call2 = okHttpClient.newCall(request);
        new Thread(() -> {
            try {
                StringBuffer buffer = new StringBuffer();
                //直接execute call
                Response response1 = call1.execute();
                if (!response1.isSuccessful()) {
                    runOnUiThread(() -> tvContent.setText("responseCaching onFailure"));
                } else {
                    String result = response1.body().string();

                    buffer.append("responseCaching1 onResponse:   " + result + "\r\n");
                    buffer.append("responseCaching1 cache response:    " + response1.cacheResponse() + "\r\n");
                    buffer.append("responseCaching1 network response:  " + response1.networkResponse() + "\r\n");
                    runOnUiThread(() -> tvContent.setText(buffer));
                }
                Response response2 = call2.execute();
                if (!response2.isSuccessful()) {
                    runOnUiThread(() -> tvContent.setText("responseCaching onFailure"));
                } else {
                    String result = response2.body().string();
                    buffer.append("responseCaching2 onResponse:   " + result + "\r\n");
                    buffer.append("responseCaching2 cache response:    " + response2.cacheResponse() + "\r\n");
                    buffer.append("responseCaching2 network response:  " + response2.networkResponse() + "\r\n");
                    runOnUiThread(() -> tvContent.setText(buffer));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }

爲了防止使用緩存的響應,可以用CacheControl.FORCE_NETWORK。爲了防止它使用網絡,使用CacheControl.FORCE_CACHE。需要注意的是:如果您使用FORCE_CACHE和網絡的響應需求,OkHttp則會返回一個504提示,告訴你不可滿足請求響應。

取消請求

使用Call.cancel()可以立即停止掉一個正在執行的call。如果一個線程正在寫請求或者讀響應,將會引發IOException。當call沒有必要的時候,使用這個api可以節約網絡資源。例如當用戶離開一個應用時。

不管同步還是異步的call都可以取消。

你可以通過tags來同時取消多個請求。當你構建一請求時,使用RequestBuilder.tag(tag)來分配一個標籤。之後你就可以用OkHttpClient.cancel(tag)來取消所有帶有這個tag的call。.

    /**
     * 取消請求
     */
    private void cancelCall() {
        final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
        final OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("https://httpbin.org/delay/2") // This URL is served with a 2 second delay.
                .build();

        final long startNanos = System.nanoTime();
        final Call call = client.newCall(request);

        StringBuffer buffer = new StringBuffer();
        // Schedule a job to cancel the call in 1 second.
        executor.schedule(new Runnable() {
            @Override
            public void run() {
                buffer.append(String.format("%.2f Canceling call.%n",
                        (System.nanoTime() - startNanos) / 1e9f));
                call.cancel();
                buffer.append(String.format("%.2f Canceled call.%n",
                        (System.nanoTime() - startNanos) / 1e9f) );
                runOnUiThread(() -> tvContent.setText(buffer));
            }
        }, 1, TimeUnit.SECONDS);

        executor.schedule(new Runnable() {
            @Override
            public void run() {
                buffer.append(String.format("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f) );
                try (Response response = call.execute()) {
                    buffer.append(String.format("%.2f Call was expected to fail, but completed: %s%n",
                            (System.nanoTime() - startNanos) / 1e9f, response));
                } catch (IOException e) {
                    buffer.append(String.format("%.2f Call failed as expected: %s%n",
                            (System.nanoTime() - startNanos) / 1e9f, e) );
                }
            }
        }, 0, TimeUnit.SECONDS);
    }

超時

OkHttp支持連接,讀取和寫入超時。

    /**
     *  超時
     */
    private void timeout() {
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .readTimeout(10, TimeUnit.SECONDS)
                .build();

         Request request = new Request.Builder()
                .url("https://httpbin.org/delay/12")
                .get()
                .build();
        final Call call = client.newCall(request);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("timeout onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                StringBuffer buffer = new StringBuffer();
                buffer.append("timeout onResponse:   " + result + "\r\n");
                runOnUiThread(() -> tvContent.setText(buffer));

            }
        });
    }

每個call配置

使用OkHttpClient,所有的HTTP Client配置包括代理設置、超時設置、緩存設置。當你需要爲單個call改變配置的時候,clone 一個 OkHttpClient。這個api將會返回一個淺拷貝(shallow copy),你可以用來單獨自定義。

    /**
     * 每個call配置
     */
    private void perCallConfiguration() {
        OkHttpClient client = new OkHttpClient.Builder()
                .build();

        Request request = new Request.Builder()
                .url("https://httpbin.org/delay/1")
                .get()
                .build();

        OkHttpClient clientCopy = client.newBuilder()
                .readTimeout(500, TimeUnit.MILLISECONDS)
                .build();

        final Call call = clientCopy.newCall(request);
        StringBuffer buffer = new StringBuffer();
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("perCallConfiguration onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                buffer.append("perCallConfiguration onResponse:   " + result + "\r\n");
                runOnUiThread(() -> tvContent.setText(buffer));

            }
        });
    }

處理身份驗證

OkHttp會自動重試未驗證的請求。當響應是401 Not Authorized時,Authenticator會被要求提供證書。Authenticator的實現中需要建立一個新的包含證書的請求。如果沒有證書可用,返回null來跳過嘗試。

    /**
     * 處理身份驗證
     */
    private void handlingAuthentication() {
        StringBuffer buffer = new StringBuffer();

        OkHttpClient client = new OkHttpClient.Builder()
                .authenticator(new Authenticator() {

                    @Override
                    public Request authenticate(Route route, Response response) throws IOException {
                        if (response.request().header("Authorization") != null) {
                            return null; // Give up, we've already attempted to authenticate.
                        }
                        buffer.append("Authenticating for response: " + response + "\r\n");
                        buffer.append("Challenges: " + response.challenges() + "\r\n");

                        String credential = Credentials.basic("jesse", "password1");
                        return response.request().newBuilder()
                                .header("Authorization", credential)
                                .build();
                    }
                })
                .build();

        Request request = new Request.Builder()
                .url("http://publicobject.com/secrets/hellosecret.txt")
                .get()
                .build();

        final Call call = client.newCall(request);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> tvContent.setText("handlingAuthentication onFailure: " + e.getMessage()));
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                buffer.append("handlingAuthentication onResponse:   " + result + "\r\n");
                runOnUiThread(() -> tvContent.setText(buffer));

            }
        });
    }

其他

  • 推薦讓 OkHttpClient保持單例,用同一個 OkHttpClient實例來執行你的所有請求,因爲每一個 OkHttpClient實例都擁有自己的連接池和線程池,重用這些資源可以減少延時和節省資源,如果爲每個請求創建一個 OkHttpClient實例,顯然就是一種資源的浪費。

  • 每一個Call(其實現是RealCall)只能執行一次,否則會報異常,具體參見 RealCall#execute()

歡迎關注我的公衆號,持續分析優質技術文章
歡迎關注我的公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章