阅读 192

OkHttp梳理

使用

  • 默认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:

    • 包装成AsyncCall(--> Runnable)

    • 线程池中执行

    • asyncCall.run --> asyncCall.execute中调用getResponseWithInterceptorChain

    • 直接调用getResponseWithInterceptorChain

    • call.enqueue/execute:进行分发、处理listener等

    • execute()

    • enqueue(callback)

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


作者:Varmin_
链接:https://juejin.cn/post/7037782267485224996

文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐