计算机技术实战

纸上得来终觉浅,绝知此事要躬行。

Download this project as a .zip file Download this project as a tar.gz file

OkHttp详解

目录

拦截器机制

wiki:Interceptors一张图说明了OkHttp的拦截器机制:

RealCall.java

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

拦截器按顺序如下:

  1. OkHttpClient.Builder.addInterceptor()方法定义的拦截器;
  2. 重试拦截器RetryAndFollowUpInterceptor
  3. BridgeInterceptor
  4. CacheInterceptor
  5. ConnectInterceptor
  6. OkHttpClient.Builder.addNetworkInterceptor()方法定义的拦截器;
  7. CallServerInterceptor

Http状态码

成功:2xx

重定向:3xx

Client错误:4xx

Server错误:5xx

重试机制

408重试

情景:服务器等待客户端请求超时,状态码408(Request Timeout): 如果服务端返回的Header:"Retry-After", "N",N>0,则表示服务器希望客户端在N秒之后再重试,此时客户端放弃;如果没有Retry-After头,或者值为0,则客户端发起重试;如果重试仍然出现408,放弃。

503重试

重试机制和408类似,规律是:重试的状态码和初始请求的错误码一致,放弃重试,例如服务器第一次和第二次均返回503,那么客户端不会进行第三次请求。但是如果服务器返回的状态码依次是408,503,408,503依次交替,那么客户端最多会重试20次,之后便抛出异常。

RetryAndFollowUpInterceptor.java

public final class RetryAndFollowUpInterceptor implements Interceptor {
  /**
   * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
   * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
   */
  private static final int MAX_FOLLOW_UPS = 20;

  @Override public Response intercept(Chain chain) throws IOException {
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release(true);
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
  }
}

3xx重试

OkHttp的300-303都是同样的重试逻辑:当服务端返回的状态码为其中一个,那么解析Response的Header,找到Location字段。若无此字段,放弃,如果有,读取其值;使用这个值构建请求发起重试。

BridgeInterceptor

gzip相关逻辑在这里处理:

post参数也在这里处理,例如构建如下请求

    public static final MediaType JSON
            = MediaType.get("application/json; charset=utf-8");

    RequestBody body = RequestBody.create(JSON, "{\"a\":\"a\"}");
    Request request = new Request.Builder()
            .url(url)
            .post(body)
            .build();

BridgeInterceptor会加上如下Header:

Content-Type: application/json; charset=utf-8
Content-Length: 9

另外,如果用户没有设置Host、Connection、User-Agent这两个请求头,BridgeInterceptor也会补充默认的:

Host: localhost:51857
Connection: Keep-Alive
User-Agent: okhttp/3.13.1

CacheInterceptor

缓存机制由CacheInterceptor来实现。

ConnectInterceptor

ConnectInterceptor的主要目的是找到可用的socket连接。连接池的复用逻辑在这里处理,用到的是http/1.1 keep-alive=true的属性,tcp连接保活。连接(RealConnection)可复用的依据:

  1. 连接池不为空;
  2. 连接池空闲,即此连接的上一次客户端-服务器请求应答完毕;
  3. 请求地址的主机名host相等;

连接池的保活时间,默认5分钟:

  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

可自定义:

OkHttpClient.Builder().connectionPool(new ConnectionPool(5, 15, TimeUnit.SECONDS));

CallServerInterceptor

CallServerInterceptor负责向输出流写入request,并从输入流读取response。

写入request:

  1. 写入"GET / HTTP/1.1”;
  2. 写入空行("\n\r")
  3. 遍历请求头,按格式"header.name: header.value"依次写入,每对Header换行;
  4. 换行;
  5. 写入requestBody,长度等于请求头"Content-Length"的长度。

读response:

  1. 读取状态行,如"HTTP/1.1 200 OK";
  2. 逐行读取Header,以":"为分隔符解析成key: value的形式;如"Content-Length: 19";
  3. 从返回头Content-Length字段读取返回body的长度:例如上面的19;
  4. 如果返回头"Connection: close”,则关闭连接,否则保持连接(keep-alive);
  5. 构造Response对象并返回。
  6. 通过Response.body().string()真正的把流读完。

Call.cancel

一个进行中的请求(RealCall),调用cancel()会关闭socket:

  public static void closeQuietly(Socket socket) {
    if (socket != null) {
      try {
        socket.close();
      } catch (AssertionError e) {
        if (!isAndroidGetsocknameError(e)) throw e;
      } catch (RuntimeException rethrown) {
        throw rethrown;
      } catch (Exception ignored) {
      }
    }
  }

各种Post请求的处理

Post String

如果格式服务器不支持,则服务器将返回415 Unsupported Media Type。

    public static final MediaType TEXT
            = MediaType.get("text/plain; charset=utf-8");
    RequestBody body = RequestBody.create(TEXT, "hello I am a string");
    Request request = new Request.Builder()
            .url(getUrl())
            .post(body)
            .build();
    Response response = client.newCall(request).execute();

请求头:

    Content-Type: text/plain; charset=utf-8
    Content-Length: 19

请求body为"hello I am a string"

Post Form

    private void doPostForm() {
        RequestBody formBody = new FormBody.Builder()
                .add("search", "Android")
                .add("from", "mwp")
                .build();
        Request request = new Request.Builder()
                .url("https://zh.wikipedia.org/w/index.php")
                .post(formBody)
                .build();

        try {
            Response response = client.newCall(request).execute();
            Log.d(TAG, "doPostForm: " + response.body().string());
        } catch (IOException e) {
            Log.e(TAG, "doPostForm: ", e);
        }
    }

请求头:

    Content-Type: application/x-www-form-urlencoded
    Content-Length: 23

请求body为"search=Android&from=mwp"。

Post File

    public static final MediaType MEDIA_TYPE_MARKDOWN
            = MediaType.get("text/x-markdown; charset=utf-8");
    File file = new File(Environment.getExternalStorageDirectory() + "/Download/test.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();

请求头

    Content-Type: text/x-markdown; charset=utf-8
    Content-Length: 9

Post Multipart

    File f = new File(Environment.getExternalStorageDirectory() + "/Download/test.md");
            Log.d(TAG, "doPostMultipart: File:" + f.length());
    RequestBody requestBody = new MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("title", "test title")
            .addFormDataPart("content", "file_name",
                    RequestBody.create(TEXT, f))
            .build();

    Request request = new Request.Builder()
            .url(getUrl())
            .post(requestBody)
            .build();
    Response response = client.newCall(request).execute();

请求头:

    Content-Type: multipart/form-data; boundary=daa37881-9c17-4e83-99c5-08cdd5627d24
    Content-Length: 345