OkHttp系列——使用教程

概述:

OkHttp
An HTTP client for Android, Kotlin, and Java.

OkHttp是一个默认高效的HTTP客户端:

  • HTTP/2支持允许对同一主机的所有请求共享一个套接字。
  • 连接池减少了请求延迟。
  • 透明GZIP压缩下载大小。
  • 响应缓存完全避免了网络重复请求。

基本使用:

添加依赖

implementation(“com.squareup.okhttp3:okhttp:3.14.2”)

常用举例

最常见的请求就是post 与 get,请求又分为阻塞式与异步请求两种,下面举例常用请求,分别介绍其使用。

(一)Http Get

  • 同步 Http Get
    /**
     * 1.同步Get  client.newCall(request).execute()
     * @throws Exception
     */
    public void  synchronousGet() throws Exception {
        //创建OkHttpClient
        OkHttpClient client = new OkHttpClient();
        //构建Request
        Request request = new Request.Builder()
                .url("https://publicobject.com/helloworld.txt")
                .build();
        //发起请求,等待结果
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            Headers responseHeaders = response.headers();
            for (int i = 0; i < responseHeaders.size(); i++) {
                System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
            }
            ResponseBody responseBody = response.body();
            responseBody.string(); // Returns the response as a string.
            InputStream inputStream = responseBody.byteStream();
            responseBody.contentLength();
           //Returns the number of bytes in that will returned by {@link #bytes}, or {@link #byteStream}, or * -1 if unknown. 

        }
    }

1.构建request信息,request 默认请求方式是get请求,也可以设置为post请求,还要通过Builder去指定请求的urlhttps://publicobject.com/helloworld.txt, 通过Builder还可以指定 设置headers、 设置RequestBody等等。
2.拿构建完成的request去生成一个Call对象,client.newCall(request)
3.拿Call对象执行execute(),等待结果Response
以上就是发起一个同步get请求的过程。

ps:拿到response后,可以通过response.body()拿到responseBody,接下来调用responseBody. byteStream()可以转成Stream流,接下来可以进行文件读写,也可以通过responseBody.string()来将response转成字符串类型。

  • 异步 Http Get
    /**
     * 2.异步Get  client.newCall(request).enqueue
     * @throws Exception
     */
    public void asynchronousGet() throws Exception {
        //创建OkHttpClient
        OkHttpClient client = new OkHttpClient();
        //构建Request
        Request request = new Request.Builder()
                .url("https://publicobject.com/helloworld.txt")
                .build();
        //构建Call
        Call call = client.newCall(request);
        //加入队列将来某个时候执行的请求。
        call.enqueue(new Callback() {
            @Override 
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
            }

            @Override 
            public void onResponse(Call call, Response response) throws IOException {
                try (ResponseBody responseBody = response.body()) {
                    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
                    System.out.println(responseBody.string());
                }
            }
        });
    }

响应成功后 onResponse回调的参数是call 与 response,response与前面介绍的一致,封装了响应信息。call则是前面对request的包装。响应失败会回调到onFailure中进行自定义处理。

以上是异步get请求,同步请求是阻塞式的,`异步请求则不会阻塞线程。

(二)Http Post

Post请求,把参数作为body 传递,Body有几种类型 RequestBody、FormBody、MultipartBody。

  • POST提交表单(键值对)
    使用的是FormBody,把键值对数据传送到服务器。
    public void doPost(Map<String, Object> params) {
        Moshi moshi = new Moshi.Builder().build();
        JsonAdapter<Map> stringJsonAdapter = moshi.adapter(Map.class);
        String paramsString = stringJsonAdapter.toJson(params);
        //创建表单
        FormBody.Builder bodyBuilder = new  FormBody.Builder();
        //用data 统一封装 body
        Map<String, String> dataParam = new HashMap<>();
        dataParam.put("data",paramsString);
        Iterator<Map.Entry<String, String>> iterator = dataParam.entrySet().iterator();

        while (iterator.hasNext()) {
            //add form parameters key value
            Map.Entry<String, String> next = iterator.next();
            //添加body 键值对 userId:10000
            bodyBuilder.add(next.getKey(), next.getValue());
        }

        Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .post(bodyBuilder.build())
                .build();
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
        }catch (Exception e){

        }
    }

发起post请求的时,参数是包含在请求体中的;FormBody添加多个String键值对,然后去构造RequestBody,最后完成我们Request的构造。

POST http://www.baidu.com/ HTTP/1.1
os: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 47
Host: www.baidu.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.2

data=%7B%22search%22%3A%22Jurassic%20Park%22%7D
  • POST提交Json数据
    public void doPost() {
        //创建OkHttpClient对象
        OkHttpClient client = new OkHttpClient();
        //数据类型为json格式
        MediaType JSON = MediaType.parse("application/json; charset=utf-8");
        //json数据
        String jsonStr = "{\"username\":\"lisi\",\"nickname\":\"李四\"}";
        RequestBody body = RequestBody.create(JSON, jsonStr);
        Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .post(body)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

            }
        });
    }

可以看到发送的数据

    POST http://www.baidu.com/ HTTP/1.1
    Content-Type: application/json; charset=utf-8
    Content-Length: 39
    Host: www.baidu.com
    Connection: Keep-Alive
    Accept-Encoding: gzip
    User-Agent: okhttp/3.14.2

    {"username":"lisi","nickname":"李四"}
  • POST上传文件
    表单上传文件功能通过MultipartBody来实现。
    public void upload() throws Exception {
        // Use the imgur image upload API as documented at 
        // https://api.imgur.com/endpoints/image
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                // Add a form data part to the body. value   String
                .addFormDataPart("title", "Square Logo")
                .addFormDataPart("image", "logo-square.png",
                        RequestBody.create(MEDIA_TYPE_PNG, FileUtils.create("a100.png")))
                .build();

        Request request = new Request.Builder()
                .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
                .url("https://api.imgur.com/3/image")
                .post(requestBody)
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) 
            throw new IOException("Unexpected code " + response);
            Log.e("myokhttpapplication", "" + response.body().string());
        }
    }

(三)添加Header

    /**
     * 3.添加Header
     *
     * @throws Exception
     */
    public void accessingHeaders() throws Exception {
        Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .header("User-Agent", "OkHttp Headers.java")
                .addHeader("Accept", "application/json; q=0.5")
                .addHeader("Accept", "application/vnd.github.v3+json")
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            System.out.println("Server: " + response.header("Server"));
            System.out.println("Date: " + response.header("Date"));
            System.out.println("Vary: " + response.headers("Vary"));
        }
    }

可以看到

    GET http://www.baidu.com/ HTTP/1.1
    User-Agent: OkHttp Headers.java
    Accept: application/json; q=0.5
    Accept: application/vnd.github.v3+json
    Host: www.baidu.com
    Connection: Keep-Alive
    Accept-Encoding: gzip

(四)响应用Json解析成对象(Parse a JSON Response With Moshi)

   /**
     * Parse a JSON Response With Moshi
     */
    public void parseJSONResponseWithMoshi() throws Exception {
        Request request = new Request.Builder()
                .url("https://api.github.com/gists/c2a7c39532239ff261be")
                .build();
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            Gist gist = gistJsonAdapter.fromJson(response.body().source());

            for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
                System.out.println(entry.getKey());
                System.out.println(entry.getValue().content);
            }
        }
    }

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

    static class GistFile {
        String content;
    }

(五)响应缓存(Response Caching)

private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(cacheDirectory, cacheSize);

    client = new OkHttpClient.Builder()
        .cache(cache)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    String response1Body;
    try (Response response1 = client.newCall(request).execute()) {
      if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

      response1Body = response1.body().string();
      System.out.println("Response 1 response:          " + response1);
      System.out.println("Response 1 cache response:    " + response1.cacheResponse());
      System.out.println("Response 1 network response:  " + response1.networkResponse());
    }

    String response2Body;
    try (Response response2 = client.newCall(request).execute()) {
      if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

      response2Body = response2.body().string();
      System.out.println("Response 2 response:          " + response2);
      System.out.println("Response 2 cache response:    " + response2.cacheResponse());
      System.out.println("Response 2 network response:  " + response2.networkResponse());
    }

    System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
  }

(六)取消请求

使用call .cancel()立即停止正在进行的调用。如果线程当前正在发起请求或读取响应,它将接收IOException。当不再需要call时,使用此功能节约网络;例如,当用户从应用程序导航时。同步和异步调用都可以取消。

  private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://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);

    // Schedule a job to cancel the call in 1 second.
    executor.schedule(new Runnable() {
      @Override public void run() {
        System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
        call.cancel();
        System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
      }
    }, 1, TimeUnit.SECONDS);

    System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
    try (Response response = call.execute()) {
      System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, response);
    } catch (IOException e) {
      System.out.printf("%.2f Call failed as expected: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, e);
    }
  }

(七)设置超时时间

当对方不可达时,使用超时使调用失败。网络分区可能是由于client连接问题、服务器可用性问题或两者之间的任何问题造成的。OkHttp支持连接、读取和写入超时。

  private final OkHttpClient client;

  public ConfigureTimeouts() throws Exception {
    client = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    try (Response response = client.newCall(request).execute()) {
      System.out.println("Response completed: " + response);
    }
  }

拦截器 Interceptors

拦截器Interceptors 是一种功能强大的机制,可以监视、重写和重试调用。下面是一个简单的拦截器,它记录发出的请求和传入的响应。

class LoggingInterceptor implements Interceptor {
    MyLog logger = new MyLog();
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();
        long t1 = System.nanoTime();
        //  Returns the connection the request will be executed on. This is only available in the chains
        // of network interceptors; for application interceptors this is always null.
        logger.info(String.format("Sending request %s on %s%n %s",
                request.url(), chain.connection(), request.headers()));

        Response response = chain.proceed(request);

        long t2 = System.nanoTime();
        logger.info(String.format("Received response for %s in %.1fms%n%s",
                response.request().url(), (t2 - t1) / 1e6d, response.headers()));

        return response;
    }
}

chain.proceed(请求)的调用是每个拦截器实现的关键部分。这个看起来很简单的方法是所有HTTP工作发生的地方,它生成一个响应来满足请求。

拦截器可以被链接。假设您同时拥有一个压缩拦截器和一个校验和拦截器:您需要决定数据是被压缩后再校验和,还是校验和后再压缩。OkHttp使用列表跟踪拦截器,并按顺序调用拦截器。
在这里插入图片描述

(一)Application Interceptors

拦截器注册为application interceptors或network interceptors。我们将使用上面定义的LoggingInterceptor来显示差异。
通过在OkHttpClient.Builder上调用addInterceptor()注册一个application拦截器:

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

URL http://www.publicobject.com/helloworld.txt 重定向到 https://publicobject.com/helloworld.txt, OkHttp自动处理重定向。我们的应用程序拦截器调用一次,从chain.proceed()返回的响应有重定向响应:

INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example

INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

可以看到请求被重定向了,因为response.request().url()与request.url()不同。两个日志语句记录两个不同的url。

(二)Network Interceptors

注册网络拦截器非常类似。用addNetworkInterceptor()替换addInterceptor():

OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

当运行这段代码时,拦截器会运行两次。一次用于对http://www.publicobject.com/helloworld.txt的初始请求,另一次用于重定向到https://publicobject.com/helloworld.txt。

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt

INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

网络请求还包含更多的数据,比如由OkHttp添加的Accept-Encoding: gzip报头,以表示对响应压缩的支持。网络拦截器链有一个非空连接Connection,可用来查询用于连接到web服务器的IP地址和TLS配置。

(三)Application Interceptors与Network Interceptors的选择

每个拦截器链都有各自的优点。

Application interceptors

  • 不需要担心像重定向和重试这样的中间响应。
  • 总是调用一次,即使HTTP响应是从缓存中提供的。
  • 观察应用程序的原始意图。不关心okhttp注入的头文件,比如If-None-Match。
  • 允许短路,不调用Chain.proceed()。
  • 允许重试并多次调用Chain.proceed()。

Network Interceptors

  • 能够操作中间响应,如重定向和重试。
  • 未为导致网络短路的缓存响应调用。
  • 只观察将通过网络传输的数据。
  • 可以拿到承载请求的Connection连接。

(四)重写Request

拦截器可以添加、删除或替换请求头。它们还可以转换一个请求的请求体。例如,如果连接到已知支持请求体压缩的web服务器,可以使用应用application interceptor添加请求体压缩。

/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
final class GzipRequestInterceptor implements Interceptor {
  @Override 
  public Response intercept(Interceptor.Chain chain) throws IOException {
    Request originalRequest = chain.request();
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
      return chain.proceed(originalRequest);
    }

    Request compressedRequest = originalRequest.newBuilder()
        .header("Content-Encoding", "gzip")
        .method(originalRequest.method(), gzip(originalRequest.body()))
        .build();
    return chain.proceed(compressedRequest);
  }

  private RequestBody gzip(final RequestBody body) {
    return new RequestBody() {
      @Override 
      public MediaType contentType() {
        return body.contentType();
      }

      @Override 
      public long contentLength() {
        return -1; // We don't know the compressed length in advance!
      }

      @Override 
      public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
        body.writeTo(gzipSink);
        gzipSink.close();
      }
    };
  }
}

(五)重写Response

对称地,拦截器可以重写响应头并转换响应体。这通常比重写请求头更危险,因为它可能违反web服务器的期望!
如果您处于一种棘手的情况,并准备处理其结果,重写响应头是解决问题的一种有效方法。例如,您可以修复服务器配置错误的Cache-Control响应头,以启用更好的响应缓存:

/** Dangerous interceptor that rewrites the server's cache-control header. */
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
  @Override 
  public Response intercept(Interceptor.Chain chain) throws IOException {
    Response originalResponse = chain.proceed(chain.request());
    return originalResponse.newBuilder()
        .header("Cache-Control", "max-age=60")
        .build();
  }
};

HTTPS

OkHttp试图平衡两个相互竞争的问题:

  • 连接到尽可能多的主机。这包括运行最新版本boringssl的高级主机,以及运行较老版本OpenSSL的过时主机。

  • 连接的安全性。这包括使用证书验证远程web服务器,以及使用强密码交换数据的隐私。

当协商到HTTPS服务器的连接时,OkHttp需要知道要提供哪些TLS版本和密码套件。想要最大限度地连接的客户机将包括过时的TLS版本和按设计而设计的密码套件。一个想要最大限度地提高安全性的严格客户端将仅限于最新的TLS版本和最强的密码套件。

特定的安全性和连接性决策由ConnectionSpec实现。OkHttp包含四个内置连接规范:

  • RESTRICTED_TLS 是一种安全配置,旨在满足更严格的遵从性需求。
  • MODERN_TLS 是一个连接到现代HTTPS服务器的安全配置。
  • COMPATIBLE_TLS 是一个安全的配置,它连接到安全但不是当前https服务器。
  • CLEARTEXT 是一种不安全的配置,用于http:// url。

默认情况下,OkHttp将尝试一个MODERN_TLS连接。然而,通过配置 connectionSpecs,如果 modern configuration失败,可以允许返回到COMPATIBLE_TLS连接。

OkHttpClient client = new OkHttpClient.Builder()
    .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
    .build();

你可以使用一组自定义的TLS版本和密码套件来构建自己的连接规范。例如,这种配置被限制在三个高度重视的密码套件中。它的缺点是需要Android 5.0+和类似的当前web服务器。

ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
    .tlsVersions(TlsVersion.TLS_1_2)
    .cipherSuites(
          CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
          CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
          CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)
    .build();

OkHttpClient client = new OkHttpClient.Builder()
    .connectionSpecs(Collections.singletonList(spec))
    .build();

证书固定

默认情况下,OkHttp信任主机平台的证书颁发机构。这种策略最大限度地提高了连接性,但它也受到证书颁发机构攻击的影响,比如2011年的DigiNotar攻击。它还假定HTTPS服务器的证书由证书颁发机构签名。
使用CertificatePinner来限制信任哪些证书和证书颁发机构。证书固定增加了安全性,但限制了服务器团队更新其TLS证书的能力。 如果没有您的服务器的TLS管理员的支持,不要使用证书固定!

  public CertificatePinning() {
    client = new OkHttpClient.Builder()
        .certificatePinner(new CertificatePinner.Builder()
            .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
            .build())
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/robots.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    for (Certificate certificate : response.handshake().peerCertificates()) {
      System.out.println(CertificatePinner.pin(certificate));
    }
  }

Customizing Trusted Certificates¶

The full code sample shows how to replace the host platform’s certificate authorities with your own set. As above, do not use custom certificates without the blessing of your server’s TLS administrator!

定制信任证书

完整的代码示例展示了如何用您自己的证书集替换主机平台的证书颁发机构。如上所述,如果没有得到服务器的TLS管理员的许可,不要使用自定义证书!

  private final OkHttpClient client;

  public CustomTrust() {
    SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream());
    client = new OkHttpClient.Builder()
        .sslSocketFactory(sslContext.getSocketFactory())
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

    Response response = client.newCall(request).execute();
    System.out.println(response.body().string());
  }

  private InputStream trustedCertificatesInputStream() {
    ... // Full source omitted. See sample.
  }

  public SSLContext sslContextForTrustedCertificates(InputStream in) {
    ... // Full source omitted. See sample.
  }

Events

事件允许您捕获应用程序HTTP调用的度量。使用事件来监视:
应用程序发出的HTTP调用的大小和频率。如果你的calls得太多,或者你的calls太大,你应该知道!
这些调用在底层网络上的性能。如果网络的性能不够好,则需要改进网络或减少使用网络。

EventListener

子类EventListener及override methods你感兴趣的方法。在没有重定向或重试的成功HTTP调用中,事件序列由该流描述。
在这里插入图片描述
Here’s a sample event listener that prints each event with a timestamp.下面是一个event listener示例,它使用时间戳打印每个事件。

class PrintingEventListener extends EventListener {
  private long callStartNanos;

  private void printEvent(String name) {
    long nowNanos = System.nanoTime();
    if (name.equals("callStart")) {
      callStartNanos = nowNanos;
    }
    long elapsedNanos = nowNanos - callStartNanos;
    System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
  }

  @Override 
  public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override 
  public void callEnd(Call call) {
    printEvent("callEnd");
  }

  @Override 
  public void dnsStart(Call call, String domainName) {
    printEvent("dnsStart");
  }

  @Override 
  public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
    printEvent("dnsEnd");
  }

  ...
}

我们发起几个请求

Request request = new Request.Builder()
    .url("https://publicobject.com/helloworld.txt")
    .build();

System.out.println("REQUEST 1 (new connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}

System.out.println("REQUEST 2 (pooled connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}

监听器打印相应的事件:

REQUEST 1 (new connection)
0.000 callStart
0.010 dnsStart
0.017 dnsEnd
0.025 connectStart
0.117 secureConnectStart
0.586 secureConnectEnd
0.586 connectEnd
0.587 connectionAcquired
0.588 requestHeadersStart
0.590 requestHeadersEnd
0.591 responseHeadersStart
0.675 responseHeadersEnd
0.676 responseBodyStart
0.679 responseBodyEnd
0.679 connectionReleased
0.680 callEnd
REQUEST 2 (pooled connection)
0.000 callStart
0.001 connectionAcquired
0.001 requestHeadersStart
0.001 requestHeadersEnd
0.002 responseHeadersStart
0.082 responseHeadersEnd
0.082 responseBodyStart
0.082 responseBodyEnd
0.083 connectionReleased
0.083 callEnd

请注意,如何不对第二个调用触发connect事件。它重用了第一个请求中的连接,以获得更好的性能。

EventListener.Factory

In the preceding example we used a field, callStartNanos, to track the elapsed time of each event. This is handy, but it won t work if multiple calls are executing concurrently. To accommodate this, use a Factory to create a new EventListener instance for each Call. This allows each listener to keep call-specific state.
在前面的示例中,我们使用了一个字段callStartNanos来跟踪每个事件的运行时间。这很方便,但如果同时执行多个调用,则无法工作。为了解决这种情况,可以使用工厂为每个调用创建一个新的EventListener实例。这允许每个侦听器保持特定于调用的状态。

class PrintingEventListener extends EventListener {
  public static final Factory FACTORY = new Factory() {
    final AtomicLong nextCallId = new AtomicLong(1L);

    @Override 
    public EventListener create(Call call) {
      long callId = nextCallId.getAndIncrement();
      System.out.printf("%04d %s%n", callId, call.request().url());
      return new PrintingEventListener(callId, System.nanoTime());
    }
  };

  final long callId;
  final long callStartNanos;

  public PrintingEventListener(long callId, long callStartNanos) {
    this.callId = callId;
    this.callStartNanos = callStartNanos;
  }

  private void printEvent(String name) {
    long elapsedNanos = System.nanoTime() - callStartNanos;
    System.out.printf("%04d %.3f %s%n", callId, elapsedNanos / 1000000000d, name);
  }

  @Override 
  public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override 
  public void callEnd(Call call) {
    printEvent("callEnd");
  }

  ...
}

我们可以使用这个监听器来跟踪一对并发的HTTP请求:

Request washingtonPostRequest = new Request.Builder()
    .url("https://www.washingtonpost.com/")
    .build();
client.newCall(washingtonPostRequest).enqueue(new Callback() {
  ...
});

Request newYorkTimesRequest = new Request.Builder()
    .url("https://www.nytimes.com/")
    .build();
client.newCall(newYorkTimesRequest).enqueue(new Callback() {
  ...
});

在家庭WiFi上运行这个跟踪显示时间(0002)比Post(0001)稍早完成:

0001 https://www.washingtonpost.com/
0001 0.000 callStart
0002 https://www.nytimes.com/
0002 0.000 callStart
0002 0.010 dnsStart
0001 0.013 dnsStart
0001 0.022 dnsEnd
0002 0.019 dnsEnd
0001 0.028 connectStart
0002 0.025 connectStart
0002 0.072 secureConnectStart
0001 0.075 secureConnectStart
0001 0.386 secureConnectEnd
0002 0.390 secureConnectEnd
0002 0.400 connectEnd
0001 0.403 connectEnd
0002 0.401 connectionAcquired
0001 0.404 connectionAcquired
0001 0.406 requestHeadersStart
0002 0.403 requestHeadersStart
0001 0.414 requestHeadersEnd
0002 0.411 requestHeadersEnd
0002 0.412 responseHeadersStart
0001 0.415 responseHeadersStart
0002 0.474 responseHeadersEnd
0002 0.475 responseBodyStart
0001 0.554 responseHeadersEnd
0001 0.555 responseBodyStart
0002 0.554 responseBodyEnd
0002 0.554 connectionReleased
0002 0.554 callEnd
0001 0.624 responseBodyEnd
0001 0.624 connectionReleased
0001 0.624 callEnd

EventListener.Factory还可以将指标限制为调用的子集。这个捕获随机10%的指标:

class MetricsEventListener extends EventListener {
  private static final Factory FACTORY = new Factory() {
    @Override public EventListener create(Call call) {
      if (Math.random() < 0.10) {
        return new MetricsEventListener(call);
      } else {
        return EventListener.NONE;
      }
    }
  };

  ...
}

Events with Failures

当操作失败时,将调用失败方法。用于在构建到服务器的连接时发生故障connectFailed()被执行,当HTTP调用永久失败时会调用callFailed()。当发生故障时,开始事件可能没有相应的结束事件。
在这里插入图片描述

总结

官方建议我们在使用时,尽量复用OkHttpClient,而不是频繁创建OkHttpClient,因此在使用之前,需要做一下封装。

谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。
欢迎爱学习的小伙伴加群一起进步:230274309

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页