OkHttp梳理

2,414 阅读10分钟

本文为过往笔记整理, 在此只作记录,不做严谨的技术分享。

OkHttp使用详解
OkHttp使用完全教程
深入浅出安卓热门网络框架 OkHttp3 和 Retrofit 原理
OkHttp3 源码解析 连接池的复用
OkHttp框架源码解析 - 多路复用
OKHTTP3源码和设计模式(上篇)
OKHTTP3源码和设计模式(下篇)

使用

  • 默认GET、request.method()配置其它类型
//GET
OkHttpClient client = new OkHttpClient.Builder()
        .build();
Request request = new Request.Builder()
        .url(Api.COMMON_LIST)
        .build();
Call call = client.newCall(request);
call.enqueue(new Callback() {});


//POST
OkHttpClient client = new OkHttpClient.Builder()
        .build();

FormBody formBody = new FormBody.Builder()
        .add("username", "10022000000")
        .add("password", "111111")
        .build();
Request request = new Request.Builder()
        .post(formBody)
        .url(Api.SIGN_IN)
        .build();
Call call = client.newCall(request);
call.enqueue(new Callback() {});

源码

不用特意记它的流程,这些看源码都能找到的

  • OKHttpClient:配置:拦截器、代理、cookie...
  • Request:配置:url、method、headers...
  • RealCall:
    • call.enqueue/execute:进行分发、处理listener等
    • execute()
      • 直接调用getResponseWithInterceptorChain
    • enqueue(callback)
      • 包装成AsyncCall(--> Runnable)
      • 线程池中执行
      • asyncCall.run --> asyncCall.execute中调用getResponseWithInterceptorChain

execute

//call.execute
<!--#RealCall-->
public Response execute() throws IOException {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  transmitter.timeoutEnter();
  transmitter.callStart();
  try {
    client.dispatcher().executed(this); //--> Dispatcher#runningSyncCalls.add(call);
    //call并不会在拦截器中执行,是用来计数、判断等操作的
    return getResponseWithInterceptorChain();
  } finally {
    client.dispatcher().finished(this);// 从runningSyncCalls中移除
  }
}

enqueue

  • 异步时会有Dispatcher# maxRequests = 64; maxRequestsPerHost = 5限制
//call.enqueue
<!--#RealCall-->
 public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    transmitter.callStart();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

<!--#Dispatcher-->
void enqueue(AsyncCall call) {
   synchronized (this) {
     readyAsyncCalls.add(call);
     ...
   }
   promoteAndExecute(); //--> runningAsyncCalls.add(asyncCall);
}

private boolean promoteAndExecute() {
  assert (!Thread.holdsLock(this));
  List<AsyncCall> executableCalls = new ArrayList<>();
  boolean isRunning;
  synchronized (this) {
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall asyncCall = i.next();
      if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
      if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
      i.remove();
      asyncCall.callsPerHost().incrementAndGet();
      executableCalls.add(asyncCall);
      runningAsyncCalls.add(asyncCall);
    }
    isRunning = runningCallsCount() > 0;
  }
  for (int i = 0, size = executableCalls.size(); i < size; i++) {
    AsyncCall asyncCall = executableCalls.get(i);
    asyncCall.executeOn(executorService()/**线程池*/);
  }
  return isRunning;
}

<!--#RealCall.AsyncCall-->
final class AsyncCall extends NamedRunnable {
  void executeOn(ExecutorService executorService) {
    try {
      //放入线程池,子线程执行run方法调用下面的execute
      executorService.execute(this);
    } catch (RejectedExecutionException e) {
      responseCallback.onFailure(RealCall.this, ioException);
    } finally {
      client.dispatcher().finished(this); //从runningAsyncCalls中移除
    }
  }

  protected void execute() {
    try {
      Response response = getResponseWithInterceptorChain();
      responseCallback.onResponse(RealCall.this, response);
    } catch (IOException e) {
      responseCallback.onFailure(RealCall.this, e);
    } finally {
      client.dispatcher().finished(this);
    }
  }
}

拦截器

image

七种拦截器:

  1. addInterceptor(Interceptor)应用拦截器,开发者设置,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加。
  2. RetryAndFollowUpInterceptor,对连接做一些初始化工作,以及请求失败的重试工作,重定向的后续请求工作。
  3. BridgeInterceptor,为用户构建一个能够进行网络访问的请求(比如添加content-type、content-length、User-Agent、Host、Cookie、gzip等),同时后续工作将网络请求回来的响应转化为用户可用的Response,以及使用Cookie存取。
  4. CacheInterceptor,处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
  5. ConnectInterceptor,负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCode。
  6. networkInterceptors网络拦截器,开发者设置,所以本质上和第一个拦截器差不多,但是由于位置不同,用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。
  7. CallServerInterceptor,进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。

应重桥缓连网请

  Response getResponseWithInterceptorChain() throws IOException {
    List<Interceptor> interceptors = new ArrayList<>();
    //自定义:应用程序拦截器
    interceptors.addAll(client.interceptors());
    
    //重连重定向拦截器
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    //桥接拦截器
    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, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
    try {
      Response response = chain.proceed(originalRequest);
      return response;
    } catch (IOException e) {
    } finally {
    }
  }

RetryAndFollowUpInterceptor

OkHttp原始解析(二)重定向

重连重定向拦截器:对连接做一些初始化工作,以及请求失败的重试工作,重定向的后续请求工作。

public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Transmitter transmitter = realChain.transmitter();

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      transmitter.prepareToConnect(request);
      // 处理取消事件
      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // 判断是否满足重定向条件,满足则重试,不满足就抛异常
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // 与服务器连接失败,检查是否满足重定向条件,满足则重试,不满足就抛异常
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // 检测到其他未知异常,则释放连接和资源
        // The network call threw an exception. Release any resources.
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      // 绑定上一个Response,指定body为空
      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Exchange exchange = Internal.instance.exchange(response);
      Route route = exchange != null ? exchange.connection().route() : null;
      // 根据响应码处理请求,返回Request不为空时则进行重定向处理
      Request followUp = followUpRequest(response, route);

      if (followUp == null) {
        if (exchange != null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }

      RequestBody followUpBody = followUp.body();
      if (followUpBody != null && followUpBody.isOneShot()) {
        return response;
      }

      closeQuietly(response.body());
      if (transmitter.hasExchange()) {
        exchange.detachWithViolence();
      }
        
      // 限制重定向次数最多为20,超过则抛出异常
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;
      priorResponse = response;
    }
  }
1.1 重试
  private boolean recover(IOException e, Transmitter transmitter,
      boolean requestSendStarted, Request userRequest) {
    // The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure()) return false;

    // We can't send the request body again.
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

    // This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false;

    // No more routes to attempt.
    if (!transmitter.canRetry()) return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
  }
1.2 资源重定向

客户端向服务器发送一个请求,获取对应的资源,服务器收到请求后,发现请求的这个资源实际放在另一个位置,于是服务器在返回的响应头的Location字段中写入那个请求资源的正确的URL,并设置reponse的状态码为30x 。

 private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      // 407 
      case HTTP_PROXY_AUTH:
        // 代理认证 
        Proxy selectedProxy = route != null
            ? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);
      // 401
      case HTTP_UNAUTHORIZED:
        // 身份认证 
        return client.authenticator().authenticate(route, userResponse);
      // 308 
      case HTTP_PERM_REDIRECT:
      // 307 
      case HTTP_TEMP_REDIRECT:
        // 如果是307、308 这两种状态码 
        // 不对GET、HEAD 以外的请求重定向
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // fall-through
      // 300 
      case HTTP_MULT_CHOICE:	
      // 301
      case HTTP_MOVED_PERM:
      // 302
      case HTTP_MOVED_TEMP:
      // 303
      case HTTP_SEE_OTHER:
        // 客户端关闭重定向
        if (!client.followRedirects()) return null;
        // 从响应头中获取Location字段 
        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        // If configured, don't follow redirects between SSL and non-SSL.
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // 重建一个新的Request
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // When redirecting across hosts, drop all authentication headers. This
        // is potentially annoying to the application layer since they have no
        // way to retain them.
        if (!sameConnection(userResponse.request().url(), url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();
      // 408
      case HTTP_CLIENT_TIMEOUT:
      // 需要发送一次相同的请求
      // ...... 
      // 503
      case HTTP_UNAVAILABLE:
        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          // specifically received an instruction to retry without delay
          return userResponse.request();
        }

        return null;

      default:
        return null;
    }
  }

当重定向时,如果想要根据某个状态码或重定向地址,自定义操作,可以自定义拦截器:

  • 路径重定向
  • http --> https重定向

Okhttp配置:禁止重定向或自定义重定向拦截器时使用

new OkHttpClient().newBuilder()
.followRedirects(false)  //禁制OkHttp的重定向操作,我们自己处理重定向
.followSslRedirects(false)//https的重定向也可处理

自定义拦截器:

public class RedirectInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        okhttp3.Request request = chain.request();
        Response response = chain.proceed(request);
        int code = response.code();
        if (code == 307) {
            //获取重定向的地址
            String location = response.headers().get("Location");
            LogUtils.e("重定向地址:", "location = " + location);
            //重新构建请求
            Request newRequest = request.newBuilder().url(location).build();
            response = chain.proceed(newRequest);
        }
        return response;
    }
}

BridgeInterceptor

桥拦截器:为用户构建一个能够进行网络访问的请求(比如添加content-type、content-length、User-Agent、Host、Cookie、gzip等),同时后续工作将网络请求回来的响应转化为用户可用的Response,以及使用Cookie存取。

  • cookieJar默认使用CookieJar.NO_COOKIES,所以OKHttp默认不支持cookie
public final class BridgeInterceptor implements Interceptor {

 public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }
    
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    // cookieJar默认使用CookieJar.NO_COOKIES,所以OKHttp默认不支持cookie
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      //设置到请求头中
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    Response networkResponse = chain.proceed(requestBuilder.build());
	//保存cookie
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }
}
使用CookieJar

OkHttp3入门介绍之Cookie持久化

链接关闭后,服务端不会记录用户的信息,Cookie 的作用就是用于解决 "如何记录客户端的用户信息";

  • 键值对:username=John Doe
  • 请求时:自动携带,供服务器识别
  • 返回时:更新cookie信息;
public interface CookieJar {
  /** 内部默认实现,不做任何操作. */
  CookieJar NO_COOKIES = new CookieJar() {
    //返回结果时,缓存或者持久化cookie  
    @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
    }

    //请求时,获取对应的cookie
    @Override public List<Cookie> loadForRequest(HttpUrl url) {
      return Collections.emptyList();
    }
  };
}
Cookie本地化读写

实现CookieJar接口,设置到OkHttpClientBuilder。 Cookie存取方式:

  • 本地存储:SP、文件
  • 内存:map

CacheInterceptor

缓存拦截器:处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。

缓存机制

客户端和服务端配合,请求和返回中携带各种参数来判断缓存策略;

HTTP缓存机制及原理
不一样的HTTP缓存体验

  • 客户端、服务器配合,设置参数Expires、Cache-Control、max-Age等参数;
  • 不属于okhttp上的功能,只是通过http协议和DiskLruCache做了处理而已;
强制缓存
  • 有缓存、有效-->用缓存
  • 没有缓存、失效--> 请求新数据--> 数据放入本地
对比缓存

缓存标识-->服务器返回是否有效

  • 有效--> 本地缓存

  • 无效--> 返回新数据--> 数据放入本地

  • 上传、下载

  • 异步:onResponse、onFailed是在子线程工作的

  • 如何判断是否联网、WIFI、4G、网速?

  • 现在的Android源码网络实现,用的是OKhttp

  • maxRequest:64(最多64个请求)、maxRequestPerHost:5(每个主机最多5个),多的进队列等待。

  • 启用Gzip

ConnectInterceptor

负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCode。

CallServerInterceptor

进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。

两种自定义拦截器区别

img
  • applications:
    • 最开始调用:关注最原始的请求数据
    • 不会因为重定向、缓存等原因不执行
    • 只会被调用一次,即使这个响应是从缓存中获取的
  • network:
    • 已经建立链接,可以观察到真实的请求和响应数据
    • 返回缓存时不被调用

自定义拦截器

  • 请求
    • 固定参数封装
    • 请求数据加密
    • 测试/正式url切换
    • header动态添加
  • 返回数据
    • 返回数据读取、封装
    • log日志打印
    public class TestInterceptor implements Interceptor {
        private final Charset UTF8 = Charset.forName("UTF-8");

        @Override
        public Response intercept(Chain chain) throws IOException {
            Request originalRequest = chain.request();
            /**
             * pre:对参数进行封装:校验参数、添加公有参数、打印参数url等信息...
             */
            HttpUrl httpUrl = originalRequest.url()
                .newBuilder()
                .addQueryParameter("key","varmin")
                .build();
            Request request = originalRequest.newBuilder().url(httpUrl).build();

            for (String queryParameterName : request.url().queryParameterNames()) {
                Log.d(TAG, "intercept: paramName = " + queryParameterName);
            }

            Response response = chain.proceed(request);

            /**
             * after:对返回内容:长度、格式、打印等控制
             */
            ResponseBody responseBody = response.body();
            BufferedSource source = responseBody.source();
            source.request(Long.MAX_VALUE);
            Buffer buffer = source.getBuffer();
            Charset charset = UTF8;
            MediaType contentType = responseBody.contentType();
            if (contentType != null) {
                charset = contentType.charset(UTF8);
            }
            //clone:不会引起callback中responseBody的close
            Log.d(TAG, "intercept: response" + buffer.clone().readString(charset));
            return response;
        }
        
    }

OKio

Okio源码解析

Tips

response.body().string() 只能调用一次?

  • 多次调用:java.lang.IllegalStateException: closed
  • 使用buffer.clone方法获取内容
<!--再次读取是报错-->
public long read(Buffer sink, long byteCount) throws IOException {
    //...
    if (closed) throw new IllegalStateException("closed");
    return buffer.read(sink, toRead);
}
<!--读取以后close释放资源-->
@Override
public void close() throws IOException {
  if (closed) return;
  closed = true;
  source.close();
  buffer.clear();
}



<!--使用buffer.clone方法获取内容-->
BufferedSource source = response.body().source();
Buffer buffer = source.getBuffer();

Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
    charset = contentType.charset(UTF8);
}

//clone:不会引起callback中responseBody的close
Log.d(TAG, "intercept: response" +buffer.clone().readString(charset));