OKHttpd的使用教程



Android系统提供了两种HTTP通信类,HttpURLConnection和HttpClient。
关于HttpURLConnection和HttpClient的选择>>官方博客
尽管Google在大部分安卓版本中推荐使用HttpURLConnection,但是这个类相比HttpClient实在是太难用,太弱爆了。
OkHttp是一个相对成熟的解决方案,据说Android4.4的源码中可以看到HttpURLConnection已经替换成OkHttp实现了。所以我们更有理由相信OkHttp的强大。

OkHttp 处理了很多网络疑难杂症:会从很多常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP。OkHttp还处理了代理服务器问题和SSL握手失败问题。

使用 OkHttp 无需重写您程序中的网络代码。OkHttp实现了几乎和Java.NET.HttpURLConnection一样的API。如果你用了 Apache HttpClient,则OkHttp也提供了一个对应的okhttp-apache 模块。


注:在国内使用OkHttp会因为这个问题导致部分酷派手机用户无法联网,所以对于大众app来说,需要等待这个bug修复后再使用。或者尝试使用OkHttp的老版本。
截止到目前,OkHttp一直没有修复,并把修复计划延迟到了OkHttp2.3中。不是所有设备都能重现,仅少量设备会出现这个问题。(如果问题这么明显,OkHttp早就修复了)

入门

官方资料

官方介绍
github源码

使用范围

OkHttp支持Android 2.3及其以上版本。
对于Java, JDK1.7以上。

jar包准备

官方介绍页面有链接位置。这里把下载链接也写在下面。
OkHttp
Okio

基本使用

HTTP GET

1
2
3
4
5
6
7
8
OkHttpClient client =  new  OkHttpClient();
 
String run(String url) throws IOException {
     Request request =  new  Request.Builder().url(url).build();
     Response response = client.newCall(request).execute();     if  (response.isSuccessful()) {         return  response.body().string();
     else  {         throw  new  IOException( "Unexpected code "  + response);
     }
}

Request是OkHttp中访问的请求,Builder是辅助类。Response即OkHttp中的响应。

Response类:
1
2
3
public boolean isSuccessful()
Returns  true  if  the code is  in  [200..300),
  which means the request was successfully received, understood, and accepted.
response.body()返回ResponseBody类

可以方便的获取string

1
2
3
4
public final String string() throws IOException
Returns the response as a string decoded  with  the charset of the Content-Type header. If that header is either absent or lacks a charset,
  this  will attempt to decode the response body as UTF-8.Throws:
IOException

当然也能获取到流的形式:

1
public final InputStream byteStream()

HTTP POST

POST提交Json数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final MediaType JSON = MediaType.parse( "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();
       Response response = client.newCall(request).execute();
     f (response.isSuccessful()) {
         return  response.body().string();
     else  {
         throw  new  IOException( "Unexpected code "  + response);
     }
}

使用Request的post方法来提交请求体RequestBody

POST提交键值对

很多时候我们会需要通过POST方式把键值对数据传送到服务器。 OkHttp提供了很方便的方式来做这件事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
OkHttpClient client =  new  OkHttpClient();
String post(String url, String json) throws IOException {
 
      RequestBody formBody =  new  FormEncodingBuilder()
     .add( "platform" "android" )
     .add( "name" "bug" )
     .add( "subject" "XXXXXXXXXXXXXXX" )
     .build();
 
       Request request =  new  Request.Builder()
       .url(url)
       .post(body)
       .build();
 
       Response response = client.newCall(request).execute();
     if  (response.isSuccessful()) {
         return  response.body().string();
     else  {
         throw  new  IOException( "Unexpected code "  + response);
     }
}

总结

通过上面的例子我们可以发现,OkHttp在很多时候使用都是很方便的,而且很多代码也有重复,因此特地整理了下面的工具类。
注意:

  • OkHttp官方文档并不建议我们创建多个OkHttpClient,因此全局使用一个。 如果有需要,可以使用clone方法,再进行自定义。这点在后面的高级教程里会提到。

  • enqueue为OkHttp提供的异步方法,入门教程中并没有提到,后面的高级教程里会有解释。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import cn.wiz.sdk.constant.WizConstant;
import com.squareup.okhttp.Callback;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response; 
  
public class OkHttpUtil {
     private static final OkHttpClient mOkHttpClient =  new  OkHttpClient();
     static{
         mOkHttpClient.setConnectTimeout(30, TimeUnit.SECONDS);
     }
     /**
      * 该不会开启异步线程。
      * @param request
      * @return
      * @throws IOException
      */
     public static Response execute(Request request) throws IOException{
         return  mOkHttpClient.newCall(request).execute();
     }
     /**
      * 开启异步线程访问网络
      * @param request
      * @param responseCallback
      */
     public static void enqueue(Request request, Callback responseCallback){
         mOkHttpClient.newCall(request).enqueue(responseCallback);
     }
     /**
      * 开启异步线程访问网络, 且不在意返回结果(实现空callback)
      * @param request
      */
     public static void enqueue(Request request){
         mOkHttpClient.newCall(request).enqueue( new  Callback() {
             
             @Override
             public void onResponse(Response arg0) throws IOException {
                 
             }
             
             @Override
             public void onFailure(Request arg0, IOException arg1) {
                 
             }
         });
     }
     public static String getStringFromServer(String url) throws IOException{
         Request request =  new  Request.Builder().url(url).build();
         Response response = execute(request);
         if  (response.isSuccessful()) {
             String responseUrl = response.body().string();
             return  responseUrl;
         else  {
             throw  new  IOException( "Unexpected code "  + response);
         }
     }
     private static final String CHARSET_NAME =  "UTF-8" ;
     /**
      * 这里使用了HttpClinet的API。只是为了方便
      * @param params
      * @return
      */
     public static String formatParams(List<BasicNameValuePair> params){
         return  URLEncodedUtils.format(params, CHARSET_NAME);
     }
     /**
      * 为HttpGet 的 url 方便的添加多个name value 参数。
      * @param url
      * @param params
      * @return
      */
     public static String attachHttpGetParams(String url, List<BasicNameValuePair> params){
         return  url +  "?"  + formatParams(params);
     }
     /**
      * 为HttpGet 的 url 方便的添加1个name value 参数。
      * @param url
      * @param name
      * @param value
      * @return
      */
     public static String attachHttpGetParam(String url, String name, String value){
         return  url +  "?"  + name +  "="  + value;
     }
}

高级

高级属性其实用的不多,这里主要是对OkHttp github官方教程进行了翻译。

同步get

下载一个文件,打印他的响应头,以string形式打印响应体。
响应体的 string() 方法对于小文档来说十分方便、高效。但是如果响应体太大(超过1MB),应避免适应 string()方法 ,因为他会将把整个文档加载到内存中。
对于超过1MB的响应body,应使用流的方式来处理body。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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());
}

异步get

在一个工作线程中下载文件,当响应可读时回调Callback接口。读取响应时会阻塞当前线程。OkHttp现阶段不提供异步api来接收响应体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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(Request request, Throwable throwable) {
         throwable.printStackTrace();
       }
 
       @Override public void onResponse(Response response) throws IOException {
         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());
       }
     });
}

提取响应头

典型的HTTP头 像是一个 Map<String, String> :每个字段都有一个或没有值。但是一些头允许多个值,像Guava的Multimap。例如:HTTP响应里面提供的Vary响应头,就是多值的。OkHttp的api试图让这些情况都适用。
当写请求头的时候,使用header(name, value)可以设置唯一的name、value。如果已经有值,旧的将被移除,然后添加新的。使用addHeader(name, value)可以添加多值(添加,不移除已有的)。
当读取响应头时,使用header(name)返回最后出现的name、value。通常情况这也是唯一的name、value。如果没有值,那么header(name)将返回null。如果想读取字段对应的所有值,使用headers(name)会返回一个list。
为了获取所有的Header,Headers类支持按index访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final OkHttpClient client =  new  OkHttpClient();
 
public void run() throws Exception {
     Request request =  new  Request.Builder()
         .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" ));
}

Post方式提交String

使用HTTP POST提交请求到服务。这个例子提交了一个markdown文档到web服务,以HTML方式渲染markdown。因为整个请求体都在内存中,因此避免使用此api提交大文档(大于1MB)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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方式提交流

以流的方式POST提交请求体。请求体的内容由流写入产生。这个例子是流直接写入Okio的BufferedSink。你的程序可能会使用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
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方式提交文件

以文件作为请求体是十分简单的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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());
}

Post方式提交表单

使用FormEncodingBuilder来构建和HTML<form>标签相同效果的请求体。键值对将使用一种HTML兼容形式的URL编码来进行编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final OkHttpClient client =  new  OkHttpClient();
 
public void run() throws Exception {
     RequestBody formBody =  new  FormEncodingBuilder()
         .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());
}

Post方式提交分块请求

MultipartBuilder可以构建复杂的请求体,与HTML文件上传形式兼容。多块请求体中每块请求都是一个请求体,可以定义自己的请求头。这些请求头可以用来描述这块请求,例如他的Content-Disposition。如果Content-LengthContent-Type可用的话,他们会被自动添加到请求头中。

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
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  MultipartBuilder()
         .type(MultipartBuilder.FORM)
         .addPart(
             Headers.of( "Content-Disposition" "form-data; name=\"title\"" ),
             RequestBody.create( null "Square Logo" ))
         .addPart(
             Headers.of( "Content-Disposition" "form-data; name=\"image\"" ),
             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是一个在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
21
22
23
24
private final OkHttpClient client =  new  OkHttpClient();
private final Gson gson =  new  Gson();
 
public void run() throws Exception {
     Request request =  new  Request.Builder()
         .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;
}

响应缓存

为了缓存响应,你需要一个你可以读写的缓存目录,和缓存大小的限制。这个缓存目录应该是私有的,不信任的程序应不能读取缓存内容。
一个缓存目录同时拥有多个缓存访问是错误的。大多数程序只需要调用一次new OkHttp(),在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,而且有可能会导致程序崩溃。
响应缓存使用HTTP头作为配置。你可以在请求头中添加Cache-Control: max-stale=3600 ,OkHttp缓存会支持。你的服务通过响应头确定响应缓存多长时间,例如使用Cache-Control: max-age=9600

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
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();
     client.setCache(cache);
}
 
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));
}

扩展

在这一节还提到了下面一句:
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.

我不是很懂cache,平时用到的也不多,所以把Google在Android Developers一段相关的解析放到这里吧。

Force a Network Response

In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip the cache, and fetch data directly from the server. To force a full refresh, add the no-cache directive:

[java]  view plain  copy
  1. connection.addRequestProperty("Cache-Control""no-cache");  

If it is only necessary to force a cached response to be validated by the server, use the more efficient max-age=0 instead:

[java]  view plain  copy
  1. connection.addRequestProperty("Cache-Control""max-age=0");  

Force a Cache Response

Sometimes you'll want to show resources if they are available immediately, but not otherwise. This can be used so your application can show something while waiting for the latest data to be downloaded. To restrict a request to locally-cached resources, add the only-if-cached directive:

1
2
3
4
5
6
7
8
try  {
      connection.addRequestProperty( "Cache-Control" "only-if-cached" );
      InputStream cached = connection.getInputStream();
      // the resource was cached! show it
   catch  (FileNotFoundException e) {
      // the resource was not cached
  }
}

This technique works even better in situations where a stale response is better than no response. To permit stale cached responses, use the max-stale directive with the maximum staleness in seconds:

1
int maxStale = 60 * 60 * 24 * 28;  // tolerate 4-weeks staleconnection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);

以上信息来自:HttpResponseCache - Android SDK | Android Developers

取消一个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
30
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);
     }
}

超时

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final OkHttpClient client;
 
public ConfigureTimeouts() throws Exception {
     client =  new  OkHttpClient();
     client.setConnectTimeout(10, TimeUnit.SECONDS);
     client.setWriteTimeout(10, TimeUnit.SECONDS);
     client.setReadTimeout(30, TimeUnit.SECONDS);
}
 
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);
}

每个call的配置

使用OkHttpClient,所有的HTTP Client配置包括代理设置、超时设置、缓存设置。当你需要为单个call改变配置的时候,clone 一个OkHttpClient。这个api将会返回一个浅拷贝(shallow copy),你可以用来单独自定义。下面的例子中,我们让一个请求是500ms的超时、另一个是3000ms的超时。

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 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  {
       Response response = client.clone()  // Clone to make a customized OkHttp for this request.
           .setReadTimeout(500, TimeUnit.MILLISECONDS)
           .newCall(request)
           .execute();
       System.out.println( "Response 1 succeeded: "  + response);
     catch  (IOException e) {
       System.out.println( "Response 1 failed: "  + e);
     }
 
     try  {
       Response response = client.clone()  // Clone to make a customized OkHttp for this request.
           .setReadTimeout(3000, TimeUnit.MILLISECONDS)
           .newCall(request)
           .execute();
       System.out.println( "Response 2 succeeded: "  + response);
     catch  (IOException e) {
       System.out.println( "Response 2 failed: "  + e);
     }
}

处理验证

这部分和HTTP AUTH有关。
相关资料:HTTP AUTH 那些事 - 王绍全的博客 - 博客频道 - CSDN.NET

OkHttp会自动重试未验证的请求。当响应是401 Not Authorized时,Authenticator会被要求提供证书。Authenticator的实现中需要建立一个新的包含证书的请求。如果没有证书可用,返回null来跳过尝试。

1
2
3
4
5
6
public List<Challenge> challenges()
Returns the authorization challenges appropriate  for  this  response's code. 
If the response code is 401 unauthorized, 
this  returns the  "WWW-Authenticate"  challenges.
If the response code is 407 proxy unauthorized,  this  returns the  "Proxy-Authenticate"  challenges.
Otherwise  this  returns an empty list of challenges.

当需要实现一个Basic challenge, 使用Credentials.basic(username, password)来编码请求头。

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 final OkHttpClient client =  new  OkHttpClient();
 
public void run() throws Exception {
     client.setAuthenticator( new  Authenticator() {
       @Override public Request authenticate(Proxy proxy, Response response) {
         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();
       }
 
       @Override public Request authenticateProxy(Proxy proxy, Response response) {
         return  null // Null indicates no attempt to authenticate.
       }
     });
 
     Request request =  new  Request.Builder()
         .build();
 
     Response response = client.newCall(request).execute();
     if  (!response.isSuccessful())  throw  new  IOException( "Unexpected code "  + response);
 
     System.out.println(response.body().string());
}

Android系统提供了两种HTTP通信类,HttpURLConnection和HttpClient。
关于HttpURLConnection和HttpClient的选择>>官方博客
尽管Google在大部分安卓版本中推荐使用HttpURLConnection,但是这个类相比HttpClient实在是太难用,太弱爆了。
OkHttp是一个相对成熟的解决方案,据说Android4.4的源码中可以看到HttpURLConnection已经替换成OkHttp实现了。所以我们更有理由相信OkHttp的强大。

OkHttp 处理了很多网络疑难杂症:会从很多常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP。OkHttp还处理了代理服务器问题和SSL握手失败问题。

使用 OkHttp 无需重写您程序中的网络代码。OkHttp实现了几乎和Java.NET.HttpURLConnection一样的API。如果你用了 Apache HttpClient,则OkHttp也提供了一个对应的okhttp-apache 模块。


注:在国内使用OkHttp会因为这个问题导致部分酷派手机用户无法联网,所以对于大众app来说,需要等待这个bug修复后再使用。或者尝试使用OkHttp的老版本。
截止到目前,OkHttp一直没有修复,并把修复计划延迟到了OkHttp2.3中。不是所有设备都能重现,仅少量设备会出现这个问题。(如果问题这么明显,OkHttp早就修复了)

入门

官方资料

官方介绍
github源码

使用范围

OkHttp支持Android 2.3及其以上版本。
对于Java, JDK1.7以上。

jar包准备

官方介绍页面有链接位置。这里把下载链接也写在下面。
OkHttp
Okio

基本使用

HTTP GET

1
2
3
4
5
6
7
8
OkHttpClient client =  new  OkHttpClient();
 
String run(String url) throws IOException {
     Request request =  new  Request.Builder().url(url).build();
     Response response = client.newCall(request).execute();     if  (response.isSuccessful()) {         return  response.body().string();
     else  {         throw  new  IOException( "Unexpected code "  + response);
     }
}

Request是OkHttp中访问的请求,Builder是辅助类。Response即OkHttp中的响应。

Response类:
1
2
3
public boolean isSuccessful()
Returns  true  if  the code is  in  [200..300),
  which means the request was successfully received, understood, and accepted.
response.body()返回ResponseBody类

可以方便的获取string

1
2
3
4
public final String string() throws IOException
Returns the response as a string decoded  with  the charset of the Content-Type header. If that header is either absent or lacks a charset,
  this  will attempt to decode the response body as UTF-8.Throws:
IOException

当然也能获取到流的形式:

1
public final InputStream byteStream()

HTTP POST

POST提交Json数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final MediaType JSON = MediaType.parse( "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();
       Response response = client.newCall(request).execute();
     f (response.isSuccessful()) {
         return  response.body().string();
     else  {
         throw  new  IOException( "Unexpected code "  + response);
     }
}

使用Request的post方法来提交请求体RequestBody

POST提交键值对

很多时候我们会需要通过POST方式把键值对数据传送到服务器。 OkHttp提供了很方便的方式来做这件事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
OkHttpClient client =  new  OkHttpClient();
String post(String url, String json) throws IOException {
 
      RequestBody formBody =  new  FormEncodingBuilder()
     .add( "platform" "android" )
     .add( "name" "bug" )
     .add( "subject" "XXXXXXXXXXXXXXX" )
     .build();
 
       Request request =  new  Request.Builder()
       .url(url)
       .post(body)
       .build();
 
       Response response = client.newCall(request).execute();
     if  (response.isSuccessful()) {
         return  response.body().string();
     else  {
         throw  new  IOException( "Unexpected code "  + response);
     }
}

总结

通过上面的例子我们可以发现,OkHttp在很多时候使用都是很方便的,而且很多代码也有重复,因此特地整理了下面的工具类。
注意:

  • OkHttp官方文档并不建议我们创建多个OkHttpClient,因此全局使用一个。 如果有需要,可以使用clone方法,再进行自定义。这点在后面的高级教程里会提到。

  • enqueue为OkHttp提供的异步方法,入门教程中并没有提到,后面的高级教程里会有解释。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import cn.wiz.sdk.constant.WizConstant;
import com.squareup.okhttp.Callback;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response; 
  
public class OkHttpUtil {
     private static final OkHttpClient mOkHttpClient =  new  OkHttpClient();
     static{
         mOkHttpClient.setConnectTimeout(30, TimeUnit.SECONDS);
     }
     /**
      * 该不会开启异步线程。
      * @param request
      * @return
      * @throws IOException
      */
     public static Response execute(Request request) throws IOException{
         return  mOkHttpClient.newCall(request).execute();
     }
     /**
      * 开启异步线程访问网络
      * @param request
      * @param responseCallback
      */
     public static void enqueue(Request request, Callback responseCallback){
         mOkHttpClient.newCall(request).enqueue(responseCallback);
     }
     /**
      * 开启异步线程访问网络, 且不在意返回结果(实现空callback)
      * @param request
      */
     public static void enqueue(Request request){
         mOkHttpClient.newCall(request).enqueue( new  Callback() {
             
             @Override
             public void onResponse(Response arg0) throws IOException {
                 
             }
             
             @Override
             public void onFailure(Request arg0, IOException arg1) {
                 
             }
         });
     }
     public static String getStringFromServer(String url) throws IOException{
         Request request =  new  Request.Builder().url(url).build();
         Response response = execute(request);
         if  (response.isSuccessful()) {
             String responseUrl = response.body().string();
             return  responseUrl;
         else  {
             throw  new  IOException( "Unexpected code "  + response);
         }
     }
     private static final String CHARSET_NAME =  "UTF-8" ;
     /**
      * 这里使用了HttpClinet的API。只是为了方便
      * @param params
      * @return
      */
     public static String formatParams(List<BasicNameValuePair> params){
         return  URLEncodedUtils.format(params, CHARSET_NAME);
     }
     /**
      * 为HttpGet 的 url 方便的添加多个name value 参数。
      * @param url
      * @param params
      * @return
      */
     public static String attachHttpGetParams(String url, List<BasicNameValuePair> params){
         return  url +  "?"  + formatParams(params);
     }
     /**
      * 为HttpGet 的 url 方便的添加1个name value 参数。
      * @param url
      * @param name
      * @param value
      * @return
      */
     public static String attachHttpGetParam(String url, String name, String value){
         return  url +  "?"  + name +  "="  + value;
     }
}

高级

高级属性其实用的不多,这里主要是对OkHttp github官方教程进行了翻译。

同步get

下载一个文件,打印他的响应头,以string形式打印响应体。
响应体的 string() 方法对于小文档来说十分方便、高效。但是如果响应体太大(超过1MB),应避免适应 string()方法 ,因为他会将把整个文档加载到内存中。
对于超过1MB的响应body,应使用流的方式来处理body。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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());
}

异步get

在一个工作线程中下载文件,当响应可读时回调Callback接口。读取响应时会阻塞当前线程。OkHttp现阶段不提供异步api来接收响应体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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(Request request, Throwable throwable) {
         throwable.printStackTrace();
       }
 
       @Override public void onResponse(Response response) throws IOException {
         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());
       }
     });
}

提取响应头

典型的HTTP头 像是一个 Map<String, String> :每个字段都有一个或没有值。但是一些头允许多个值,像Guava的Multimap。例如:HTTP响应里面提供的Vary响应头,就是多值的。OkHttp的api试图让这些情况都适用。
当写请求头的时候,使用header(name, value)可以设置唯一的name、value。如果已经有值,旧的将被移除,然后添加新的。使用addHeader(name, value)可以添加多值(添加,不移除已有的)。
当读取响应头时,使用header(name)返回最后出现的name、value。通常情况这也是唯一的name、value。如果没有值,那么header(name)将返回null。如果想读取字段对应的所有值,使用headers(name)会返回一个list。
为了获取所有的Header,Headers类支持按index访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final OkHttpClient client =  new  OkHttpClient();
 
public void run() throws Exception {
     Request request =  new  Request.Builder()
         .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" ));
}

Post方式提交String

使用HTTP POST提交请求到服务。这个例子提交了一个markdown文档到web服务,以HTML方式渲染markdown。因为整个请求体都在内存中,因此避免使用此api提交大文档(大于1MB)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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方式提交流

以流的方式POST提交请求体。请求体的内容由流写入产生。这个例子是流直接写入Okio的BufferedSink。你的程序可能会使用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
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方式提交文件

以文件作为请求体是十分简单的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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());
}

Post方式提交表单

使用FormEncodingBuilder来构建和HTML<form>标签相同效果的请求体。键值对将使用一种HTML兼容形式的URL编码来进行编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final OkHttpClient client =  new  OkHttpClient();
 
public void run() throws Exception {
     RequestBody formBody =  new  FormEncodingBuilder()
         .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());
}

Post方式提交分块请求

MultipartBuilder可以构建复杂的请求体,与HTML文件上传形式兼容。多块请求体中每块请求都是一个请求体,可以定义自己的请求头。这些请求头可以用来描述这块请求,例如他的Content-Disposition。如果Content-LengthContent-Type可用的话,他们会被自动添加到请求头中。

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
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  MultipartBuilder()
         .type(MultipartBuilder.FORM)
         .addPart(
             Headers.of( "Content-Disposition" "form-data; name=\"title\"" ),
             RequestBody.create( null "Square Logo" ))
         .addPart(
             Headers.of( "Content-Disposition" "form-data; name=\"image\"" ),
             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是一个在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
21
22
23
24
private final OkHttpClient client =  new  OkHttpClient();
private final Gson gson =  new  Gson();
 
public void run() throws Exception {
     Request request =  new  Request.Builder()
         .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;
}

响应缓存

为了缓存响应,你需要一个你可以读写的缓存目录,和缓存大小的限制。这个缓存目录应该是私有的,不信任的程序应不能读取缓存内容。
一个缓存目录同时拥有多个缓存访问是错误的。大多数程序只需要调用一次new OkHttp(),在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,而且有可能会导致程序崩溃。
响应缓存使用HTTP头作为配置。你可以在请求头中添加Cache-Control: max-stale=3600 ,OkHttp缓存会支持。你的服务通过响应头确定响应缓存多长时间,例如使用Cache-Control: max-age=9600

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
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();
     client.setCache(cache);
}
 
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));
}

扩展

在这一节还提到了下面一句:
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.

我不是很懂cache,平时用到的也不多,所以把Google在Android Developers一段相关的解析放到这里吧。

Force a Network Response

In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip the cache, and fetch data directly from the server. To force a full refresh, add the no-cache directive:

[java]  view plain  copy
  1. connection.addRequestProperty("Cache-Control""no-cache");  

If it is only necessary to force a cached response to be validated by the server, use the more efficient max-age=0 instead:

[java]  view plain  copy
  1. connection.addRequestProperty("Cache-Control""max-age=0");  

Force a Cache Response

Sometimes you'll want to show resources if they are available immediately, but not otherwise. This can be used so your application can show something while waiting for the latest data to be downloaded. To restrict a request to locally-cached resources, add the only-if-cached directive:

1
2
3
4
5
6
7
8
try  {
      connection.addRequestProperty( "Cache-Control" "only-if-cached" );
      InputStream cached = connection.getInputStream();
      // the resource was cached! show it
   catch  (FileNotFoundException e) {
      // the resource was not cached
  }
}

This technique works even better in situations where a stale response is better than no response. To permit stale cached responses, use the max-stale directive with the maximum staleness in seconds:

1
int maxStale = 60 * 60 * 24 * 28;  // tolerate 4-weeks staleconnection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);

以上信息来自:HttpResponseCache - Android SDK | Android Developers

取消一个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
30
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);
     }
}

超时

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final OkHttpClient client;
 
public ConfigureTimeouts() throws Exception {
     client =  new  OkHttpClient();
     client.setConnectTimeout(10, TimeUnit.SECONDS);
     client.setWriteTimeout(10, TimeUnit.SECONDS);
     client.setReadTimeout(30, TimeUnit.SECONDS);
}
 
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);
}

每个call的配置

使用OkHttpClient,所有的HTTP Client配置包括代理设置、超时设置、缓存设置。当你需要为单个call改变配置的时候,clone 一个OkHttpClient。这个api将会返回一个浅拷贝(shallow copy),你可以用来单独自定义。下面的例子中,我们让一个请求是500ms的超时、另一个是3000ms的超时。

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 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  {
       Response response = client.clone()  // Clone to make a customized OkHttp for this request.
           .setReadTimeout(500, TimeUnit.MILLISECONDS)
           .newCall(request)
           .execute();
       System.out.println( "Response 1 succeeded: "  + response);
     catch  (IOException e) {
       System.out.println( "Response 1 failed: "  + e);
     }
 
     try  {
       Response response = client.clone()  // Clone to make a customized OkHttp for this request.
           .setReadTimeout(3000, TimeUnit.MILLISECONDS)
           .newCall(request)
           .execute();
       System.out.println( "Response 2 succeeded: "  + response);
     catch  (IOException e) {
       System.out.println( "Response 2 failed: "  + e);
     }
}

处理验证

这部分和HTTP AUTH有关。
相关资料:HTTP AUTH 那些事 - 王绍全的博客 - 博客频道 - CSDN.NET

OkHttp会自动重试未验证的请求。当响应是401 Not Authorized时,Authenticator会被要求提供证书。Authenticator的实现中需要建立一个新的包含证书的请求。如果没有证书可用,返回null来跳过尝试。

1
2
3
4
5
6
public List<Challenge> challenges()
Returns the authorization challenges appropriate  for  this  response's code. 
If the response code is 401 unauthorized, 
this  returns the  "WWW-Authenticate"  challenges.
If the response code is 407 proxy unauthorized,  this  returns the  "Proxy-Authenticate"  challenges.
Otherwise  this  returns an empty list of challenges.

当需要实现一个Basic challenge, 使用Credentials.basic(username, password)来编码请求头。

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 final OkHttpClient client =  new  OkHttpClient();
 
public void run() throws Exception {
     client.setAuthenticator( new  Authenticator() {
       @Override public Request authenticate(Proxy proxy, Response response) {
         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();
       }
 
       @Override public Request authenticateProxy(Proxy proxy, Response response) {
         return  null // Null indicates no attempt to authenticate.
       }
     });
 
     Request request =  new  Request.Builder()
         .build();
 
     Response response = client.newCall(request).execute();
     if  (!response.isSuccessful())  throw  new  IOException( "Unexpected code "  + response);
 
     System.out.println(response.body().string());
}
我们把同一标的(昆仑万维,现价 43.20 元,2026-07-31 收盘)交给三套系统,各出一份独立分析: **C 报告(CoordClaw 基于管理学多智能体系统)**——投研级。它由五个角色构成:周婷整合撰写、李静出基本面、王芳出技术面、赵明出风险、陈默做 PM 终审。最终产物是一份 38 项分级风险清单(P0×4 / P1×12 / P2×12 / P3×6 / 尾部×4)、双源交叉验证的财务数据(EM/Sina 差异 <0.01%)、严格的口径纪律,以及一份原样保留的"待核实"清单。结论冷冰冰:高风险,不建议参与。 **D 报告(DeepSeek)**——信息整理级。它把"4+3 AGI 战略"、天工 AI、Opera 浏览器、StarMaker 拆得很漂亮,核心财务数据(营收 81.98 亿、归母 -15.93 亿)也没算错。但整篇没有技术面、没有量化风控,更关键的是——它完全没提实控人已减持 75%、质押状态未知、净现金仅 15.19 亿且续航只有 1.26~1.81 年这些要命的负面。这是典型的"选择性呈现"。 **K 报告(Kimi)**——以对比评估的方式呈现。它搭起"数据准确性 / 分析维度 / 结论合理性"的三维框架,把几份材料放在一起对照,给出各自的强弱判定。它的维度意识比 D 报告更自觉,但作为一份独立分析,它对"评估方法本身的信度"交待不足,部分引用的核对也不够彻底。 结果两家的结论高度一致。C 报告(多智能体)被评投研级、居首;D 报告(DeepSeek 自己写的)被评信息整理级、居中;K 报告(Kimi 自己那份)维度较全但核验深度有限,排在两者之间。DeepSeek 的那份评估把 C 给了五星、D 三星、K 四星;Kimi 的那份评估也独立地把最高分给了 C。
问题现象 - 同一张 TF 卡/SD 卡在其他电脑上能正常识别,插到本机却只"响一声",资源管理器里不显示盘符; - 磁盘管理里能看到磁盘,但显示为灰色 / RAW / 无盘符; - 插入 U 盘、读卡器后偶尔能显示,重启或更换卡片后又消失; - 使用 diskpart、mountvol 等命令时系统卡住无响应。 ## 问题根源 经排查,这类问题通常不是 TF 卡损坏,而是 Windows 存储子系统与本机读卡器/驱动之间的兼容性故障,主要包括: 1. **盘符未分配** —— 卷已被系统识别,但没有挂载点,因此资源管理器不显示; 2. **USB 幽灵设备残留** —— 系统里残留 `Disconnected` 状态的 USBSTOR 设备记录,阻塞新设备枚举; 3. **USB 选择性暂停(Selective Suspend)** —— Windows 空闲时给读卡器断电,导致识别不稳定; 4. **automount 被关闭或失效** —— 新插入的卷无法自动分配盘符; 5. **存储堆栈挂起** —— 残留的 diskpart 进程锁死存储服务。 ## 解决方案(一键永久修复) 本项目是一个 **Codex Skill + 一键安装器**,自动完成以下修复: - 启用系统自动挂载(automount enable / scrub) - 清理 Disconnected 幽灵 USB 设备 - 永久禁用 USB 选择性暂停(防读卡器被断电) - 自动为所有无盘符的可移动卷分配盘符 - 注册**开机守护任务**:每次开机自动执行以上修复,彻底告别手动操作 ## 使用方法 ```powershell # 在其他电脑上(Windows 10/11): # 1. 下载本项目 # 2. 右键 install.bat → 以管理员身份运行(或直接双击后点"是")
内容概要:本文档是PCI-SIG发布的工程变更通知(ECN),标题为“DSM Function Revision Clarifications”,发布于2020年2月12日,旨在澄清PCI固件规范3.2版本及后续ECNs中关于ACPI设备特定方法(_DSM)的修订规则。文档明确了_DSM函数中“Revision ID”参数的有效取值范围,规定当前版本的最高修订号为6,并详细说明了当新增或修改函数时,如何统一更新修订值。同时,文档修正了此前不一致的应用方式,确保未来对_DSM接口的扩展具有一致性和向后兼容性,并列出所有已定义_DSM函数的初始与当前有效修订号,涵盖PCI Express插槽信息、电源管理、延迟容忍报告等功能。此外,还描述了操作系统平台(OSPM)与系统固件之间如何协商使用正确的修订版本号。; 适合人群:从事固件开发、系统架构设计、ACPI或PCI Express相关技术工作的工程师,尤其是参与操作系统与硬件交互层开发的技术人员。; 使用场景及目标:①指导开发者正确实现_DSM函数的版本控制机制;②帮助固件和操作系统开发者确保对_DSM接口的支持符合规范一致性要求;③为支持Runtime Device Power Management和Downstream Port Containment等特性的系统提供标准化依据; 阅读建议:此文档属于技术规范类文件,建议结合PCI Firmware Specification 3.2全文及其他相关ECN一起阅读,重点关注Table 4-7及各_DSM函数的参数定义,理解版本协商流程及其对系统行为的影响。
内容概要:本白皮书系统分析了2026年中国体重管理连锁加盟行业的发展现状与未来趋势,以“伊简梅”品牌为深度案例,从政策环境、市场规模、消费趋势、行业格局、商业模式、技术体系、合规能力及盈利模型五个维度展开研究。报告指出,行业正处于监管趋严与需求升级的双重驱动下,迎来从野蛮生长向高质量发展的转型期。伊简梅凭借“中医辨证+六体打造”的核心技术体系、“零品牌授权费+设备权益金”的利益绑定模式,以及完善的八大帮扶体系,在高闭店率的行业中实现了年均闭店率不足5%、成交率93%、复购率48.7%的优异表现,展现出较强的可复制性与投资价值。; 适合人群:有意进入体重管理行业的女性创业者、社区创业者、美业转型者、副业试水者及区域代理商;尤其适合重视合规经营、具备服务意识、追求长期稳定回报的中小投资者。; 使用场景及目标:①帮助创业者全面了解体重管理加盟行业的政策风险、市场机会与核心痛点;②评估伊简梅等系统驱动型品牌的商业模式可行性与投资回报周期;③指导加盟商如何规避合规风险、提升获客能力与门店盈利能力。; 阅读建议:本报告数据详实、逻辑严密,建议结合实地考察与财务测算使用,重点关注品牌直营验证时长、费用透明度、总部赋能落地性等关键指标,理性判断个体适配性,避免盲目投资。
gilisoft usb lock官方版是目前互联网上最优秀的一款usb端口管理软件,也是首款usb端口加密软件,不但可以锁定USB端口,同时支持用户对usb端口设置密码,以及支持网站锁定、程序锁定、设备锁定等功能,可以轻松防止未经许可的通过USB将电脑上的数据复制到USB驱动器、外部驱动器、DVD/CD刻录机或其他可移动设备上。 软件功能 1、阻止USB / SD驱动器 禁止从USB / SD磁盘读取,禁止写入USB / SD磁盘,阻止非系统分区。它不允许任何类型的USB / SD驱动器访问您的计算机,除非您对其进行了授权或它已在受信任的设备白名单中。 2、CD锁,阻止媒体和蓝光光盘 禁用从DVD / CD光盘读取或将DVD / CD刻录机设为只读。此应用程序还阻止使用磁盘集线器,托架,组合或CD / DVD驱动器的所有光盘,并分配驱动器号。 3、受信任的设备白名单 您可以创建白名单以允许“某些批准的” USB笔式驱动器。然后,它将阻止除白名单中的所有USB驱动器。 4、报告和日志 USB Lock提供完整的报告和日志: (1)USB活动-监视连接到计算机的所有USB磁盘上的所有文件操作(例如创建删除文件)。 (2)拒绝并允许访问历史记录。 (3)活动白名单。 5、网站锁定 禁止访问某些网站。此实用程序使您可以阻止不需要的网站在Internet Explorer中显示。如果网站被阻止,则用户将被转到空白页面或“被阻止的页面”,并且原始页面的内容未加载到您的PC上。 6、设备锁 该程序可用于限制对可移动媒体设备(例如CD,DVD,软盘,SD卡读取器,闪存和USB驱动器)的读写访问。它还可以用于禁用iPhone,Android手机,打印机,调制解调器,COM端口,红外,蓝牙,1394端口。 7、程序锁 阻止运行任何程序,包括IE,Outlo
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值