OkHttp wiki官方文档翻译(一)

Calls

请求

每一个Http请求包含一个URL,请求方法(比如GET Or POST),以及一系列的header。请求也有可能包含一个请求体:某种特殊类型的数据流。

响应

响应的内容包括一个响应码(比如200代表成功 或者 404代表未找到),响应头以及响应体。

重写请求

当你使用OkHttp发起一个Http请求,Okhttp会重写你的请求在真正发送该请求之前。
OkHttp会向你的请求添加如下的请求头,包括Content-Length,Transfer-Encoding, User-Agent, Host, Connection, and Content-Type. 为了实现响应压缩透明化,OKHttp会向请求中添加Accept-Encoding 请求头,除非已经存在了该请求头。如果你有Cookies,OkHttp会添加一个Cookie 请求头。
一些请求需要缓存,当缓存不是最新的时候,okHttp会发起一个GET请求更新缓存。这需要诸如If-Modified-Since 和If-None-Match添加进来。

重写响应

如果使用透明压缩,OKHttp会丢弃对应的Content-Encoding 和Content-Length响应头,因为这些响应头并不能用在解压响应体中。如果一个GET请求成功了,来自网络和缓存的相应会按照一定的规则合并。

后续请求

当你请求的URL已经被移除的时候,WebServer会返回一个302的状态码,以表名文档的新的URL,OkHttp会重定向以得到最终的响应。

如果响应要求授权验证,OkHttp会请求一个Authenticator (如果已经配置)以满足授权要求.如果authenticator 提供了一个凭证,请求会携带该证书进行请求重试。

请求重试.

有时候连接会失败:连接池中的连接过时、不可连,或者是WebServer本身不可达,OkHttp会通过不同的路由重试请求,如果有一个连接是可用的。

Calls

通过重写、重定向、后续和重试,你的一个简单的请求会产生很多请求和响应。OkHttp使用Call并通过许多必要的中间请求和响应来满足你请求的任务模型。通常情况下,这不是很多!如果你的URLs被重定向或者你切换到另外一个IP地址进行故障转移,你的代码仍然会正常工作。

Calls通过以下两种方式执行:

a)同步:你的线程会被阻塞,直到响应可读。

b)异步:在任意线程入队(队列)你的请求,当响应可读的时候,你可以在另一个线程得到Call Back回调。

Calls可以在任意线程被取消.这将使没有被执行完毕的Call执行失败!当一个call被取消的时候,正在写入请求体或者读取响应体的代码会得到一个IOException。

调度

对于同步请求,你需要管理并发请求,太多的并发连接浪费资源,太少又浪费延迟。

对于异步请求,Dispatcher实现了最大并发请求数策略.你可以设置每台WebServer(默认是5)和全部的最大并发数(默认64)。 

Connections

虽然你只是提供了一个URL,Okhttp计划使用三种类型连接到你的WebServer:URL,Address和Route。

URLs

URLs(比如https://github.com/square/okhttp)是Http和Internet的基础。除了是网络上通用和分散的命名方案,他们还指定了如何访问网络资源。

URLs是抽象的:

a)指定请求是明文的(http)或者加密的(https),但是并没有指定使用哪一种加密算法,也没有指定如何验证对等端的证书(HostnameVerifier)或者哪一种证书应该被信任(SSLSocketFactory)

b)没有指定使用哪种代理服务器或者如何与该服务器进行身份认证

Urls也是具体的:每个Url声明了特定的路径(比如/square/okhttp)和查询(比如?q=shark&lang=en).每个WebServer hosts需要URLs.

Address

Address指定了一个WebServer(比如github.com)和连接到一个服务器的所有必要的静态配置:端口号,Https设置和网络协议(比如Http/2或者SPDY)

共享相同address的Urls也会共享相同的TCP socket连接。共享连接有如下益处:低延迟、高吞吐量(由于TCP慢启动)和保养电池。OkHttp使用连接池自动的复用http/1.x连接和Http/2、SPDY连接。

在OkHttp中,地址的一些字段来自于URL(scheme、hostname、port)、剩下的来自于OkHttpClient。

Routes

路由(Routes)提供了连接WebServer的动态信息。那就是一个指定的IP地址(被DNS查询到的),代理服务器(如果一个ProxySelector在使用)和TLS协议版本(为了Https连接)。

对于一个地址可能对应多个路由信息.比如在多个数据中心托管的Web服务器,它可能会在其DNS响应产生多个IP地址。

Connections

当你使用OkHttp请求一个URL,OkHttp会做如下工作:

1、OkHttp会用URL和OkHttpClient的配置信息去创建一个地址(Address),这个地址指定了我们如何去连接一个Web服务器.

2、OkHttp会根据上述的地址尝试着从连接池中取回一个可用的连接

3、如果在连接池中没有发现一个可用的连接,OkHttp会选择一个路由(Route)去尝试.这就意味着会做一个DNS请求以获取一个服务器的IP地址.必要情况下,OkHttp会选择一个TLS的版本和代理服务器

4、如果是一个新的路由,OkHttp会建立一个Socket连接,一个TLS通道(为了Https和Http代理)或者一个TLS连接.OkHttp的TLS 握手是必要的

5、发送Http请求、读取响应

如果连接出现了问题,OkHttp会选择另外一个路由然后再次尝试。当一个服务器的地址的一个子集不可达时,OkHttp能够自动恢复。当连接池是过时或者试图TLS版本不受支持时,这种方式是很有用的。

一旦接收到了响应,连接就会被返回给连接池留着之后复用.连接在连接池中闲置一段时间后,它会被清除

Recipes

我们已经写了一些方法声明OkHttp是如何解决一些常见的问题.

同步Get

下载一个文件,打印headers和以String字符串的方式打印响应体。

响应体的string()方法对于小的文件是一个方便高效的方法.但是如果响应体超过1MB,应该避免使用string()因为它会将整个文件加载到内存中,在这种场景下,可以使用流来进行处理

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    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));
      }

      System.out.println(response.body().string());
    }
  }

异步Get

在工作线程下载一个文件,当响应可读的时候,会得到回调。这个回调在响应头准备好了之后产生.读取一个响应体可能会一直阻塞,目前OkHttp不提供异步API来接收响应体

private final OkHttpClient client = new OkHttpClient();

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

    client.newCall(request).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);

          Headers responseHeaders = response.headers();
          for (int i = 0, size = responseHeaders.size(); i < size; i++) {
            System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
          }

          System.out.println(responseBody.string());
        }
      }
    });
  }

获取headers

典型的Http Headers是Map<String,String>:每一个字段有一个值或者空值。但是一些headers允许多个值,像Guava的Multimap。比如,对于一个Http响应提供多个Vary 头。OkHttp的api试图使这两种情况都适用。

当写请求头的时候,使用header(name,value)去设置name对应的唯一值。如果已经存在了,将会被新添加的值覆盖。使用addHeader(name,value)添加一个头不会覆盖已经存在的值。

当读取响应头的时候,使用head(name)返回最后设置的value值.如果没有获取到,则header(name)会返回null。如果想读取一个字段的对应的值的列表,可以使用headers(name)方法

为了访问所有的头,可以使用headers类,该类支持通过索引获取值

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    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")
        .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"));
    }
  }

提交字符串

使用一个Http POST向服务发送一个请求体。这个例子是向Web服务发送了一个markdown文档,该Web服务可以将markdown文档渲染成一个HTML。因为整个请求体是被放置到内存中的,因此避免使用这个Api发送大文档(大于1MB)。

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

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    String postBody = ""
        + "Releases\n"
        + "--------\n"
        + "\n"
        + " * _1.0_ May 6, 2013\n"
        + " * _1.1_ June 15, 2013\n"
        + " * _1.2_ August 11, 2013\n";

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
        .build();

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

      System.out.println(response.body().string());
    }
  }
提交流

此处我们以流的形式提交请求体。请求体的内容由流写入产生。这个例子是流直接写入Okio的BufferedSink。你的程序可能会使用OutputStream,你可以使用BufferedSink.outputStream()来获取。

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

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody requestBody = new RequestBody() {
      @Override public MediaType contentType() {
        return MEDIA_TYPE_MARKDOWN;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.writeUtf8("Numbers\n");
        sink.writeUtf8("-------\n");
        for (int i = 2; i <= 997; i++) {
          sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
        }
      }

      private String factor(int n) {
        for (int i = 2; i < n; i++) {
          int x = n / i;
          if (x * i == n) return factor(x) + " × " + i;
        }
        return Integer.toString(n);
      }
    };

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(requestBody)
        .build();

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

      System.out.println(response.body().string());
    }
  }

提交文件

使用文件作为请求体是非常简单的。

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

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    File file = new File("README.md");

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
        .build();

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

      System.out.println(response.body().string());
    }
  }

提交表单参数

使用FormBody.Builder去构建一个请求体,作用和HTML中的<form>标签一样。键值对将使用一种HTML兼容形式的URL编码来进行编码。
private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody formBody = new FormBody.Builder()
        .add("search", "Jurassic Park")
        .build();
    Request request = new Request.Builder()
        .url("https://en.wikipedia.org/w/index.php")
        .post(formBody)
        .build();

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

      System.out.println(response.body().string());
    }
  }

提交分块请求

MultipartBody.Builder能够构建一个与HTML 文件上传表单相兼容的复杂请求体。分块请求的每一个部分本身就是一个请求体,可以单独定义其headers,这些请求头可以用来描述这块请求,例如他的Content-Disposition。如果Content-LengthContent-Type可用的话,他们会被自动添加到请求头中。
  /**
   * The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
   * these examples, please request your own client ID! https://api.imgur.com/oauth2
   */
  private static final String IMGUR_CLIENT_ID = "...";
  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

  private final OkHttpClient client = new OkHttpClient();

  public void run() 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)
        .addFormDataPart("title", "Square Logo")
        .addFormDataPart("image", "logo-square.png",
            RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.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);

      System.out.println(response.body().string());
    }
  }

使用MoShi解析一个JSON 响应

MoShi是一个很方便的Api,实现了Json与Java Objects之间的转换。下面这个例子,我们使用MoShi去解析一个来自GitHub Api的Json响应。

注意当解析一个响应体,ResponseBody.charStream() 会使用 Content-Type响应头选择使用哪一种字符集。默认情况下是UTF-8.

  private final OkHttpClient client = new OkHttpClient();
  private final Moshi moshi = new Moshi.Builder().build();
  private final JsonAdapter<Gist> gistJsonAdapter = moshi.adapter(Gist.class);

  public void run() 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;
  }

响应缓存

为了缓存响应,你需要一个你可以读写的缓存目录,和缓存大小的限制。这个缓存目录应该是私有的,不信任的程序应不能读取缓存内容。
一个缓存目录同时拥有多个缓存访问是错误的。大多数程序只需要调用一次new OkHttp(),在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,而且有可能会导致程序崩溃。
响应缓存使用HTTP头作为配置。你可以在请求头中添加Cache-Control: max-stale=3600 ,OkHttp缓存会支持。你的服务通过响应头确定响应缓存多长时间,例如使用Cache-Control: max-age=9600

有一些缓存heads会强制响应缓存,强制网络响应,或者通过GET请求强制网络响应。

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));
  }

为了防止响应使用缓存,可以使用CacheControl.FORCE_NETWORK  为了防止使用网络,可以使用CacheControl.FORCE_CACHE ,注意如果你使用了FORCE_CACHE,但是响应需要使用网络,那么OKHttp会返回504Unsatisfiable Request响应。

取消一个Call

使用Call.cancel()可以立即终止一个请求。如果一个线程正在进行写请求或者读响应,则会收到一个IOException异常。当call没有必要的时候,使用这个api可以节约网络资源。例如当用户离开一个应用时。不管同步还是异步的call都可以取消。

你可以通过tags来同时取消多个请求。当你构建一请求时,使用RequestBuilder.tag(tag)来分配一个标签。之后你就可以用OkHttpClient.cancel(tag)来取消所有带有这个tag的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);
    }
  }
超时

当服务端不可达的时候,超时机制可以使请求结束。没有响应的原因可能是客户点链接问题、服务器可用性问题或者这之间的其他东西。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);
    }
  }
每个Call的配置

OKHTTPClient的所有的配置包括代理设置、超时设置、缓存设置。当你需要为单个call改变配置的时候,调用OkHttpClient.newBuilder(),该方法返回共用一个连接池、分发器(dispatcher)和配置的原始Client。下面的例子中,我们让一个请求是500ms的超时、另一个是3000ms的超时。

 private final OkHttpClient client = new OkHttpClient();

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

    // Copy to customize OkHttp for this request.
    OkHttpClient client1 = client.newBuilder()
        .readTimeout(500, TimeUnit.MILLISECONDS)
        .build();
    try (Response response = client1.newCall(request).execute()) {
      System.out.println("Response 1 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 1 failed: " + e);
    }

    // Copy to customize OkHttp for this request.
    OkHttpClient client2 = client.newBuilder()
        .readTimeout(3000, TimeUnit.MILLISECONDS)
        .build();
    try (Response response = client2.newCall(request).execute()) {
      System.out.println("Response 2 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 2 failed: " + e);
    }
  }
处理验证

OKHttp会自动重试未验证的请求。当一个响应是401未授权的时候,Authenticator 会被要求提供证书,Authenticator的实现中需要建立一个新的包含证书的请求。如果没有证书可用,返回null来跳过尝试。

可以使用 Response.challenges() 方法获取任何一个authentication challenges的schemes and realms of any . 当需要实现一
Basic challenge, 使用Credentials.basic(username, password)来编码请求头。

private final OkHttpClient client;

  public Authenticate() {
    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.
            }

            System.out.println("Authenticating for response: " + response);
            System.out.println("Challenges: " + response.challenges());
            String credential = Credentials.basic("jesse", "password1");
            return response.request().newBuilder()
                .header("Authorization", credential)
                .build();
          }
        })
        .build();
  }

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

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

      System.out.println(response.body().string());
    }
  }
当授权无效的时候,为了避免多次重试,你可以返回null以放弃重试。比如,当这些证书被尝试验证之后,你想跳过重试

 if (credential.equals(response.request().header("Authorization"))) {
    return null; // If we already failed with these credentials, don't retry.
   }
当达到应用设置的重试次数之后,你可以跳过重试

  if (responseCount(response) >= 3) {
    return null; // If we've failed 3 times, give up.
  }

  private int responseCount(Response response) {
    int result = 1;
    while ((response = response.priorResponse()) != null) {
      result++;
    }
    return result;
  }

本篇翻译部分参考:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0106/2275.html,感谢。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值