okhttp翻译计划(三)--Recipes

第三部分:Recipes 用例

Recipes

我们写了一些用例来演示怎样用OkHttp来解决常见问题.你可以通过阅读这些代码来了解这些东西是怎么协同工作的,并且可以自由的复制粘贴这些例子,这正是他们的作用.

Synchronous Get 同步GET

下载一个文件,打印他的消息头并把他的响应体以字符串的形式打印出来.

对于小的文档来说,对响应体使用string()方法是十分方便和有效率的.但是如果响应体很大(大于1MB)就要避免使用string()方法,因为它会在内存中加载整个文档.这种情况下,更应该把响应体当做流来处理.

private final OkHttpClient client = new OkHttpClient();

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

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

Asyronous Get 异步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 {
        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(response.body().string());
      }
    });
  }

Accessing Headers 获取消息头

典型的HTTP消息头结构就像是Map<String,String>:每个键都对应1个或0个值.但是一些消息头允许有多个值,就像Guava的Multimap.举一个例子,对于一个HTTP响应,支持多个变化的消息头是合法,普遍的.OkHttp的APIs尽力使这些个情况处理起来都很方便.

当写入请求头时,使用header(name,value)来设置唯一的键值对.如果原来存在有值,那么在添加新的值之前,原来的值会被删除.你可以使用addHeader(name,value)来添加一个消息头,这样即使原来存在有,原来的值也不会被删除.

当读取响应头时,使用header(name)会返回最后一组键值对.通常这也是唯一的一组.如果没有值,那么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();

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

Posting a String 发送一个字符串

使用一个HTTP POST来发送一个请求体到服务器.这个例子是发送一个markdown文档到一个网站服务器,然后用HTML展示markdown文档.因为整个请求体都保存在内存里,避免使用这个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();

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

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

Post Streaming 发送数据流

我们可以把请求体当做数据流来发送.生成的请求体的内容就是写出来的内容.这个例子里的数据流直接进入Okio的缓冲区.你的程序可能更想要一个输出流,那么你可以使用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();

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

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

Posting a File 发送一个文件

把文件当做请求体是很简单的一件事.

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

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

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

Posting form parameters 发送表单参数

你可以使用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();

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

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

Posting a mulitipart request 发送多个请求

MultipartBody.Builder可以构造一个和HTML文件上传表单兼容的复杂请求体.多重请求体的每部分都是一个请求体,并且可以单独定义每个请求的消息头.如果可以,这些消息头应该描述这些请求体,比如它们的Content-disposition.如果Content-LengthContent-Type消息头可用,那么他们会被紫铜添加.

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

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

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

Parse a Json Response With Gson 使用Gson来解析JSON格式的响应

Gson是一个很好用的API,它可以用来进行JSON和Java对象的转换.这里我们用它来解码一个来自Github API的的JSON响应.

注意当解码响应体时,ResponseBody.charStream()使用Content-Type响应头来选择使用相应的字符集.如果没有特殊的规定字符集,那它将会使用默认的UTF-8.

private final OkHttpClient client = new OkHttpClient();
  private final Gson gson = new Gson();

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

    Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
    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 响应缓存

为了缓存响应,你需要一个可以读写的高速缓存目录,并且要对缓存的大小做一个限制.高速缓存目录应该是保密的,不被信任的应用不会读取出它的内容.
当多个缓存器同时访问相同的高速缓存目录时会出现错误.大多数应用应该只调用new OkHttpClient()一次,然后在其他地方使用它的相同实例,否则两个缓存实例就会发生冲突.响应缓存就会发生污染,还可能会使你的程序宕掉.

响应缓存使用HTTP消息头来设置所有的配置.你可以添加一个请求头比如Cache-control: max-stale=3600,然后OkHttp就会按照这个配置执行.你的网站服务器会使用它自己的响应消息头来设置了多长时间响应会被缓存一次,就=像这样Cache-Control: max-age=9600.这样缓存头可以强制得到一个缓存的响应,强制一个网络响应,或者强制使用一个条件性的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();

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

    String 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());

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

    String 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.FROCE_CACHE.警告:如果你使用了FORCE_CACHE但是响应需要从网络获取,OkHttp将会返回504 Unsatisfiable Request.

Canceing a Call 取消请求

你可以使用Call.cancel()来立刻停止一个正在进行的请求.如果有一个线程正在写请求或者读取响应,那么将会收到一个IOExeption.当一个请求不在必须时,你这样做可以节省网络资源.比如当你的用户离开应用的时候.同步或者异步的请求都会被取消.

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

    try {
      System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
      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);
    }
  }

Timeout 超时

当请求不可达时,你可以设置超时来结束这个请求.网络断开可能是由客户端的连通出现了问题造成的,也可能能是服务器的问题,或者是介于二者之间的问题.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();

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

Per-call Configuration 每个请求的设置.

所有的HTTP连接设置都存在于OkHttpClient,包括代理设置,超时设置和缓存设置.当你需要改变单个请求的配置时,调用OkHttpClient.newBuilder().这会返回一个和原连接享有同样连接池,调度器和配置的构造器(Builder).在下面这个例子里,我们构造了一个使用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();

    try {
      // Copy to customize OkHttp for this request.
      OkHttpClient copy = client.newBuilder()
          .readTimeout(500, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();
      System.out.println("Response 1 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 1 failed: " + e);
    }

列表项

    try {
      // Copy to customize OkHttp for this request.
      OkHttpClient copy = client.newBuilder()
          .readTimeout(3000, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();
      System.out.println("Response 2 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 2 failed: " + e);
    }
  }

Handling authentrication 处理认证

OkHttp可以自动重试未经身份验证的请求,当响应的状态码是401未经验证时,一个Authenticator(验证人)被要求提供一个凭据.实现应该构造一个包括缺少的凭据的新请求.如果没有凭据可用,OkHttp将会跳过重试直接返回null.

使用Response.challenges()来拿到认证要求的方案和范围.当完成了一个基本的(Basic)认证,使用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 {
            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();

    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.
  }

上面的代码都依赖一个responseCount()方法:

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