OKHttp功能类介绍
OKHttp网络请求流程分析
OKHttp连接池
OKHttp分发器
OKHttp拦截器
HTTP是现代应用程序网络的方式。这就是我们交换数据和媒体的方式。有效地执行HTTP可使您的内容加载更快并节省带宽。
OkHttp是默认情况下有效的HTTP客户端:
当网络出现问题时,OkHttp会坚持不懈:它将从常见的连接问题中静默恢复。如果您的服务具有多个IP地址,则在第一次连接失败时,OkHttp将尝试使用备用地址。这对于IPv4 + IPv6和冗余数据中心中托管的服务是必需的。 OkHttp支持现代TLS功能(TLS 1.3,ALPN,证书固定)。可以将其配置为回退以实现广泛的连接。
使用OkHttp很容易。它的请求/响应API具有流畅的构建器和不变性。它支持同步阻塞调用和带有回调的异步调用。
该程序下载URL,并将其内容打印为字符串。
OkHttpClient client = new OkHttpClient();
String run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
post提交数据到服务器的方式。
public static final MediaType JSON
= MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
OkHttp可在Android 5.0+(API级别21+)和Java 8+上运行。
OkHttp依靠Okio获得高性能的I / O和Kotlin标准库。两者都是具有强大的向后兼容性的小型库。
我们强烈建议您保持OkHttp为最新。与自动更新Web浏览器一样,保持HTTPS客户端的最新状态是防范潜在安全问题的重要防御措施。我们跟踪动态TLS生态系统并调整OkHttp以改善连接性和安全性。
OkHttp使用您平台的内置TLS实现。在Java平台上,OkHttp还支持Conscrypt,它将BoringSSL与Java集成在一起。如果OkHttp是第一个安全提供程序,它将使用Conscrypt:
Security.insertProviderAt(Conscrypt.newProvider(), 1);
OkHttp 3.12.x分支支持Android 2.3+(API级别9+)和Java 7+。这些平台不支持TLS 1.2,因此不应使用。但是由于升级困难,我们将在2021年12月31日之前将重要补丁程序移植到3.12.x分支中。
最新版本在Maven Central上可用。
implementation("com.squareup.okhttp3:okhttp:4.5.0")
HTTP客户端的工作是接受您的请求并产生响应。从理论上讲这很简单,但在实践中却很棘手
每个HTTP请求都包含一个URL,一个方法(如GET或POST)和标头列表。请求还可以包含body:特定内容类型的数据流。
响应使用代码(例如200表示成功或404表示找不到),标头和其自己的可选body来回答请求。
当您向OkHttp提供HTTP请求时,就是在高层次上描述该请求:“使用这些标头向我获取此URL。”为了确保正确性和效率,OkHttp在传输请求之前会先对其进行重写。
OkHttp可以添加原始请求中不存在的标头,包括Content-Length,Transfer-Encoding,User-Agent,Host,Connection和Content-Type。它将添加用于透明响应压缩的Accept-Encoding头,除非该头已经存在。如果您有Cookie,OkHttp会在其中添加一个Cookie标头。
一些请求将具有缓存的响应。如果此缓存响应不新鲜,OkHttp可以执行条件GET来下载更新的响应,如果它比缓存的要新。这要求添加诸如If-Modified-Since和If-None-Match的标题。
如果使用透明压缩,则OkHttp将删除相应的响应标头Content-Encoding和Content-Length,因为它们不适用于解压缩的响应正文。
如果有条件的GET成功,则按照规范的指示将来自网络和缓存的响应合并。
当您请求的URL移动后,网络服务器将返回响应代码(例如302),以指示文档的新URL。 OkHttp将遵循重定向以检索最终响应。
如果响应发出授权挑战,OkHttp将要求Authenticator(如果已配置)满足挑战。如果身份验证器提供了凭据,则会使用该凭据重试请求。
有时连接失败:池化连接已陈旧且已断开连接,或者无法访问网络服务器本身。如果可用,OkHttp将使用其他路由重试该请求。
通过重写,重定向,跟进和重试,您的简单请求可能会产生许多请求和响应。 OkHttp使用Call来建模通过许多中间请求和响应来满足您的请求的任务。通常数量不多!但是,很高兴知道,如果您的URL重定向或您故障转移到备用IP地址,您的代码将继续有效。
调用以两种方式之一执行:
可以从任何线程取消呼叫。如果尚未完成,将导致呼叫失败!当取消调用时,编写请求正文或读取响应正文的代码将发生IOException。
对于同步调用,您需要带上自己的线程并负责管理您发出的同时请求数。同时连接过多会浪费资源。太少会损害延迟。
对于异步调用,Dispatcher实施最大并发请求的策略。您可以设置每个Web服务器的最大值(默认为5)和整体(默认为64)。
尽管仅提供URL,但OkHttp使用以下三种类型计划其与Web服务器的连接:URL, Address, and Route.
URL(例如https://github.com/square/okhttp)是HTTP和Internet的基础。他们不仅是针对网络上所有内容的通用,分散式命名方案,还规定了如何访问网络资源。
网址是抽象的:
他们指定Call可以是纯文本(http)或加密(https),但不应该使用哪种加密算法。他们也没有指定如何验证对等方的证书(HostnameVerifier)或可以信任的证书(SSLSocketFactory)。
他们没有指定是否应使用特定的代理服务器或如何使用该代理服务器进行身份验证。
它们也很具体:每个网址都标识一个特定的路径( 如/square/okhttp )和查询(如?q=sharks&lang=en)。每个Web服务器托管许多URL。
地址指定一个Web服务器(例如github.com)和连接到该服务器所需的所有静态配置:端口号,HTTPS设置和首选网络协议(例如HTTP / 2或SPDY)。
共享相同地址的URL也可能共享相同的基础TCP套接字连接。共享连接具有显着的性能优势:更低的延迟,更高的吞吐量(由于TCP启动缓慢)和节省的电池。 OkHttp使用一个ConnectionPool,它可以自动重用HTTP / 1.x连接并复用HTTP / 2和SPDY连接。
在OkHttp中,地址的某些字段来自URL(方案,主机名,端口),其余部分来自OkHttpClient。
路由提供实际连接到Web服务器所需的动态信息。这是要尝试的特定IP地址(由DNS查询发现),要使用的确切代理服务器(如果正在使用ProxySelector)以及要协商的TLS版本(对于HTTPS连接)。
一个地址可能有很多路由。例如,托管在多个数据中心中的Web服务器可能会在其DNS响应中产生多个IP地址。
当您使用OkHttp请求URL时,它的作用是:
如果连接出现问题,OkHttp将选择其他路由,然后重试。这样,当服务器地址的一部分无法访问时,OkHttp可以恢复。如果共用连接失效或尝试的TLS版本不受支持,此功能也很有用。 收到响应后,连接将返回到池中,以便可以将其重新用于以后的请求。闲置一段时间后,连接将从池中退出。
通过事件,您可以捕获应用程序的HTTP调用中的指标。使用事件来监视:
子类EventListener和重写您感兴趣的事件的方法。在没有重定向或重试的成功HTTP调用中,此流程描述了事件的顺序。
这是一个示例事件侦听器,可显示带有时间戳的每个事件。
class PrintingEventListener extends EventListener {
private long callStartNanos;
private void printEvent(String name) {
long nowNanos = System.nanoTime();
if (name.equals("callStart")) {
callStartNanos = nowNanos;
}
long elapsedNanos = nowNanos - callStartNanos;
System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
}
@Override public void callStart(Call call) {
printEvent("callStart");
}
@Override public void callEnd(Call call) {
printEvent("callEnd");
}
@Override public void dnsStart(Call call, String domainName) {
printEvent("dnsStart");
}
@Override public void dnsEnd(Call call, String domainName, List inetAddressList) {
printEvent("dnsEnd");
}
...
}
我们添加几个Call请求:
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
System.out.println("REQUEST 1 (new connection)");
try (Response response = client.newCall(request).execute()) {
// Consume and discard the response body.
response.body().source().readByteString();
}
System.out.println("REQUEST 2 (pooled connection)");
try (Response response = client.newCall(request).execute()) {
// Consume and discard the response body.
response.body().source().readByteString();
}
侦听器将打印相应的事件:
REQUEST 1 (new connection)
0.000 callStart
0.010 dnsStart
0.017 dnsEnd
0.025 connectStart
0.117 secureConnectStart
0.586 secureConnectEnd
0.586 connectEnd
0.587 connectionAcquired
0.588 requestHeadersStart
0.590 requestHeadersEnd
0.591 responseHeadersStart
0.675 responseHeadersEnd
0.676 responseBodyStart
0.679 responseBodyEnd
0.679 connectionReleased
0.680 callEnd
REQUEST 2 (pooled connection)
0.000 callStart
0.001 connectionAcquired
0.001 requestHeadersStart
0.001 requestHeadersEnd
0.002 responseHeadersStart
0.082 responseHeadersEnd
0.082 responseBodyStart
0.082 responseBodyEnd
0.083 connectionReleased
0.083 callEnd
请注意,第二个呼叫如何不触发连接事件。它重用了从第一个请求开始的连接,从而显着提高了性能。
在前面的示例中,我们使用了一个名为callStartNanos的字段来跟踪每个事件的经过时间。这很方便,但是如果同时执行多个调用,它将不起作用。为了适应这种情况,请使用Factory为每个Call创建一个新的EventListener实例。这允许每个侦听器保持特定于呼叫的状态。
该sample factory 为每个呼叫创建唯一的ID,并使用该ID区分日志消息中的呼叫。
class PrintingEventListener extends EventListener {
public static final Factory FACTORY = new Factory() {
final AtomicLong nextCallId = new AtomicLong(1L);
@Override public EventListener create(Call call) {
long callId = nextCallId.getAndIncrement();
System.out.printf("%04d %s%n", callId, call.request().url());
return new PrintingEventListener(callId, System.nanoTime());
}
};
final long callId;
final long callStartNanos;
public PrintingEventListener(long callId, long callStartNanos) {
this.callId = callId;
this.callStartNanos = callStartNanos;
}
private void printEvent(String name) {
long elapsedNanos = System.nanoTime() - callStartNanos;
System.out.printf("%04d %.3f %s%n", callId, elapsedNanos / 1000000000d, name);
}
@Override public void callStart(Call call) {
printEvent("callStart");
}
@Override public void callEnd(Call call) {
printEvent("callEnd");
}
...
}
我们可以使用此侦听器来竞争一对并发的HTTP请求:
Request washingtonPostRequest = new Request.Builder()
.url("https://www.washingtonpost.com/")
.build();
client.newCall(washingtonPostRequest).enqueue(new Callback() {
...
});
Request newYorkTimesRequest = new Request.Builder()
.url("https://www.nytimes.com/")
.build();
client.newCall(newYorkTimesRequest).enqueue(new Callback() {
...
});
在家用WiFi上进行的比赛显示,Times(0002)比Post(0001)稍早完成:
0001 https://www.washingtonpost.com/
0001 0.000 callStart
0002 https://www.nytimes.com/
0002 0.000 callStart
0002 0.010 dnsStart
0001 0.013 dnsStart
0001 0.022 dnsEnd
0002 0.019 dnsEnd
0001 0.028 connectStart
0002 0.025 connectStart
0002 0.072 secureConnectStart
0001 0.075 secureConnectStart
0001 0.386 secureConnectEnd
0002 0.390 secureConnectEnd
0002 0.400 connectEnd
0001 0.403 connectEnd
0002 0.401 connectionAcquired
0001 0.404 connectionAcquired
0001 0.406 requestHeadersStart
0002 0.403 requestHeadersStart
0001 0.414 requestHeadersEnd
0002 0.411 requestHeadersEnd
0002 0.412 responseHeadersStart
0001 0.415 responseHeadersStart
0002 0.474 responseHeadersEnd
0002 0.475 responseBodyStart
0001 0.554 responseHeadersEnd
0001 0.555 responseBodyStart
0002 0.554 responseBodyEnd
0002 0.554 connectionReleased
0002 0.554 callEnd
0001 0.624 responseBodyEnd
0001 0.624 connectionReleased
0001 0.624 callEnd
EventListener.Factory还可以将指标限制为调用的子集。这是随机抽取10%的指标:
class MetricsEventListener extends EventListener {
private static final Factory FACTORY = new Factory() {
@Override public EventListener create(Call call) {
if (Math.random() < 0.10) {
return new MetricsEventListener(call);
} else {
return EventListener.NONE;
}
}
};
...
}
当操作失败时,将调用失败方法。这是connectFailed()用于在建立与服务器的连接时失败,而在HTTP调用永久失败时则用于callFailed()。发生故障时,开始事件可能没有相应的结束事件。
OkHttp具有弹性,可以自动从某些连接故障中恢复。在这种情况下,connectFailed()事件不是终端事件,也不是callFailed()之后的事件。尝试重试时,事件侦听器将收到多个相同类型的事件。
单个HTTP调用可能需要发出后续请求以处理身份验证质询,重定向和HTTP层超时。在这种情况下,可能会尝试多个连接,请求和响应。跟踪是单个调用可能触发相同类型的多个事件的另一个原因。
在OkHttp 3.11中,事件可以作为公共API使用。将来的版本可能会引入新的事件类型;您将需要覆盖相应的方法来处理它们。
OkHttp试图平衡两个相互冲突的问题:
在协商与HTTPS服务器的连接时,OkHttp需要知道要提供哪些TLS版本和密码套件。想要最大程度地提高连接性的客户端将包括过时的TLS版本和弱设计密码套件。想要最大化安全性的严格客户端将仅限于最新的TLS版本和最强的密码套件。
特定的安全性与连接性决定由ConnectionSpec实施。 OkHttp包含四个内置的连接规范:
这些宽松地遵循了Google云政策中设置的模型。我们跟踪对此政策的更改。 默认情况下,OkHttp将尝试建立MODERN_TLS连接。但是,如果现代配置失败,则可以通过配置客户端连接规范来允许回退到COMPATIBLE_TLS连接。
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.build();
每个规范中的TLS版本和密码套件可随每个发行版而更改。例如,在OkHttp 2.2中,为了响应POODLE攻击,我们放弃了对SSL 3.0的支持。在OkHttp 2.3中,我们放弃了对RC4的支持。与桌面Web浏览器一样,保持OkHttp的最新状态是确保安全的最佳方法。
您可以使用一组自定义的TLS版本和密码套件来构建自己的连接规范。例如,此配置仅限于三个备受推崇的密码套件。它的缺点是它需要Android 5.0+和类似的当前Web服务器。
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();
默认情况下,OkHttp信任主机平台的证书颁发机构。此策略可最大程度地提高连接性,但会受到诸如2011 DigiNotar攻击等证书颁发机构的攻击。它还假定您的HTTPS服务器的证书是由证书颁发机构签名的。
使用CertificatePinner限制受信任的证书和证书颁发机构。证书固定可以提高安全性,但会限制您的服务器团队更新其TLS证书的能力。没有服务器的TLS管理员的祝福,请不要使用证书固定!
private final OkHttpClient 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();
try (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));
}
}
}
完整的代码示例显示了如何用您自己的证书集替换主机平台的证书颁发机构。如上所述,请勿使用未经服务器TLS管理员欢迎的自定义证书!
private final OkHttpClient client;
public CustomTrust() {
X509TrustManager trustManager;
SSLSocketFactory sslSocketFactory;
try {
trustManager = trustManagerForCertificates(trustedCertificatesInputStream());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { trustManager }, null);
sslSocketFactory = sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustManager)
.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.
}
拦截器是一种强大的机制,可以监视,重写和重试Call。这是一个简单的拦截器,用于记录传出请求和传入响应。
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工作发生的地方,它会产生一个响应来满足请求。如果不止一次调用chain.proceed(request),则必须关闭以前的响应主体。
拦截器可以链接。假设您同时具有压缩拦截器和校验和拦截器:您需要确定数据是先压缩后再校验和,还是先校验后再压缩。 OkHttp使用列表来跟踪拦截器,并按顺序调用拦截器。
拦截器已注册为应用程序或网络拦截器。我们将使用上面定义的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();
URL 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()与request.url()不同。这两个日志语句记录两个不同的URL。
注册网络拦截器非常相似。调用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();
当我们运行此代码时,拦截器将运行两次。一次是对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配置。
Application interceptors
Network Interceptors
拦截器可以添加,删除或替换请求标头。他们还可以转换那些具有一个请求的主体。例如,如果您要连接到已知支持请求的网络服务器,则可以使用应用程序拦截器添加请求正文压缩。
/** 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();
}
};
}
}
相应地,拦截器可以重写响应头并转换响应主体。这通常比重写请求标头更危险,因为它可能违反网络服务器的期望! 如果您处在棘手的情况下并准备好处理后果,则重写响应标头是解决问题的有效方法。例如,您可以修复服务器的错误配置的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服务器上的相应修复程序进行补充时,此方法最有效!
我们编写了一些样例,展示了如何解决OkHttp的常见问题。通读它们以了解一切如何协同工作。随意剪切并粘贴这些示例;这就是他们的目的。
下载文件,打印其标题,并将其响应主体打印为字符串。 响应主体上的string()方法对于小型文档而言既方便又高效。但是,如果响应主体很大(大于1 MiB),请避免使用string(),因为它将把整个文档加载到内存中。在这种情况下,最好将主体作为流处理。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
try (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());
}
}
在辅助线程上下载文件,并在响应可读时调用该文件。响应头准备好后进行回调。读取响应正文可能仍然会阻塞。 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 {
try (ResponseBody responseBody = response.body()) {
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(responseBody.string());
}
}
});
}
通常,HTTP标头的工作方式类似于Map
如果存在现有值,将在添加新值之前将其删除。使用addHeader(name,value)添加标题而不删除已经存在的标题。 当读取响应头时,使用header(name)返回最后一次出现的命名值。通常这也是唯一的情况!如果不存在任何值,则header(name)将返回null。要将所有字段的值读取为列表,请使用headers(name)。
要访问所有Header,请使用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();
try (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"));
}
}
使用HTTP POST将请求正文发送到服务。本示例将markdown文档发布到将markdown呈现为HTML的Web服务。因为整个请求主体同时在内存中,所以请避免使用此API发布大型(大于1 MiB)文档。
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
在这里,我们将请求正文作为流发布。该请求正文的内容是在编写时生成的。此示例直接流入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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
将文件用作请求正文很容易.
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
使用FormBody.Builder可以构建类似于HTML