OKHttp实现原理分享

前言介绍

        大约在2年半之前,就想写一篇关于OKHttp原理的文章,一来深入了解一下其原理,二来希望能在了解原理之后进行更好的使用。但是因为种种原因,一直无限往后推迟,最近因为我们情景智能半个月一次的分享轮到我了,所以有了压力之后,抽出了一整天的时间完成了这篇文章。之所以每半个月搞一次技术分享,其目标之一也是为了督促分享人能够在压力之后,达成设定的目标,所以技术分享最大的收益其实恰恰就是分享者自身。

        另外这里也顺便给大家带来一些我对AI工具使用的心得。完成这次分享的时候,充分借助了AI工具。完成简单工作时,AI工具确实能够提供很大的帮助,甚至直接帮助我完成,但是在完成一些具有一定深度的任务时,AI工具给的结果不是很理想往往给的不准确或者就是错误,这也正常,因为具有深度的问题被搜索的频次较少,样本量较低,则准确性就不高。

        言归正传,回到OKHttp,OKHttp是一个开源的HTTP客户端库,由Square公司开发,广泛应用于Java和Kotlin应用程序中进行网络请求和处理响应。目前OKHttp已成为安卓端最为主流通信的框架,之前存在的一些自带的框架如HttpClient已经逐渐从源码中都已废弃,那么为什么都在使用OKHttp,OKHttp有哪些优势,我们一起来看一下。

一.基本用法

1.1 构建OKHttpClient

通过三步流程进行进行创建,使用创建者模式。

首先创建Builder;

然后对Builder配置一些参数;

最后通过build生成OKHttpClient对象。

Java
val builder = OkHttpClient.Builder()
builder.cache(Cache(File(context?.filesDir?.absolutePath + File.separator + "ok"), 100))
client = builder.build()

1.2 构建Request

通过三步流程进行创建,使用创建者模式。

首先创建Builder对象;

然后对Builder配置一些参数;

最后通过build生成Request对象。

Java
val builder = Request.Builder()
val cacheBuilder = CacheControl.Builder()
cacheBuilder.noStore()
builder.cacheControl(cacheBuilder.build())
val request = builder.url("https://www.baidu.com").get().build()

1.3 构建Call

通过client和request构造生成Call。

Java
val newCall = client.newCall(request)

1.4 同步发送请求

直接使用call.execute()方法发送请求,execute方法阻塞线程。

Java
val response = newCall.execute()

1.5 异步发送请求

直接使用call.enqueue()方法发送请求,enqueue方法不阻塞线程。

Java
newCall.enqueue(object : Callback {
    override fun onFailure(call: okhttp3.Call, e: IOException) {
    }

    override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
    }
})

1.6 解析Reponse

Java
val content = IOHelper.readStrByCode(response.body()?.byteStream(), "utf-8")

1.7 支持状态监听

Java
//OKHttpClient.Builder
public Builder eventListener(EventListener eventListener) {
  if (eventListener == null) throw new NullPointerException("eventListener == null");
  this.eventListenerFactory = EventListener.factory(eventListener);
  return this;
}
//EventListener
public abstract class EventListener {
    public void callStart(Call call) {}
    public void dnsStart(Call call, String domainName) {}
    ...
}

 

二.请求流程

正常网络请求流程

OKHttp请求流程

1.构建RealCall;

Java
val newCall = client.newCall(request)

2.构建责任链

Java
Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    //2.
构建5+2层拦截器;
    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());
   
    //3.开始一层层处理拦截器流程;
    Response response =  chain.proceed(originalRequest);
    //4.返回结果。
    return response;
}

.5+2层拦截器

3.1 责任链

调用一层层的往下层递归嵌套调用,结果一层层的向上层返回。

3.2 RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor是第一个拦截器,主要作用就是两个:重试和重定向。

需要说明的是,这里的重试并不是通常意义上的只要失败了无论怎么样都会再试一次的那个重试,而是满足特定条件下因为某种小的错误才会尝试的重试。

重试逻辑中,如果下层返回异常,如3.2.1代码和3.2.2代码,则进入到Exception的逻辑。

该逻辑中,会通过recover方法进行判断是否需要重试,如果需要则继续循环,否则如3.2.3代码返回reponse结束流程。

Java
@Override
public Response intercept(Chain chain) throws IOException {
   
    while (true) {
        try {
         
response = realChain.proceed(request, streamAllocation, null, null);
          releaseConnection = false;
        } catch( RouteException e ) {
            //3.2.1
代码
            if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
              throw e.getLastConnectException();
            }
            releaseConnection = false;
            continue;
        } catch( IOException e ) {
            //3.2.2代码
            boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
            if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
            releaseConnection = false;
            continue;
        } finally {
            ...
        }
        Request followUp = followUpRequest(response, streamAllocation.route());
        if (followUp == null) {
          if (!forWebSocket) {
            streamAllocation.release();
          }
          //3.2.3代码
          return response;
        }
        ...
        if (++followUpCount > MAX_FOLLOW_UPS) {
            streamAllocation.release();
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
        }
        ...
    }
}

private boolean recover(IOException e, StreamAllocation streamAllocation,
    boolean requestSendStarted, Request userRequest) {
  streamAllocation.streamFailed(e);

  // The application layer has forbidden retries.
  if (!client.retryOnConnectionFailure()) return false;

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

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

  // No more routes to attempt.
  if (!streamAllocation.hasMoreRoutes()) return false;

  // For failure recovery, use the same route selector with a new connection.
  return true;
}

重试逻辑中

判断是否需要重试的逻辑在recover方法中,主要分为4种。

应用层禁止重试,不能再次请求,发生致命的异常,没有路由可以尝试。

重定向逻辑中

会尝试最多进行20次的重定向,超过20次则认为失败。

3.3 BridgeInterceptor

桥接拦截器的的主要作用有三个:

  • 首先把一个用户请求转换为网络请求;
  • 其次调用网络请求;
  • 最后把网络响应转换为用户的响应。

则顾名思义,header中的各种基础信息,如gzip、keep-alive、text/html等等,进行内容的组装。

接下来看代码

Java
public Response intercept(Chain chain) throws IOException {
    RequestBody body = userRequest.body();
    //
拼装请求的基础信息
    if (body != null) {
        MediaType contentType = body.contentType();
        if (contentType != null) {
          requestBuilder.header("Content-Type", contentType.toString());
        }
        ...
    }
    ...
    //拼装Cookie
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
    ...
    //责任链传递
   
Response networkResponse = chain.proceed(requestBuilder.build());
    //解析响应的基础信息
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
    //gzip解压
    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();
}

3.4 CacheInterceptor

CacheInterceptor的主要作用,就是对数据进行缓存以及确认是否使用缓存。

执行流程:

主要分为3块:

  • 根据缓存策略确定是否使用;
  • 如果不使用缓存,则发送请求;
  • 更新缓存;

Java
@Override
public Response intercept(Chain chain) throws IOException {
    //
找到缓存
    Response cacheCandidate = cache != null ? cache.get(chain.request()) : null;
    long now = System.currentTimeMillis();
   
    //构建缓存策略,通过缓存策略决定是否使用此次的缓存Reponse
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
   
    //如果使用缓存,则会把networkRequest设置为空,则返回CacheResponse。
    if (networkRequest == null) {
        return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();
    }
   
networkResponse = chain.proceed(networkRequest);
   
    //
更新缓存Response
    if (cacheResponse != null) {
        ...
        cache.update(cacheResponse, response);
        return response;
    }
    //添加新的缓存,或者根据请求的配置删除缓存。
    Response response = networkResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();
    if (cache != null) {
        if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
          // Offer this request to the cache.
          CacheRequest cacheRequest = cache.put(response);
          return cacheWritingResponse(cacheRequest, response);
        }
        if (HttpMethod.invalidatesCache(networkRequest.method())) {
            cache.remove(networkRequest);
        }
     }
     return response;
}

缓存和缓存策略:

简单看一下缓存和缓存策略。

使用的是LruCache,key为url。

Java
public final class Cache implements Closeable, Flushable {
    final DiskLruCache cache;
   
    Cache(File directory, long maxSize, FileSystem fileSystem) {
        ...
        this.cache = DiskLruCache.create(fileSystem, directory, 201105, 2, maxSize);
    }
   
    Response get(Request request) {
        String key = key(request.url());
        DiskLruCache.Snapshot snapshot;
       
        snapshot = this.cache.get(key);
        Entry entry = new Entry(snapshot.getSource(0));
       
        Response response = entry.response(snapshot);
        return response;
    }
}

3.5 ConnectInterceptor

ConnectInterceptor的主要作用就是创建连接。

请求流程:

Java
@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    //
创建协调类
    StreamAllocation streamAllocation = realChain.streamAllocation();
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //创建,通过域名DNS解析,获取到IP地址和端口,创建连接对象HttpCodec。
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    //这里的connection就是上面创建好的
    RealConnection connection = streamAllocation.connection();
    //交给下一个拦截器处理
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

  • 创建协调类StreamAllocation,它负责协调Collections、Stream、Calls的关系。
  • 创建HttpCodec对象,这个对象是经过DNS解析后,直接存储IP地址和端口的类,分为HTTP1.1和HTTP2.0两个版本。下面是我断点获取到的访问baidu的信息

Java
Connection{www.baidu.com:443, proxy=DIRECT hostAddress=www.baidu.com/180.101.50.242:443 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 protocol=http/1.1}

  • 尝试进行连接。
  • 交给下一个拦截器。

我们主要看一下newStream中的流程。这里的核心就是获取一个有效的连接,核心代码在StreamAllocation的findConnection方法中。

主要有三部分,首先尝试从连接池中查找,如果找不到则通过路由生成一个新的连接,最后更新连接到连接池。

Java
private RealConnection findConnection() throws IOException {
    if (result == null) {
        //
获取连接之前,先查询连接缓存池
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
            foundPooledConnection = true;
            result = connection;
        } else {
            selectedRoute = route;
        }
    }
    ...
    //创建新的连接
    if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }
    }
    //上面找到了连接,则添加到缓存池
    Internal.instance.put(connectionPool, result);
}

缓存池获取连接

Java
//获取连接
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    for (RealConnection connection : connections) {
        if (connection.isEligible(address, route)) {
          //确定使用直接把connection指向streamAllocation,其实这里返回值并没有用到。
          streamAllocation.acquire(connection, true);
          return connection;
        }
    }
    return null;
}

//合法性判断
public boolean isEligible(Address address, @Nullable Route route) {
    ...
    //基础类,判断是否正在被使用
    if (allocations.size() >= allocationLimit || noNewStreams) return false;
    //判断地址是否匹配
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
    //判断URL是否匹配,如果完全匹配则直接返回。
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }
   
    // 必须支持HTTP/2
    if (http2Connection == null) return false;
   
    // 路由必须共享一个IP地址,不支持代理连接
    if (route == null) return false;
    if (route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (!this.route.socketAddress().equals(route.socketAddress())) return false;
   
    // 连接的证书支持新的域名
    if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
   
    ...
   
    return true;
}

 

开始连接

首先通过DNS获取地址

然后通过路由创建连接

Java
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    ...
    if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }
        route = selectedRoute;
        refusedStreamCount = 0;
        //
创建连接
        result = new RealConnection(connectionPool, selectedRoute);
        acquire(result, false);       
    }
    //进行握手
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());
}   

添加连接到连接池

Java
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    ...
    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;
   
      // Pool the connection.
      Internal.instance.put(connectionPool, result);
      ...
    }
    ...
    eventListener.connectionAcquired(call, result);
    return result;
}

3.6 CallServerInterceptor

到了这一步,连接已经创建好了。所以CallServerInterceptor的作用就是发送最终的请求到后台。

主要分为几步:

写入header;

写入requestBody;

发送请求流;

读取ResponseHeader;

读取ResponseBody。

Java
@Override public Response intercept(Chain chain) throws IOException {
    //
材料准备齐全,开始干活了。
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();   
    //写入header
    httpCodec.writeRequestHeaders(request);
   
    //写入requestBody
    long contentLength = request.body().contentLength();
    CountingSink requestBodyOut =
        new CountingSink(httpCodec.createRequestBody(request, contentLength));
    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
    request.body().writeTo(bufferedRequestBody);
    bufferedRequestBody.close();
   
    //IO流发送
    httpCodec.finishRequest();
   
    //解析ReponseHeader
    responseBuilder = httpCodec.readResponseHeaders(false);
   
    //解析ReponseBody
    if (forWebSocket && code == 101) {
   
    } else {
        response = response.newBuilder().body(httpCodec.openResponseBody(response)).build();
    }
    return response;
}

写入header的逻辑。

Java
@Override public void writeRequestHeaders(Request request) throws IOException {
  String requestLine = RequestLine.get(
      request, streamAllocation.connection().route().proxy().type());
  writeRequest(request.headers(), requestLine);
}

public void writeRequest(Headers headers, String requestLine) throws IOException {
  if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
  sink.writeUtf8(requestLine).writeUtf8("\r\n");
  for (int i = 0, size =
headers.size(); i < size; i++) {
    sink.writeUtf8(headers.name(i))
        .writeUtf8(": ")
        .writeUtf8(headers.value(i))
        .writeUtf8("\r\n");
  }
  sink.writeUtf8("\r\n");
  state = STATE_OPEN_REQUEST_BODY;
}

读取header的逻辑。

Java
//Http1Codec
public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    //
这里阻塞,等到有响应了才继续往下走。
    StatusLine statusLine = StatusLine.parse(readHeaderLine());
    Response.Builder responseBuilder = new Response.Builder().protocol(statusLine.protocol).code(statusLine.code).message(statusLine.message).headers(readHeaders());
    ...
    return responseBuilder;  
}

3.7 自定义外层拦截器

举2个例子典型的例子:

例子1:内容转换为对象

客户端和服务端进行通信,如果采用的是Protobuff或者其它的数据类型,那么把返回的二进制流,转换为我们想要的对象类型,就可以在外层拦截器执行。

例子2:MOCK数据

我们现在和后台一般是并行开发,并行开发会存在一个问题,客户端前期开发时没有后台接口可用。这时候我们往往会选择mock数据,但是mock的数据是写在逻辑层的,侵入性较高。但是如果我们把mock的逻辑放在外层拦截器中,就会方便很多。首先,逻辑层完全不需要改动;其次拦截器可以进行配置,生产缓存不是使用该拦截器;最后,拦截器还可以设置读取磁盘上的文件甚至本地后台服务,方便内容修改。

还可以有以下作用:

  • 修改请求:可以在请求发送之前修改请求的URL头信息或请求体。
  • 处理响应:可以在响应接收之后修改响应的头信息或响应体。
  • 重试请求:可以在请求失败时实现重试逻辑。
  • 日志记录:可以记录请求和响应的详细信息,用于调试和监控。

3.8 自定义连接拦截器

  • 网络层拦截:networkInterceptors在网络请求和响应的过程中拦截,而普通的Interceptor可以在缓存、重定向等过程中拦截。
  • 访问原始数据:networkInterceptors可以访问网络请求和响应的原始数据,包括未解码的响应体。
  • 顺序执行:networkInterceptors必须按顺序执行,并且必须调用Chain.proceed()方法继续请求或响应的处理。

3.9 小结

通过一系列的拦截器,完成不同的任务,从而实现一个完整的请求。

.线程设计

4.1 请求任务线程池

OKHttp任务调度,使用的线程池的设计。

添加任务:

相关代码如下:

Java
//如果未超过最大请求数,则直接执行,否则加入等待队列
synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}
//OKHttp中是一个不设置上限数量的线程池,PS:这个支持外部配置
public synchronized ExecutorService executorService() {
  if (executorService == null) {
    executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
  }
  return executorService;
}

//6个参数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

 

线程池6个参数:

参数名

参数含义

corePoolSize

核心线程数

maximumPoolSize

最大线程数

keepAliveTime

持续存活时间

unit

时间单位

workQueue

任务队列

任务执行

Java
final class AsyncCall extends NamedRunnable {
    @Override
    protected void execute() {    
        try{
            Response response = getResponseWithInterceptorChain();
            ...
        } catch (Exception e) {
            ...
            responseCallback.onFailure(RealCall.this, e);
        }
       
    }
}

4.2 连接池线程池

连接池线程池用于连接池定期清理一些过期的连接。

创建一个最大线程数量为1,过期时间为60秒的线程池。

Java
private final Executor executor = new ThreadPoolExecutor(
    0 /* corePoolSize */, 1 /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

当连接数量大于0时,设置任务定期清理连接。

Java
private Runnable cleanupRunnable = new Runnable() {
  @Override
  public void run() {
    while (true) {
        //cleanup
为根据各种条件计算出来的等待时间,这里不是重点就不详细介绍了。
        long waitNanos = cleanup(System.nanoTime());
        ...
        ConnectionPool.this.wait(waitMillis, (int) waitNanos);
    }
  }
}   

   

4.3 缓存线程池

缓存线程池用于定期清理一些过期的缓存。

创建最大线程数量为1的线程池。

Java
Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));

缓存线程池,是每次使用缓存的时候进行过期检查。而只执行一次,清理完成则任务执行完成。

Java
//DisLruCache
public synchronized Snapshot get(String key) throws IOException {
    if (journalRebuildRequired()) {
      executor.execute(cleanupRunnable);
    }
}

//
private final Runnable cleanupRunnable = new Runnable() {
    public void run() {
        synchronized (DiskLruCache.this) {
            ...
            trimToSize();
        }
    }
}

private void trimToSize() throws IOException {
  while (size > maxSize) {
    Entry toEvict = lruEntries.values().iterator().next();
    removeEntry(toEvict);
  }
}

.Socket连接

socket连接过程

整个请求过程中,Socket完整连接时间分为写入,等待,读取三个部分。

如果使用读写线程不分离:

如果使用读写线程分离:

OKHttpsocket连接

因此,OKHttp的OKIO,其实就是对SocketInputStream的一层封装,最终还是依赖底层的能力。

反思

所以:为什么OKHttp不是用读写分离?

这个问题就留给读者了。

.本篇文章未涉及部分

自定义DNS、OKIO、HTTP2.0多路复用等等。

.随堂小问题

问:OKHttp中涉及到哪些设计模式?

答:构建者、责任链、工厂、观察者。

问:连接池有什么作用?

答:避免重复的握手连接,提高通道复用效率。

问:OKHttp有哪些优势?

答:使用方便、监听->丰富的监听方便排查问题;

自定义责任链->可扩展性强;

责任链->问题排查方便;

连接池复用、支持GZIP压缩、缓存机制等。

问:OKHttp可能存在哪些缺点?

答:频繁创建对象、读写未分离。

问:五层拦截器,第1,2,3,4,5层的作用是什么?

答:RetryAndFollowUpInterceptor:重试和重定向;

BridgeInterceptor:请求和响应时实现对象和文本的转换;

CacheInterceptor:缓存响应,方便复用;

ConnectInterceptor:创建合适的连接并完成握手;

CallServerInterceptor:完成最后的请求发送和响应的解析。

问:统一失败配置,比如不同的请求,无网或若网时失败返回话术不一样,应该怎么做?

答:自定义外层拦截器

问:如果想修改原有的缓存策略,比如后台返回不缓存的,仍要缓存,应该怎么做?

答:自定义连接拦截器中,修改request的url地址即可。

问:再举一些外层拦截器可能使用到的场景?

答:重试、日志记录、加密解密、压缩解压、MOCK工具

问:HTTP1.1和HTTP2.0区别?

答:

多路复用(Multiplexing)

头部压缩(Header Compression)

服务器推送(Server Push)

数据帧(Data Frames)

连接管理(Connection Management)

流量控制(Flow Control)

优先级(Priority)

问:为什么OKHttp使用只包含1个线程的线程池,而不是使用安卓的Handler?

答:个人推测:OKHttp面向的对象并不仅仅只是安卓。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/53870.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【鸿蒙】HarmonyOS NEXT星河入门到实战1-开发环境准备

目录 一、达成目标 二、鸿蒙开发环境准备 2.1 开发者工作下载 2.2 解压安装 2.3 运行配置安装node.js和SDK 2.4 开始创建第一个项目 2.5 预览 2.5.1 预览遇到的问题&#xff08;报错&#xff09; 2.5.2 修改内容查看预览 三、备用下载地址&#xff08;如果下载是4.X版…

Prism库:详解其核心组件和使用方法

Prism库简介 Prism库是一个开源项目&#xff0c;由 Microsoft 社区开发和维护。它是一组用于创建 WPF、UWP 和 Xamarin 应用程序的工具和库&#xff0c;提供了一种基于模块化和依赖注入的架构模式&#xff0c;同时它提供了一系列的工具&#xff0c;帮助开发人员构建可扩展、可…

MATLAB、FPGA、STM32中调用FFT计算频率、幅值及相位差

系列文章目录 文章目录 系列文章目录前言MATLABSTM32调用DSPSTM32中实现FFT关于初相位 FPGA 前言 最近在学习如何在STM32中调用FFT MATLAB 首先对FFT进行一下说明&#xff0c;我们输入N个点的数据到FFT中&#xff0c;FFT会返回N个点的数据&#xff0c;这些数据都是复数&#…

ctfshow-PHP反序列化

web254 源码 <?php/* # -*- coding: utf-8 -*- # Author: h1xa # Date: 2020-12-02 17:44:47 # Last Modified by: h1xa # Last Modified time: 2020-12-02 19:29:02 # email: h1xactfer.com # link: https://ctfer.com //mytime 2023-12-4 0:22 */ error_reporting(0)…

MATLAB | R2024b更新了哪些好玩的东西?

Hey, 又到了一年两度的MATLAB更新时刻&#xff0c;MATLAB R2024b正式版发布啦&#xff01;&#xff0c;直接来看看有哪些我认为比较有意思的更新吧! 1 小提琴图 天塌了&#xff0c;我这两天才写了个半小提琴图咋画&#xff0c;MATLAB 官方就出了小提琴图绘制方法。 小提琴图…

鸿蒙读书笔记1:《鸿蒙操作系统设计原理与架构》

笔记来自新书&#xff1a;《鸿蒙操作系统设计原理与架构》 HarmonyOS采用分层架构&#xff0c;从下到 上依次分为内核层、系统服务层、框架层和应用层。 1. 内核层 内核层主要提供硬件资源抽象和常用软件资源&#xff0c;包括进程/线程管 理、内存管理、文件系统和IPC&#xff…

Unity教程(十五)敌人战斗状态的实现

Unity开发2D类银河恶魔城游戏学习笔记 Unity教程&#xff08;零&#xff09;Unity和VS的使用相关内容 Unity教程&#xff08;一&#xff09;开始学习状态机 Unity教程&#xff08;二&#xff09;角色移动的实现 Unity教程&#xff08;三&#xff09;角色跳跃的实现 Unity教程&…

react js 路由 Router

完整的项目,我已经上传了 资料链接 起因, 目的: 路由, 这部分很难。 原因是, 多个组件,进行交互,复杂度比较高。 我看的视频教程 1. 初步使用 安装: npm install react-router-dom 修改 index.js/ 或是 main.js 把 App, 用 BrowserRouter 包裹起来 2. Navigate 点击…

redis基本数据类型和常见命令

引言 Redis是典型的key-value&#xff08;键值型&#xff09;数据库&#xff0c;key一般是字符串&#xff0c;而value包含很多不同的数据类型&#xff1a; Redis为了方便我们学习&#xff0c;将操作不同数据类型的命令也做了分组&#xff0c;在官网&#xff08; Commands | Do…

TS 常用类型

我们经常说TypeScript是JavaScript的一个超级 TypeScript 常用类型 TypeScript 是 JS 的超集&#xff0c;TS 提供了 JS 的所有功能&#xff0c;并且额外的增加了&#xff1a;类型系统 所有的 JS 代码都是 TS 代码 JS 有类型&#xff08;比如&#xff0c;number/string 等&…

《JavaEE进阶》----14.<SpringMVC配置文件实践之【验证码项目】>

本篇博客介绍的是Google的开源项目Kaptcha来实现的验证码。 这种是最简单的验证码。 也是很常见的一种验证码。可以去看项目结果展示。就可以明白这个项目了。 前言&#xff1a; 随着安全性的要求越来越高、很多项目都使用了验证码。如今验证码的形式也是有许许多多、更复杂的图…

基于SpringBoot的古城墙景区管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的古城墙景区管理系…

2024数学建模国赛官方评阅标准发布!

​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑​↑…

C++类与对象(下)--最后的收尾

内部类 • 如果⼀个类定义在另⼀个类的内部&#xff0c;这个内部类就叫做内部类。内部类是⼀个独⽴的类&#xff0c;跟定义在 全局相⽐&#xff0c;他只是受外部类类域限制和访问限定符限制&#xff0c;所以外部类定义的对象中不包含内部类。 #include<iostream> using…

安装FTP服务器教程

一。安装vsftpd yum install vsftpd 二。修改配置文件&#xff0c;匿名账户具有访问&#xff0c;上传和创建目录的权限 vim /etc/vsftpd/vsftpd.conf &#xff08;红色进行设置放开YES&#xff09; local_enable&#xff1a;本地登陆控制&#xff0c;no表示禁止&#xff0c;ye…

3. 轴指令(omron 机器自动化控制器)——>MC_MoveAbsolute

机器自动化控制器——第三章 轴指令 4 MC_MoveAbsolute变量▶输入变量▶输入输出变量▶输入输出变量 功能说明▶指令详情▶时序图▶重启运动指令▶多重启动运动指令▶异常 示例程序1▶参数设定▶动作示例▶梯形图▶结构文本(ST) 示例程序2▶参数设定▶动作示例▶梯形图▶结构文…

RDMA应用场景及效果

GPU Direct 参考&#xff1a;网络架构如何支持超万卡的大规模 AI 训练&#xff1f;| AICon_芯片与网络_InfoQ精选文章 GPU 网络的情况已经发生了很大变化。每个 GPU 都有自己的内部互联&#xff0c;例如 NVIDIA 的 A100 或 H800&#xff0c;它们内部的 NVLink 互联可以达到 6…

单个 java 虚拟机 生产者消费者

一、通过 java.lang.Object#wait()&#xff0c;java.lang.Object#notify&#xff0c;java.lang.Object#notifyAll来实现 生产者&#xff0c;消费者 public abstract class Goods {protected String type;protected String goodName;protected int number;public abstract …

【Authing身份云-注册安全分析报告-无验证方式导致安全隐患】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

香港打工人√三天通过微软mos认证

在繁忙的香港&#xff0c;时间就是金钱&#xff0c;效率就是生命。作为一名香港的打工人&#xff0c;在这座竞争激烈的城市中&#xff0c;不断提升自我是保持竞争力的关键。最近&#xff0c;我完成了一项挑战&#xff1a;在短短三天内通过微软MOS认证大师。以下是我备考的经验分…