OkHttp入门级-Kotlin代码版本

概念概要

Http

HTTP是现代应用网络的方式。这是我们交换数据和媒体的方式。高效地执行HTTP可以使您的工作负载更快,并节省带宽。
http是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII码形式给出;而消息内容则具有一个类似MIME的格式。

Https

HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 [1] 。HTTPS 在HTTP 的基础下加入SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。 HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP 之间)。

OkHttp

OkHttp是一个默认高效的HTTP客户端:,一个处理网络请求的开源项目,是安卓端最火热的轻量级框架

调用Calls

概念

HTTP客户机的工作是接受请求并生成响应。

Requests请求

每个HTTP请求包含一个URL、一个方法(如GET或POST)和一列报头。请求还可以包含一个主体:特定内容类型的数据流。

Responses响应

响应使用代码(比如200表示成功,404表示未找到)、头文件和它自己的可选主体来响应请求。

调用

同步:线程阻塞,直到响应可读。
异步:在任何线程上对请求排队,当响应可读时在另一个线程上被回调。

连接Connections

概念概要

OkHttp使用三种类型来达到它到您的web服务器连接:URL、Address地址和Routes路由。

  • URL:url(比如https://github.com/square/okhttp)是HTTP和Internet的基础。除了作为一种通用的、分散的web命名方案之外,它们还指定了如何访问web资源。
  • 地址Address:地址指定一个webserver(如github.com)和连接到该服务器所需的所有静态配置:端口号、HTTPS设置和首选网络协议(如HTTP/2或SPDY)。
  • 路由Routes:路由提供实际连接到web服务器所需的动态信息。这是要尝试的特定IP地址(由DNS查询发现)、要使用的确切代理服务器(如果使用了ProxySelector)以及要协商的TLS版本(用于HTTPS连接)。

方法Recipes

下面是一些常用的方法和代码示例

Synchronous Get同步Get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  private val client = OkHttpClient()

  fun run() {
    val request = Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build()

    client.newCall(request).execute().use { response ->
      if (!response.isSuccessful) throw IOException("Unexpected code $response")

      for ((name, value) in response.headers) {
        println("$name: $value")
      }

      println(response.body!!.string())
    }
  }

Asynchronous Get异步Get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  private val client = OkHttpClient()

  fun run() {
    val request = Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build()

    client.newCall(request).enqueue(object : Callback {
      override fun onFailure(call: Call, e: IOException) {
        e.printStackTrace()
      }

      override fun onResponse(call: Call, response: Response) {
        response.use {
          if (!response.isSuccessful) throw IOException("Unexpected code $response")

          for ((name, value) in response.headers) {
            println("$name: $value")
          }

          println(response.body!!.string())
        }
      }
    })
  }

以上是发送一个get请求的步骤,首先构造一个Request对象,参数必须有url,可以通过Request.Builder设置更多的参数比如:header、method
然后通过request的对象去构造得到一个Call对象,类似于将请求封装成了任务
call.enqueue是异步调用,直接execute是同步调用。将call加入调度队列,然后等待任务执行完成,我们在Callback中即可得到结果。
Above:正常情况下写完OkHttp代码较长,因此多封装为Http工具类达到简单调用使用的特点

TIPS:onResponse回调的参数是response,一般情况下,比如我们希望获得返回的字符串,可以通过response.body().string()获取;如果希望获得返回的二进制字节数组,则调用response.body().bytes();如果你想拿到返回的inputStream,则调用response.body().byteStream()

Accessing Headers访问Header

通常,HTTP头信息的工作方式类似于Map:每个字段有一个值或没有一个值。但是有些头文件允许多个值,比如Guava的Multimap。例如,HTTP响应提供多个变体报头是合法且常见的。OkHttp的api试图让这两种情况都适合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  private val client = OkHttpClient()

  fun run() {
    val request = 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()

    client.newCall(request).execute().use { response ->
      if (!response.isSuccessful) throw IOException("Unexpected code $response")

      println("Server: ${response.header("Server")}")
      println("Date: ${response.header("Date")}")
      println("Vary: ${response.headers("Vary")}")
    }
  }

Posting a String发布一个字符串

使用HTTP POST将请求体发送到服务。此示例将markdown文档发布到将markdown呈现为HTML的web服务。由于整个请求体同时位于内存中,因此避免使用此API发布大型(大于1 MiB)文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  private val client = OkHttpClient()

  fun run() {
    val postBody = """
        |Releases
        |--------
        |
        | * _1.0_ May 6, 2013
        | * _1.1_ June 15, 2013
        | * _1.2_ August 11, 2013
        |""".trimMargin()

    val request = Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(postBody.toRequestBody(MEDIA_TYPE_MARKDOWN))
        .build()

    client.newCall(request).execute().use { response ->
      if (!response.isSuccessful) throw IOException("Unexpected code $response")

      println(response.body!!.string())
    }
  }

  companion object {
    val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType()
  }

Post Streaming发布流媒体

这里,我们将请求体作为流发布。这个请求体的内容是在编写时生成的。这个例子直接流到Okio缓冲池。您的程序可能更喜欢OutputStream,您可以从BufferedSink.outputStream()获得它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  private val client = OkHttpClient()

  fun run() {
    val requestBody = object : RequestBody() {
      override fun contentType() = MEDIA_TYPE_MARKDOWN

      override fun writeTo(sink: BufferedSink) {
        sink.writeUtf8("Numbers
")
        sink.writeUtf8("-------
")
        for (i in 2..997) {
          sink.writeUtf8(String.format(" * $i = ${factor(i)}
"))
        }
      }

      private fun factor(n: Int): String {
        for (i in 2 until n) {
          val x = n / i
          if (x * i == n) return "${factor(x)} × $i"
        }
        return n.toString()
      }
    }

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

    client.newCall(request).execute().use { response ->
      if (!response.isSuccessful) throw IOException("Unexpected code $response")

      println(response.body!!.string())
    }
  }

  companion object {
    val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType()
  }

Posting a File上传一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  private val client = OkHttpClient()

  fun run() {
    val file = File("README.md")

    val request = Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(file.asRequestBody(MEDIA_TYPE_MARKDOWN))
        .build()

    client.newCall(request).execute().use { response ->
      if (!response.isSuccessful) throw IOException("Unexpected code $response")

      println(response.body!!.string())
    }
  }

  companion object {
    val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType()
  }

Posting form parameters发布形式参数

使用FormBody。生成器来构建一个类似于HTML
标记的请求体。名称和值将使用html兼容的表单URL编码进行编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  private val client = OkHttpClient()

  fun run() {
    val formBody = FormBody.Builder()
        .add("search", "Jurassic Park")
        .build()
    val request = Request.Builder()
        .url("https://en.wikipedia.org/w/index.php")
        .post(formBody)
        .build()

    client.newCall(request).execute().use { response ->
      if (!response.isSuccessful) throw IOException("Unexpected code $response")

      println(response.body!!.string())
    }
  }

Posting a multipart request发布一个多部分请求

MultipartBody。Builder可以构建与HTML文件上传表单兼容的复杂请求体。多部分请求体的每个部分本身就是一个请求体,可以定义自己的头。如果存在,这些头应该描述部分主体,比如它的内容配置。如果内容长度和内容类型头是可用的,则自动添加它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  private val client = OkHttpClient()

  fun run() {
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
    val requestBody = MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("title", "Square Logo")
        .addFormDataPart("image", "logo-square.png",
            File("docs/images/logo-square.png").asRequestBody(MEDIA_TYPE_PNG))
        .build()

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

    client.newCall(request).execute().use { response ->
      if (!response.isSuccessful) throw IOException("Unexpected code $response")

      println(response.body!!.string())
    }
  }

  companion object {
    /**
     * 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 val IMGUR_CLIENT_ID = "9199fdef135c122"
    private val MEDIA_TYPE_PNG = "image/png".toMediaType()
  }

Parse a JSON Response With Moshi使用Moshi解析JSON响应

Moshi是一个方便的API,用于在JSON和Java对象之间进行转换。这里我们使用它来解码来自GitHub API的JSON响应。
请注意,ResponseBody.charStream()使用内容类型的响应头来选择在解码响应体时使用哪个charset。如果没有指定字符集,则默认为UTF-8。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  private val client = OkHttpClient()
  private val moshi = Moshi.Builder().build()
  private val gistJsonAdapter = moshi.adapter(Gist::class.java)

  fun run() {
    val request = Request.Builder()
        .url("https://api.github.com/gists/c2a7c39532239ff261be")
        .build()
    client.newCall(request).execute().use { response ->
      if (!response.isSuccessful) throw IOException("Unexpected code $response")

      val gist = gistJsonAdapter.fromJson(response.body!!.source())

      for ((key, value) in gist!!.files!!) {
        println(key)
        println(value.content)
      }
    }
  }

  @JsonClass(generateAdapter = true)
  data class Gist(var files: Map<String, GistFile>?)

  @JsonClass(generateAdapter = true)
  data class GistFile(var content: String?)

额外介绍一种使用Gson来解析JSON响应
Gson是一个在JSON和Java对象之间转换非常方便的api。这里我们用Gson来解析Github API的JSON响应。
注意:ResponseBody.charStream()使用响应头Content-Type指定的字符集来解析响应体。默认是UTF-8。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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响应缓存

要缓存响应,您需要一个可以读写的缓存目录,以及对缓存大小的限制。缓存目录应该是私有的,不受信任的应用程序不应该能够读取它的内容!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  private val client: OkHttpClient = OkHttpClient.Builder()
      .cache(Cache(
          directory = cacheDirectory,
          maxSize = 10L * 1024L * 1024L // 1 MiB
      ))
      .build()

  fun run() {
    val request = Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build()

    val response1Body = client.newCall(request).execute().use {
      if (!it.isSuccessful) throw IOException("Unexpected code $it")

      println("Response 1 response:          $it")
      println("Response 1 cache response:    ${it.cacheResponse}")
      println("Response 1 network response:  ${it.networkResponse}")
      return@use it.body!!.string()
    }

    val response2Body = client.newCall(request).execute().use {
      if (!it.isSuccessful) throw IOException("Unexpected code $it")

      println("Response 2 response:          $it")
      println("Response 2 cache response:    ${it.cacheResponse}")
      println("Response 2 network response:  ${it.networkResponse}")
      return@use it.body!!.string()
    }

    println("Response 2 equals Response 1? " + (response1Body == response2Body))
  }

Canceling a Call取消一个Call

使用Call.cancel()可以立即停止掉一个正在执行的call。如果一个线程正在写请求或者读响应,将会引发IOException。当call没有必要的时候,使用这个api可以节约网络资源。例如当用户离开一个应用时。不管同步还是异步的call都可以取消。
你可以通过tags来同时取消多个请求。当你构建一请求时,使用RequestBuilder.tag(tag)来分配一个标签。之后你就可以用OkHttpClient.cancel(tag)来取消所有带有这个tag的call。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  private val executor = Executors.newScheduledThreadPool(1)
  private val client = OkHttpClient()

  fun run() {
    val request = Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build()

    val startNanos = System.nanoTime()
    val call = client.newCall(request)

    // Schedule a job to cancel the call in 1 second.
    executor.schedule({
      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 {
      call.execute().use { response ->
        System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
            (System.nanoTime() - startNanos) / 1e9f, response)
      }
    } catch (e: IOException) {
      System.out.printf("%.2f Call failed as expected: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, e)
    }
  }

Timeouts超时

没有响应时使用超时结束call。没有响应的原因可能是客户点链接问题、服务器可用性问题或者这之间的其他东西。OkHttp支持连接,读取和写入超时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  private val client: OkHttpClient = OkHttpClient.Builder()
      .connectTimeout(5, TimeUnit.SECONDS)
      .writeTimeout(5, TimeUnit.SECONDS)
      .readTimeout(5, TimeUnit.SECONDS)
      .callTimeout(10, TimeUnit.SECONDS)
      .build()

  fun run() {
    val request = Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build()

    client.newCall(request).execute().use { response ->
      println("Response completed: $response")
    }
  }

Interceptors拦截器

概念

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();

    long t1 = System.nanoTime();
    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;
  }
}

拦截器作用图

Application Interceptors应用拦截器

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

1
2
3
4
5
6
7
8
9
10
11
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();

Network Interceptors网络拦截器

注册一个网络拦截器非常类似。调用addNetworkInterceptor()而不是addInterceptor()

1
2
3
4
5
6
7
8
9
10
11
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();