程序员人生 网站导航

OkHttp 官方中文文档

栏目:综合技术时间:2016-07-13 10:18:30

OkHttp官方中文文档

本文结构

  • Calls
  • Connections
  • Recipes
  • Interceptors
  • HTTPS
    本文翻译来自 官方OkHttp Wiki

  • OkHttp官方中文文档
    • 1Calls
      • 1 要求
      • 2 响应
      • 3重写要求
      • 4重写响应
      • 5后续要求
      • 6要求重试
      • 7 呼唤
      • 8调度
    • 2Connections
      • 1URLs
          • URLs摘要
      • 2 Addresses
      • 3 Routes
      • 4Connections
    • 3Recipes
      • 1同步获得
      • 2异步获得
      • 3访问头
      • 4Posting a String
      • 5 Post Streaming
      • 6 Posting a File
      • 7 发布表单参数
      • 8 发布multipart要求
      • 9 通过GSON解析响应的JSON
      • 10 响应缓存
      • 11 取消Call
      • 12 超时
      • 13 每一个呼唤配置
      • 14 认证处理
    • 4拦截器
      • 1 利用拦截器
      • 2 网络拦截器
      • 3 利用程序和网络拦截之间进行选择
        • 利用拦截器
        • 网络拦截器
      • 4重写要求
      • 5 重写响应
      • 6 可用性
    • 5 HTTPS
      • 1证书钉扎
      • 2定制信任证书

1、Calls

HTTP客户真个工作是接受你的request,并产生它的response。这个在理论上是简单的,但在实践中确是很辣手。

1.1 要求

每个HTTP要求中都包括1个URL,1个方法(如GETPOST),和1个要求头列表(headers)。要求还可以含有1个要求体(body):1个特定内容类型的数据流。

1.2 响应

每个HTTP响应中都包括1个状态码(如200代表成功,404代表未找​​到),1个响应头列表(headers)和1个可选的响应体(body)。

1.3重写要求

当你的OkHttp发送1个HTTP要求,你在描写1个高层次的要求:“给我获得这个网址中的这些要求头。”对正确性和效力,OkHttp发送前会重写你的要求。

OkHttp可以从原来的要求中添加要求头(headers),包括Content-Length, Transfer-Encoding, User-Agent, Host, Connection, 和 Content-Type。除非要求头已存在紧缩响应,否则它还将添加1个Accept-Encoding要求头。如果你有cookies,OkHttp还将添加1个Cookie要求头。

1些要求会有1个缓存的响应。当这个缓存的响应不是最新的,OkHttp会发送1个有条件的GET来下载更新的响应,如果它比缓存还新。它将会添加需要的要求头,如IF-Modified-SinceIf-None-Match

1.4重写响应

如果使用的是透明紧缩,OkHttp会丢失相应的响应头Content-EncodingContent-Length,这是由于它们不能用于解压响应体(body)。

如果1个条件GET是成功的,在唆使的规范下响应来自于网络和缓存的合并。

1.5后续要求

当你的要求的URL已移动,Web服务器将返回1个响应码像302,以表明本文档的新的URL。OkHttp将依照重定向检索的终究响应。

如果响应问题是1个的授权盘问,OkHttp将会要求身份验证(如果有1个已配置好),以满足盘问。如果身份验证提供凭据,要求将会带着凭证进行重试。

1.6要求重试

有时连接失败:要末是连接池已过时和断开,或是Web服务器本身没法达成。如果有1个是可用的,OkHttp将会使用不同的路由进行要求重试。

1.7 呼唤

随侧重写,重定向,后续和重试,你简单的要求可能会产生很多要求和响应。OkHttp使用呼唤(Call)并通过许多必要的中间要求和响应来满足你要求的任务模型。通常情况,这是否是很多!如果您的网址被重定向,或如果您故障转移到另外一个IP地址,但它会欣慰的知道你的代码会继续工作。

通过以下两种方式进行呼唤:
- 同步:直到响应,你的线程块是可读的。
- 异步:你在任何线程进行排队要求,并且当响应是可读的时候,你会在另外一个线程得到回调。

呼唤(Calls)可以在任何线程中取消。如果它还没有完成,它将作为失败的呼唤(Calls)!当呼唤(Call)被取消的时候,如果代码试图进行写要求体(request body)或读取响应体(response body)会遭受IOException异常。

1.8调度

对同步调用,你带上你自己的线程,并负责管理并发要求。并发连接过量浪费资源; 过少的危害等待时间。

对异步调用,调度实现了最大同时要求策略。您可以设置每一个Web服务器最大值(默许值为5),和整体(默许为64)。

2、Connections

虽然只提供了URL,但是OkHttp计划使用3种类型连接你的web服务器:URL, Address, 和 Route。

2.1URLs

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

URLs摘要:
  • 它们指定该呼唤(Call)可以被明文(HTTP)或加密的(HTTPS),但不指定用哪一个加密算法。他们也不指定如何验证对方的证书(HostnameVerifier)或证书可以信任(SSLSocketFactory)。
  • 他们不指定是不是应使用特定的代理服务器或如何与该代理服务器进行身份验证。

他们还具体:每一个URL辨认特定的路径(如 /square/okhttp)和查询(如 ?q=sharks&lang=en)。每一个Web服务器主机的网址。

2.2 Addresses

Addresses指定网络服务器(如github.com)和所有的静态必要的配置,和连接到该服务器:端口号,HTTPS设置和首选的网络协议(如HTTP / 2SPDY)。

同享相同地址的URL也能够同享相同的基础TCP套接字连接。同享1个连接有实实在在的性能优点:更低的延迟,更高的吞吐量(由于TCP慢启动)和保养电池。OkHttp使用的ConnectionPool自动重用HTTP / 1.x的连接和多样的HTTP/ 2和SPDY连接。

在OkHttp地址的某些字段来自URL(scheme, hostname, port),其余来自OkHttpClient。

2.3 Routes

Routes提供连接到1个网络服务器所必须的动态信息。就是尝试特定的IP地址(如由DNS查询发现),使用确切的代理服务器(如果1个特定的IP地址的ProxySelector在使用中)和协商的TLS版本(HTTPS连接)。

可能有单个地址对应多个路由。例如,在多个数据中心托管的Web服务器,它可能会在其DNS响应产生多个IP地址。

2.4Connections

当你使用OkHttp进行1个URL要求,下面是它的操作流程:

  1. 它使用URL和配置OkHttpClient创建1个address。此地址指定我们将如何连接到网络服务器
  2. 它通过地址从连接池中取回1个连接。
  3. 如果它没有在池中找到连接,它会选择route尝试。这通常意味着使1个DNS要求, 以获得服务器的IP地址。如果需要,它会选择1个的TLS版本和代理服务器
  4. 如果它是1个新的route,它连接通过建立不管是直接的socket连接,socket连接使用TLS安全通道(用于HTTPS通过1个HTTP代理),或直接TLS连接。它的TLS握手是必要的。
  5. 它发送HTTP要求并读取响应。
    如果有连接出现问题,OkHttp将选择另外一条route,然后再试1次。当1个服务器的地址的1个子集是不可达时,这使得OkHttp能够恢复。当连接池是过时或试图TLS版本不受支持时,这类方式是很有用的。

1旦响应已被接收到,该连接将被返回到池中,以便它可以在将来的要求中被重用。连接在池中闲置1段时间后,它会被赶出。

3、Recipes

我们已写了1些方法,演示了如何解决OkHttp常见问题。通过浏览他们了解1切是如何正常工作的。可以自由剪切和粘贴这些例子。

3.1同步获得

下载文件,打印其头部,并以字符串情势打印其响应体。

string() 方法在响应体中是方便快捷的小型文件。但是,如果响应体是大的(大于1 MIB以上),它会在全部文件加载到内存中,所以应当避免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()); }

3.2异步获得

下载1个工作线程的文件,当响应是可读的时候,获得回调(Callback)。当响应头已准备好后,将产生回调(Callback)。读取响应体可能1直阻塞。目前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()); } }); }

3.3访问头

典型的HTTP头工作就像1个Map<String, String> :每一个字段都有1个值或无值。但是,1些头部(headers)允许多个值,比如Guava的Multimap。例如,它共同为1个HTTP响应提供多个Vary头。OkHttp的API,试图使这两种情况下都能舒适使用。

当写要求头,用header(name, value)来为唯1出现的name设置value。如果存在现有值,在添加新的value之前,他们将被移除。使用addHeader(name, value)来添加头部不需要移除当前存在的headers

当读取响应头,用header(name)返回最后设置name的value。如果没有valueheader(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")); }

3.4Posting a String

使用HTTP POST的要求体发送到服务。下面例子post了1个markdown文档到1个的Web服务(将markdown作为HTML)。由于全部要求体是同时在内存中,应避免使用此API发送较大(大于1 MIB)的文件。

public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf⑻"); 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()); }

3.5 Post Streaming

在这里,我们POST要求体作为stream。正在生成要求体的内容写入到stream中。下面例子streams直接进入 Okio缓冲水槽。你的程序可能更喜欢使用OutputStream,你可以通过BufferedSink.outputStream()取得 OutputStream。

public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf⑻"); 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()); }

3.6 Posting a File

它是很容易的将文件作为要求体。

public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf⑻"); 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()); }

3.7 发布表单参数

使用FormBody.Builder建立1个要求体,它就像1个HTML 的标记。Namesvalues将使用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()); }

3.8 发布multipart要求

MultipartBody.Builder可以构建与HTML文件上传表单兼容的复杂的要求主体。multipart要求体的每部份本身就是要求体,并且可以定义自己的头部。如果存在,这些头应当描写的部份要求体,如它的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()); }

3.9 通过GSON解析响应的JSON

GSON是1个JSON和Java对象之间的便利转换的API。这里,我们用它来解码从GitHub的API 响应的JSON。

需要注意的是ResponseBody.charStream()使用的Content-Type响应头进行解码时,所使用的字符集,如果没有指定字符集,它默许为UTF⑻ 。

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

3.10 响应缓存

要缓存响应,你需要1个缓存目录来进行读取和写入,和1个缓存的大小限制。缓存目录应当是私有的,不信任的利用程序不应当能够浏览其内容!

多个缓存同时访问相同的缓存目录,这是毛病的。大多数利用程序应当调用1次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.FORCE_CACHE。正告:如果您使用FORCE_CACHE和响应要求网络,OkHttp将会返回1个504不可满足要求的响应。

3.11 取消Call

使用Call.cancel()立即停止正在进行的Call。如果1个线程目前正在写要求或读响应,它还将收到1个IOException异常。当1个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); 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); } }

3.12 超时

当其查询没法访问时,使用超时失败的调用。网络划分可以是由于客户端连接问题,服务器可用性的问题,或之间的任何东西。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); }

3.13 每一个呼唤配置

所有的HTTP客户端配置都在OkHttpClient中包括代理设置,超时和缓存。当你需要改变单1Call的配置时,调用OkHttpClient.newBuilder() 。这将返回同享相同的连接池,调度和配置与原来的客户真个建造器。在下面的例子中,我们做了500毫秒超时,另外1个3000毫秒超时要求。

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

3.14 认证处理

OkHttp可以自动重试未经授权的要求。当响应是401 Not Authorized,1个Authenticator被要求提供凭据。实现应当建立1个包括缺少凭据的新要求。如果没有凭证可用,则返回null跳太重试。

使用Response.challenges()取得任何认证挑战方案和领域。当完成1个基本的挑战,用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; //如果我们已使用这些凭据失败,不重试 }

您也能够跳太重试,当你1个利用尝试的次数超过了限制的次数:

if (responseCount(response) >= 3) { return null; //如果我们已失败了3次,放弃。 . }

这上面的代码依赖于这个responseCount()方法:

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

4、拦截器

拦截器是1个强大的机制,它可以监控,重写和重试Calls。下面是记录传出要求和响应传入1个简单的拦截器。

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

1个呼唤chain.proceed(request)是每一个拦截器的实现的重要组成部份。这个看起来简单的方法是,所有的HTTP工作情况,产生满足要求的响应。

拦截器可以链接。假定你有1个既紧缩拦截器和拦截器校验:你需要肯定数据是不是被紧缩,然后履行校验,或是先校验然后再紧缩。OkHttp使用列表来跟踪拦截器,为了拦截器被调用。
这里写图片描述

4.1 利用拦截器

拦截器被注册为任1利用程序或网络拦截器。我们将使用LoggingInterceptor上面定义以示区分。

注册1个利用程序拦截器通过在OkHttpClient.Builder上调用addInterceptor()

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遵守这类自动重定向。我们的利用程序拦截器被调用1次,并且从返回的响应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。

4.2 网络拦截器

注册网络拦截器相当类似。调用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();

当我们运行这段代码,拦截器运行两次。1个是初始要求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头部通知支持紧缩响应。网络拦截器的链具有非空的连接,它可用于询问IP地址和用于连接到网络服务器的TLS配置。

4.3 利用程序和网络拦截之间进行选择

每一个拦截器链(interceptor chain)具有相对优势。

利用拦截器

  • 没必要担心像重定向和重试的中间响应。
  • 总是被调用1次,即便HTTP响应来自缓存服务。
  • 视察利用程序的原意。不关心OkHttp注入的头文件,如 If-None-Match
  • 允许短路和不调用Chain.proceed()
  • 允许重试,并屡次调用Chain.proceed() 。

网络拦截器

  • 能够操作像重定向和重试的中间响应。
  • 在短路网络不调用的缓存的响应。
  • 视察会在网络上传输的数据。
  • 访问Connection承载要求。

4.4重写要求

拦截器可以添加,删除或替换要求头。他们还可以转换要求体。例如,如果你连接到已知支持它的网络服务器,你可使用利用程序拦截器添加要求体的紧缩。

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

4.5 重写响应

相对应的,拦截器也能够重写响应头和转换响应体。这通常不是重写要求头,由于它可能违背了Web服务器的期望致使更危险!

如果你在1个辣手的情况下,并做好应对的后果,重写响应头是解决问题的有效方式。例如,您可以修复服务器的配置毛病的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(); } };

通常,此方法效果最好的时候,它补充了在Web服务器上相应的修复!

4.6 可用性

OkHttp的拦截器需要OkHttp 2.2或更高。不幸的是,拦截器不能与OkUrlFactory工作,或说建立在其上的库,包括 Retrofit ≤1.8和 Picasso≤2.4。

5、 HTTPS

OkHttp试图平衡两个相互竞争的耽忧:

  • 连接到尽量多的主机越好。这包括运行最新版本的先进主机boringssl和运行旧版的日期主机OpenSSL
  • 安全的连接。这包括远程Web服务器证书的验证和强密码交换的数据隐私。

当触及到HTTPS服务器的连接,OkHttp需要知道提供哪些TLS版本和密码套件。如果客户端想要最大限度地连接包括过时的TLS版本和弱由设计的密码套件。客户端想要最大限度地提高安全性,应当被要求使用最新版本的TLS和实力最强的加密套件。

具体的安全与连接的决定是由实行ConnectionSpec接口。OkHttp包括3个内置的连接规格:

  • MODERN_TLS是连接到现代的HTTPS服务器安全的配置。
  • COMPATIBLE_TLS是连接到1个安全,但不是现代的-HTTPS服务器的安全配置。
  • CLEARTEXT是用于不安全配置的http://网址。
    默许情况下,OkHttp将尝试MODERN_TLS连接,如果现代配置失败的话将退回到COMPATIBLE_TLS连接。

在每个规范的TLS版本和密码套件可随每一个发行版而更改。例如,在OkHttp 2.2,我们降落支持响应POODLE攻击的SSL 3.0。而在OkHttp 2.3我们降落的支持RC4。与桌面Web阅读器,保持最新的OkHttp是保持安全的最好办法。

你可以用1组自定义TLS版本和密码套件建立自己的连接规格。例如,这类配置限制为3个备受推重的密码套件。它的缺点是,它需要的Andr​​oid 5.0+和1个类似的电流网络服务器

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

5.1证书钉扎

默许情况下,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)); } }

5.2定制信任证书

完全的代码示例显示了如何用自己的1套替换主机平台的证书颁发机构。如上所述,在没有你的服务器的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. }
------分隔线----------------------------
------分隔线----------------------------

最新技术推荐