您的位置:首页 > 理论基础 > 计算机网络

我的 Android 学习笔记-okhttp 的使用(译)

2017-02-23 10:58 393 查看
okhttp 已经是非常流行的网络请求库了。网上介绍的文章非常之多,但感觉都不是特别系统。遂想到官方应该有介绍的文档,仔细寻找一番,果然。但可惜是英文的,于是就大致翻译了一下,权当做笔记了。

1.Calls

HTTP 客户端的任务是接受你的请求病产生相应。这在理论上很简单,但在实践中却很棘手。

a.Requests

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

b.Respones

响应体用一个响应码(例如成功的话就是
200
或没有找到内容的话
404
),响应头列表和可选的响应体来响应请求。(译者注:这段话直译的话感觉有点说不清楚。简单来说就是,响应请求的
respone
包括一个
responeCode
、响应头
responeHaders
和响应体
responeBody


c.Rewriting Requests

为了正确性和效率
OkHttp
在发送前重写
Request


OkHttp
可以添加原始请求中缺少的请求头,包括
Content-Length
Transfer-Encoding
User-Agent
Host
Connection
Content-Type
。如果
Accept-Encoding
不存在,那么
OkHttp
将自动添加一个。如果你使用了
Cookie
OkHttp
将添加一个
Cookie
请求头。

某些请求有缓存响应。当这个请求的缓存响应过期时,
OkHttp
会更新这个响应。这需要添加如
If-Modified-Since
If-None-Match
的请求头。

d.Rewriting Respones

如果使用了压缩,·
OkHttp
将会删除相应的响应头
Content-Encoding
Content-Length
,因为它们不适用于压缩过的响应体。

如果
GET
成功,则来自网络和缓存的响应按照规则合并。

e.Follow-up Requests

如果你请求的网址发生了变动,网络服务器将返回
302
响应码,以指示文档的新地址。
OkHttp
将按照重定向请求最终的响应。

如果响应发出授权质询,
OkHttp
将要求身份验证器(如果配置)来满足质询。如果认证者提供凭证,则使用包含该凭证的请求重试。

f.Retrying Requests

有时连接失败:连接池过时或断开连接,又或是Web服务器本身无法访问。
OkHttp
将使用不同的路由(如果路由可用)重试请求。

g.Calls

调用一下面两种方式之一执行:

同步:线程阻塞,直到响应可读。

异步:将请求放入队列,当响应可读时在另一个线程拿到回调。

可以在任何线程取消调用。如果请求没有完成,取消调用将会产生失败。编写请求体和响应体的代码将会在其取消时产生
IOException


h.Dispath

对于同步调用,你将自己管理同时发出多少请求。

对于异步调用,
Dispather
实现了最大并发策略。你可以设置每个网络服务器的最大值(默认值
5
)和整体值(默认值
64
)(译者注:这个不知道是什么意思……)。

2.Connections

尽管你只提供
URL
,OkHttp 则会使用三种类型(
URL
Address
Route
)与你的服务器相连。

a.URLs

URLs (如
https://github.com/square/okhttp
)是 HTTP 和 Internet 的基础。它除了是一个在网络上通用、分散的命名规则外,还标识了如何访问网络资源。

URLs 是抽象的:

它们指定调用可以是明文(
http
)或加密(
https
),但不指定使用哪种加密算法。它们也不指定如何验证对等体的证书(
HostnameVerifier
)或哪些证书可以信任(
SSLSocketFactory
)。

它们不指定是否应使用特定的代理服务器或如何使用该代理服务器进行身份验证。

它们也是具体的:每个URL标识一个特定的路径(如
/square/okhttp
)和查询(如
?q=sharks&lang=en
)。 每个Web服务器都托管多个 URL。

b.Address

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

共享相同地址的 URL 也可以共享相同的底层 TCP 套接字连接。共享连接具有显着的性能优势:更低的延迟,更高的吞吐量(由于 TCP 启动慢)和节省电量。 OkHttp 使用 ConnectionPool,自动重用 HTTP/1.x 连接并复用 HTTP/2 和 SPDY 连接。

在OkHttp中,地址的一些字段来自 URL(scheme,主机名,端口),其余的来自 OkHttpClient。

c.Routes

路由提供实际连接到Web服务器所需的动态信息。这是要尝试的特定IP地址(如由DNS查询所发现的),要使用的确切代理服务器(如果使用了 ProxySelector)和要协商的TLS的哪个版本(用于HTTPS连接)。

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

d.Connections

当你使用 OkHttp 请求网址时,它做了如下工作:

使用 URL 和配置好的 OkHttpClient 创建一个address。此地址指定如何连接到网络服务器。

尝试从connection pool连接池中检索使用该地址的连接。

如果在连接池中找不到连接,它会选择要尝试的route路由。这通常意味着使用 DNS 请求获取服务器的IP地址。然后,如有必要,选择TLS版本和代理服务器。

如果它是一个新路由,它通过构建直接的 socket 连接,TLS 隧道(通过HTTP proxy 的 HTTPS)或直接TLS连接进行连接。如果有必要,进行 TLS 握手。

发送 HTTP 请求并读取响应。

如果连接有问题,OkHttp将选择另一个路由重试。这允许 OkHttp 在服务器的地址的不可用时恢复。当连接池失效或尝试的TLS版本不受支持时,也很有用。

一旦接收到响应,连接将返回到连接池,以便它可以被将来的其他请求重用。在不活动一段时间后,连接将从连接池中移除。

3.Recipes

一些使用 OkHttp 的示例。

a.Synchronous Get

下载一个文件,打印请求响应的响应头,把响应体作为 string 打印。

响应体的
string()
方法对于小文件来说是方便和高效的。但是如果响应体比较大(大于 1MiB ),就要避免使用
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());
}


b.Asynchronous Get

在工作线程下载文件,并在响应可读时回调。回调在响应头准备好之后进行。读取响应体可能仍会阻塞。 OkHttp 目前不提供异步API来接收部分响应体。

pr
174d3
ivate 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());
}
});
}


c.Accessing Headers

HTTP 头的通常工作方式像一个
Map<String, String>
:每一个字段对应有一个值或没有。但一些头允许有多个值,像 Guava 的 Multimap。例如,HTTP 响应提供多个
Vary
头是合法且常见的。OkHttp 的 API 试图兼容这两种情况。

在写请求头的时候,使用
header(name, value)
设置 name 的唯一对应值 value。如果 value 已经存在,那么它将会在新的 value 添加进来之前把老的 value 移除。(译者注:使用
header(name, value)
设置请求头时,name 是唯一的,name 对应的值 value 也只有一个,并且以最新的值为准,因为之前的 value 会被删除。) 使用
addHeader(name,value)
添加头则不会删除之前的 value 。

在读取响应头时,使用
header(name)
返回最后一次出现 name 的值。通常这是唯一的。如果没有值,
header(name)
将返回 null。要读取 name 对应的所有的值作为列表读取,使用
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"));
}


d.Posting a String

使用 HTTP POST 将请求正文发送到服务器。此示例将 markdown 文档发布到将 markdown 标记为 HTML 的Web服务。由于整个请求体在内存中,因此请避免使用此 API 发布大(大于 1MiB )文档。

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


e.Post Streaming

这个例子我们将请求体作为流提交。请求体的内容边生成边写入。这个例子直接使用的时 Okio,如果你使用的是
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();

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

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


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


g.Posting form parameters

使用
FormBody.Builder
可以构建一个类似于
HTML<form>
标签的请求体。Names 和 values 将使用 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());
}


h.Posting a multipart request

MultipartBody.Builder
可以构建与HTML文件上传表单兼容的复杂请求体。
MultipartBody
的每个部分本身都是一个请求体,并且可以定义其自己的 headers。如果存在,这些 headers 描述子请求体,例如子请求体的
Content-Disposition
。如果
Content-Length
Content-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());
}


i.Parse a JSON Response With Gson

Gson 是一个用于在 JSON 和 Java 对象之间进行转换的方便 API。这里我们使用它来解码来自 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;
}


j.Response Caching

为了缓存响应,你需要一个可以读写的缓存目录,并且设置缓存的大小。缓存目录应该是私有的,不受信任的应用不能访问其内容。

使多个缓存同时访问同一缓存目录是错误的。大多数应用程序应该调用
new OkHttpClient
只一次,配置它的缓存,并在所有使用它的地方使用同一个实例。否则,两个缓存实例会互斥,破坏响应缓存,并可能导致程序崩溃。

Response caching uses HTTP headers for all configuration. You can add request headers like
Cache-Control: max-stale=3600
and OkHttp’s cache will honor them. Your webserver configures how long responses are cached with its own response headers, like
Cache-Control: max-age=9600
. There are cache headers to force a cached response, force a network response, or force the network response to be validated with a conditional 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 将返回
504 Unsatisfiable Request
响应。

k.Canceling a Call

使用
Call.cancel()
立即停止正在进行的请求。如果线程正在写入请求或读取响应,会发生
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);
}
}


l.Timeouts

当请求无法到达服务器时使用超时使请求失败。造成这种情况的网络原因可能使客户端连接问题,服务器服务不可用或者两者之间的其他原因。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);
}


m.Per-call Configuration

所有的 HTTP 客户端配置包括代理设置、超时和缓存都存在于
OkHttpClient
中。 当需要更改单个请求的配置时,请使用
OkHttpClient.newBuilder()
。 这将返回与原始客户端共享同一连接池,调度器和配置的构造器。 在下面的示例中,我们使用500毫秒超时发出一个请求,另一个请求使用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);
}
}


n.Handling authentication

OkHttp 可以自动重试未经身份验证的请求。当响应为
401未授权
时,将要求
Authenticator
提供凭据。构建包含证书的新请求。如果没有可用的凭据,则返回null以跳过重试。

Use Response.challenges() to get the schemes and realms of any authentication challenges. When fulfilling a Basic challenge, use Credentials.basic(username, password) to encode the request header.

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


4.Interceptors

拦截器是一个可以监视,重写和重试呼叫的强大机制。这里有一个简单的拦截器记录请求和响应。

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


chain.proceed(request)
的调用是每个拦截器实现的关键部分。这个简单的方法是所有HTTP工作,产生满足请求响应的地方。

拦截器可以链接。假设你有一个压缩拦截器和一个校验和拦截器:你需要决定数据是压缩然后校验,还是校验后压缩。 OkHttp使用列表跟踪拦截器,拦截器按顺序调用。

a.Application Interceptors

拦截器可以被注册为应用拦截器或网络拦截器。我们将使用上面定义的
LoggingInterceptor
来展示它们之间的差异。

通过在
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();


网址
http://www.publicobject.com/helloworld.txt
重定向到
https://publicobject.com/helloworld.txt
,OkHttp 会自动跟踪此重定向。我们的应用拦截器被调用一次,并且从
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()
equest.url()
不一样。

b.Network Interceptors

注册网络拦截器与注册应用拦截器非常相似,把
addInterceptor()
替换为
addNetworkInterceptor()
就行了:

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


当运行上面代码时,拦截器执行了两次。第一次是请求
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
标识对响应压缩的支持。网络拦截器的
Chain
有一个非空连接,可用于查询连接到Web服务器的 IP 地址和 TLS 配置。

c.Choosing between application and network interceptors

每种拦截器链都有其相对的优点。

Application interceptors

不需要担心中间的响应(如重定向和重试)。

总是调用一次,即使HTTP响应是从缓存提供的。

遵守应用程序的原始意图。 不关心OkHttp注入headers If-None-Match。

允许短路,允许不调用
Chain.proceed()


允许重试并调用多个
Chain.proceed()


Network interceptors

能够对中间的响应(如重定向和重试)进行操作。

不调用缓存响应使网络短路。

能够观察传输的数据。

能够访问携带请求的连接。

d.Rewriting Requests

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

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


e.Rewriting Responses

对称地,拦截器可以重写响应头并转换响应体。这通常比重写请求头更危险,因为它可能违反网络服务器的协议!

如果你处在一个棘手的情况,并准备处理后果,重写响应头是解决问题的有力方法。 例如,您可以修复服务器配置错误的
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();
}
};


f.Availability

OkHttp 的拦截器需要 OkHttp 2.2 或更高的版本。 不幸的是,拦截器不能使用
OkUrlFactory
或由它构建的库,包括 Retrofit ≤ 1.8和 Picasso ≤ 2.4。

5.HTTPS

OkHttp 尽可能去平衡连接到更多的主机和连接的安全性这两者的关系。

当 OKHttp 通过一个连接连接到一个 HTTPS 服务器,OkHttp 需要知道服务器提供的 TLS Versioncipher suites。想要最大程度的提高连接性即连接更多的服务器,客户端就要支持连接包括有着过时 TLS 版本和弱密码设计的服务器。如果要求最大化安全性,则客户端仅限连接有着最新 TLS 版本和密码安全设计的比较强的服务器。

特定的安全性和连接性决定由
ConnectionSpec
实现。 OkHttp 包括三个内置的连接规则:

MODERN_TLS 是一种连接到新式 HTTPS 服务器的安全配置。

COMPATIBLE_TLS 是一种连接到安全但不是当前HTTPS服务器的安全配置。

CLEARTEXT 是用于
http://
网址的不安全配置。

默认情况下,OkHttp 将尝试一个
MODERN_TLS
连接,如果配置的连接失败,则回退到
COMPATIBLE_TLS
连接。

LS版本和密码套件的规范可能会随每个版本变化。例如,在 OkHttp 2.2 中,我们在响应中放弃了对 SSL 3.0 的支持,以防止POODLE攻击。在OkHttp 2.3 中,我们放弃了对 RC4 的支持。与你的桌面网络浏览器一样,保持使用最新的 OkHttp 版本是保持安全的最佳方式。

你可以使用一组自定义的TLS版本和密码套件来构建您自己的连接规范。例如,该配置限于三个高度重视的密码套件。(这句的原文: For example, this configuration is limited to three highly-regarded cipher suites.)它的缺点是它需要 Android 5.0+ 和当前网络服务器的支持。

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


a.Certificate Pinning

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


b.Customizing Trusted Certificates

下面的代码完整的展示了如何使用自定义的设置代替证书机构颁发的证书。如上所述,没有服务器 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.
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 文档