量化交易android基础知识

OKHttp使用

2018-09-09  本文已影响105人  Jowney

前言

在Android开发中,发送HTTP请求是很常见的。SDK中自带的HttpURLConnection虽然能基本满足需求,但是在使用上有诸多不便,为此,square公司实现了一个HTTP客户端的类库——Okhttp 。

Okhttp特点

Okhttp是一个支持HTTP 和 HTTP/2 的客户端,可以在Android和Java应用程序中使用,其具有以下特点:

当前Okhttp最新的版本是3.x,支持Android 2.3+。要想在Java应用程序中使用Okhttp,JRE的最低版本要求是1.7。

Okhttp API: http://square.github.io/okhttp/3.x/okhttp/

本文中的代码来自于Okhttp的官方Wiki中的示例代码。


下载Okhttp

在Gradle中进行如下配置以便在Android Studio中使用:

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

核心类

我们在使用Okhttp进行开发的时候,主要牵扯到以下几个核心类:OkHttpClient、Request、Call 和 Response。


同步GET

以下示例演示了如何同步发送GET请求,输出响应头以及将响应体转换为字符串。

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

下面对以上代码进行简单说明:

1.png

为了解决同时获取多个name相同的响应头的值,Headers中提供了一个public List<String> values(String name)方法,该方法会返回一个List<String>对象,所以此处通过Headers对象的values(‘Set-Cookie’)可以获取全部的Cookie信息,如果调用Headers对象的get(‘Set-Cookie’)方法,那么只会获取最后一条Cookie信息。


异步GET

以下示例演示了如何异步发送GET网络请求,代码如下所示:

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

下面对以上代码进行一下说明:


请求头和响应头

典型的HTTP请求头、响应头都是类似于Map<String, String>,每个name对应一个value值。不过像我们之前提到的,也会存在多个name重复的情况,比如相应结果中就有可能存在多个Set-Cookie响应头,同样的,也可能同时存在多个名称一样的请求头。响应头的读取我们在上文已经说过了,在此不再赘述。一般情况下,我们只需要调用header(name, value)方法就可以设置请求头的name和value,调用该方法会确保整个请求头中不会存在多个名称一样的name。如果想添加多个name相同的请求头,应该调用addHeader(name, value)方法,这样可以添加重复name的请求头,其value可以不同,例如如下所示:

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

上面的代码通过addHeader方法添加了两个Accept请求头,且二者的值不同,这样服务器收到客户端发来的请求后,就知道客户端既支持application/json类型的数据,也支持application/vnd.github.v3+json类型的数据。


用POST发送String

可以使用POST方法发送请求体。下面的示例演示了如何将markdown文本作为请求体发送到服务器,服务器会将其转换成html文档,并发送给客户端。

  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发送Stream流

下面的示例演示了如何使用POST发送Stream流。

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

下面对以上代码进行说明:


用POST发送File

下面的代码演示了如何用POST发送文件。

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

我们之前提到,RequestBody.create()静态方法可以接收File参数,将File转换成请求体,将其传递给post()方法。


用POST发送Form表单中的键值对

如果想用POST发送键值对字符串,可以使用在post()方法中传入FormBody对象,FormBody继承自RequestBody,类似于Web前端中的Form表单。可以通过FormBody.Builder构建FormBody

示例代码如下所示:

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

需要注意的是,在发送数据之前,Android会对非ASCII码字符调用encodeURIComponent方法进行编码,例如”Jurassic Park”会编码成”Jurassic%20Park”,其中的空格符被编码成%20了,服务器端会其自动解码。


用POST发送multipart数据

我们可以通过Web前端的Form表单上传一个或多个文件,Okhttp也提供了对应的功能,如果我们想同时发送多个Form表单形式的文件,就可以使用在post()方法中传入MultipartBody对象。MultipartBody继承自RequestBody,也表示请求体。只不过MultipartBody的内部是由多个part组成的,每个part就单独包含了一个RequestBody请求体,所以可以把MultipartBody看成是一个RequestBody的数组,而且可以分别给每个RequestBody单独设置请求头。

示例代码如下所示:

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

下面对以上代码进行说明:


用Gson处理JSON响应

Gson是Google开源的一个用于进行JSON处理的Java库,通过Gson可以很方面地在JSON和Java对象之间进行转换。我们可以将Okhttp和Gson一起使用,用Gson解析返回的JSON结果。

下面的示例代码演示了如何使用Gson解析GitHub API的返回结果。

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

下面对以上代码进行说明:


缓存响应结果

如果想缓存响应结果,我们就需要为Okhttp配置缓存目录,Okhttp可以写入和读取该缓存目录,并且我们需要限制该缓存目录的大小。Okhttp的缓存目录应该是私有的,不能被其他应用访问。

Okhttp中,多个缓存实例同时访问同一个缓存目录会出错,大部分的应用只应该调用一次new OkHttpClient(),然后为其配置缓存目录,然后在App的各个地方都使用这一个OkHttpClient实例对象,否则两个缓存实例会互相影响,导致App崩溃。

缓存示例代码如下所示:

  private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    String okhttpCachePath = getCacheDir().getPath() + File.separator + "okhttp";
    File okhttpCache = new File(okhttpCachePath);
    if(!okhttpCache.exists()){
        okhttpCache.mkdirs();
    }

    Cache cache = new Cache(okhttpCache, 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));
  }

下面对以上代码进行说明:


取消请求

当请求不再需要的时候,我们应该中止请求,比如退出当前的Activity了,那么在Activity中发出的请求应该被中止。可以通过调用Call的cancel方法立即中止请求,如果线程正在写入Request或读取Response,那么会抛出IOException异常。同步请求和异步请求都可以被取消。

示例代码如下所示:

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

上述请求,服务器端会有两秒的延时,在客户端发出请求1秒之后,请求还未完成,这时候通过cancel方法中止了Call,请求中断,并触发IOException异常。


设置超时

一次HTTP请求实际上可以分为三步:

  1. 客户端与服务器建立连接
  2. 客户端发送请求数据到服务器,即数据上传
  3. 服务器将响应数据发送给客户端,即数据下载

由于网络、服务器等各种原因,这三步中的每一步都有可能耗费很长时间,导致整个HTTP请求花费很长时间或不能完成。

为此,可以通过OkHttpClient.BuilderconnectTimeout()方法设置客户端和服务器建立连接的超时时间,通过writeTimeout()方法设置客户端上传数据到服务器的超时时间,通过readTimeout()方法设置客户端从服务器下载响应数据的超时时间。

在2.5.0版本之前,Okhttp默认不设置任何的超时时间,从2.5.0版本开始,Okhttp将连接超时、写入超时(上传数据)、读取超时(下载数据)的超时时间都默认设置为10秒。如果HTTP请求需要更长时间,那么需要我们手动设置超时时间。

示例代码如下所示:

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

如果HTTP请求的某一部分超时了,那么就会触发异常。


处理身份验证

有些网络请求是需要用户名密码登录的,如果没提供登录需要的信息,那么会得到401 Not Authorized未授权的错误,这时候Okhttp会自动查找是否配置了Authenticator,如果配置过Authenticator,会用Authenticator中包含的登录相关的信息构建一个新的Request,尝试再次发送HTTP请求。

示例代码如下所示:

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

上面对以上代码进行说明:


ResponseBody

通过Response的body()方法可以得到响应体ResponseBody,响应体必须最终要被关闭,否则会导致资源泄露、App运行变慢甚至崩溃。

ResponseBody和Response都实现了CloseableAutoCloseable接口,它们都有close()方法,Response的close()方法内部直接调用了ResponseBody的close()方法,无论是同步调用execute()还是异步回调onResponse(),最终都需要关闭响应体,可以通过如下方法关闭响应体:

对于同步调用,确保响应体被关闭的最简单的方式是使用try代码块,如下所示:

 Call call = client.newCall(request);
 try (Response response = call.execute()) {
   ... // Use the response.
 }

Response response = call.execute()放入到try()的括号之中,由于Response 实现了CloseableAutoCloseable接口,这样对于编译器来说,会隐式地插入finally代码块,编译器会在该隐式的finally代码块中执行Response的close()方法。

也可以在异步回调方法onResponse()中,执行类似的try代码块,try()代码块括号中的ResponseBody也实现了CloseableAutoCloseable接口,这样编译器也会在隐式的finally代码块中自动关闭响应体,代码如下所示:

   Call call = client.newCall(request);
   call.enqueue(new Callback() {
     public void onResponse(Call call, Response response) throws IOException {
       try (ResponseBody responseBody = response.body()) {
         ... // Use the response.
       }
     }

     public void onFailure(Call call, IOException e) {
       ... // Handle the failure.
     }
   });

响应体中的数据有可能很大,应该只读取一次响应体的数据。调用ResponseBody的bytes()string()方法会将整个响应体数据写入到内存中,可以通过source()byteStream()charStream()进行流式处理。

上一篇 下一篇

猜你喜欢

热点阅读