android基础复习笔记——5.从OkHttp的源码来看HTTP

1.OkHttp的历史:

最初是square觉得android给的那一套方案不是很好用,于是他给做了一下包装,包装以后就好用了,慢慢地,他们把httpclient给剔除了,再后来,他被Google给收购了,现在我们用的比较新的android系统(4.4的时候),内部的HttpURLConnection的实现用的其实是okhttp的代码。
okhttp其实就是原生的从头到尾实现了http的一个工具,同时让你对http的使用方便一点,你想要cash、cookie都比较方便。他并不只是一个方便工具,首先他是一个http。但是tcp这些连接的过程他也全都做了,他完全不依赖Google的那一套东西了。最初依赖,后来不依赖,最后Google把他给收了。

2.是什么?

https://square.github.io/okhttp/
他是一个http和http2的client。
在介绍retrofit的时候还提到了他是类型安全的。因为他本身就做了一些额外的工作,他接收到的那些数据他会去做处理,会去做转型,你收到的是一个body,但是你可以转成一个User,这个就跟类型安全有关,你怎么转我不给你报错,而okhttp他只是去做http的,而且他主要做的是下层支持,上层只是比较舒服而已,但他不是一个上层库。

3.怎么用?

看官网。
我先创建一个client,然后newcall、execute。
然后我们android不能用execute,因为这个execute跟retrofit一样,execute是同步的,是不转线程的,但是我们得把他放在后台线程。我要换另外一个方法是enqueue

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

自己写一个demo,用法特别简单。

OkHttpClient client = new OkHttpClient();
client.newCall(new Request.Builder().url("http://api.github.com").build())
        .enqueue(new okhttp3.Callback() {
            @Override
            public void onFailure(okhttp3.Call call, IOException e) {
                
            }

            @Override
            public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {

            }
        });

第一步把他初始下来,然后设定我要访问哪个url,这样的话默认就是一个get请求,你的get不用写了,默认是get,就好像你在浏览器里面写一样。

new Request.Builder().url("http://api.github.com").build()

括号里面是创建一个request,你每次request都需要重新创建一次,然后他的生命周期,怎么去调数据,怎么去进行访问,返回这结果,这都是okhttp来帮你管理,但是你每次要做的时候,你需要创建一个新的请求,这个请求叫做request。
然后,enqueue就是异步地去做请求。

4.源码解读

主要讲结构:
okhttp是怎样实现了http,是怎样实现了tcp,是怎么实现了https

1.newcall入手

newcall入手,newcall是什么?是他创建一个call,你的client创建一个call,你创建一个call,就可以用这个call去进行真正的网络交互了。而他传了一个参数,这个参数是request,而这个request是你自己拼出来的。这个方法就是,传进来一个request,然后我用这个request创建一个call,创建一个待用的网络请求。
点进去

@Override public Call newCall(Request request) {
  return RealCall.newRealCall(this, request, false /* for web socket */);
}

他会调用另一个方法,RealCall.newRealCall(),他会返回一个realcall,我要的是call,返回的是realcall,很明显realcall是call的实现。

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
  // Safely publish the Call instance to the EventListener.
  RealCall call = new RealCall(client, originalRequest, forWebSocket);
  call.eventListener = client.eventListenerFactory().create(call);
  return call;
}

第一行是创建这个对象, 利用你传过来的参数,
client是大总管,总配置的,没有什么好解析的。等会细讲。
originalRequest就是你传过来的request,他为什么叫originalRequest?这个跟待会要讲到的链有关了,他是最初的request。稍后可能会转变得不一样。
forWebSocket关系就比较远了,他是指你这是不是一个WebSocket的call。WebSocket是http的一种扩展。他能在你的http的请求和响应之间做一点手脚,你依然是一个http,但是我做一点手脚,我让你们的交互,他是什么状态呢?我们之前说http是一种怎样的模式,是一种cs模式。是一个客户端服务器的模式,客户端发一个请求,服务器接收以后再把这个请求的响应返回来。你再发一个我再返回来。都是那种你来我往,一来一往这样的交互。服务器是不能主动发请求的。我向客户端请求数据,或者我直接向客户端推送,这个做不到。而WebSocket就是对http做了这样的扩展。他并没有扩展标准,只是在实现上他改了。那么就可以让服务器去给客户端做推送了。但是我们用的都是传统的http。很少有用到WebSocket这种方式的。WebSocket谁用呢?那些做交易平台的,不是咸鱼那种交易平台,而是股票,证券什么的这种交易平台,还有虚拟货币这种东西,他们需要经常地,频繁的刷新数据。但是每次都是去轮询的话,非常的费流量,非常费电,那么他们会用WebSocket,用这种方式来实现推送。所以一般都用不到。另外呢,用到的人,对这个都很熟了。你在炒股的公司你会对WebSocket不熟吗?所以这个东西不说了,他是额外的一个小领域。
第二行,taught会创建一个eventListener。你的okhttp他的http过程中会有一些关键的时间点,比如tcp连接建立了,所有连接都是指的tcp连接,或者是ssl连接没有所谓的http连接。什么叫连接?就是,我是一个机器,你是一个机器,我们两个交互,我要能够记住你是谁。那么你给我发消息我直接就知道了, 你不用再说你是谁,这是连接。http本来就是无连接的,所有连接都是指的tcp连接,或者是ssl连接(安全连接,就是https所使用的连接)。就是你有很多很多状态,什么连接建立呀、开始请求呀、还有返回响应这些东西,他们都是一些时间点,你可以记录在eventListener里面,用他来做响应做标记。

2.enqueue

再看一下enqueue,点进来

public interface Call extends Cloneable {
...
void enqueue(Callback responseCallback);
...
}

他是一个接口,这个call跟retrofit的call不是一个call,但是他们都是一个接口,那么我怎么看他的实现呢?刚刚看到了RealCall是call的实现,我就直接跳到RealCall里面

@Override public void enqueue(Callback responseCallback) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace();
  eventListener.callStart(this);
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

内容不多,往listener里面加了个事件

eventListener.callStart(this);

另外有这么一行,他又把东西转交给别人了。转交给一个叫dispatcher的东西,让他去enqueue了,然后他也生成一个新的对象AsyncCall,一个异步的call

client.dispatcher().enqueue(new AsyncCall(responseCallback));

那我们分别看一下dispatcher()和AsyncCall是什么

public Dispatcher dispatcher() {
  return dispatcher;
}

dispatcher返回的是一个Dispatcher对象,那么我看一下Dispatcher

public final class Dispatcher {
  private int maxRequests = 64;
  private int maxRequestsPerHost = 5;
  private @Nullable Runnable idleCallback;

  /** Executes calls. Created lazily. */
  private @Nullable ExecutorService executorService;
  ...
  }

Dispatcher其实是一个管理线程的东西,就是你的每一个新的请求,新的request和他的response这个过程,你的请求和返回的过程,他是需要一个单独的线程,这样你不用的请求之间不会互相被挡着。怎么做的呢?靠的是线程控制。那线程控制用的是谁呢?用的就是这个Dispatcher。他的内部实现用的是execute。
暂时只要知道,他是用来管理线程的,有了他,多个线程就可以被简单地控制,我要多就可以多,我要少就可以少,可以分配。
Dispatcher有两个默认的东西,

 private int maxRequests = 64;
 private int maxRequestsPerHost = 5;

maxRequests:当我的总连接达到64的时候,就不去做新的请求了,我等一等,
maxRequestsPerHost:当我对某一个主机的请求达到5个,这个时候我对这个主机不做新的请求了。比如现在我只请求了两个主机,a主机有两个请求,b主机有五个请求,这个时候用户又要往b主机发一个请求,我把她先给放着。就是你对某一个网站,对某一个服务器也不进行过大的压力。
之前说过,enqueue的过程,压到队列的过程,他并不是让你没一个请求按顺序执行,而是让他们分部,按队列执行,只是到一定程度的时候他们会等一等。他们的程度就是在这设置的。
这两个值都可以设置,如果你想让请求一个一个按顺序执行,就可以把maxRequests设为1。

这个时候知道什么是Dispatcher了,那看一下他的enqueue,刚才已经说过enqueue是干什么了,就是让他们并列执行,只是到顶峰的时候,再往队列里面存一存。

synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}

if分两步:
如果我的数量没超限,那么就直接执行了,
如果我超限了,就放到ready队列里面。一个待命的队列,随时准备发出请求。
另外看一个东西,他的参数是AsyncCall,这个AsyncCall他做了什么?
他应该有一个run方法,但是没有找到,线程控制都是runnable里面的run方法被执行

final class AsyncCall extends NamedRunnable {...}

这里面没有,就去他的父类看。

public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = Util.format(format, args);
  }

  @Override public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();
}

父类里面有一个run方法,不过我发现这个run方法还是执行execute(),注意这个execute()又是另外一个类了,跟前面的dispatcher不是一个。是谁?就是RealCall里的AsyncCall实现的execute()。

@Override protected void execute() {
  boolean signalledCallback = false;
  try {
    Response response = getResponseWithInterceptorChain();
    if (retryAndFollowUpInterceptor.isCanceled()) {
      signalledCallback = true;
      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } else {
      signalledCallback = true;
      responseCallback.onResponse(RealCall.this, response);
    }
  } catch (IOException e) {
    if (signalledCallback) {
      // Do not signal the callback twice!
      Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      eventListener.callFailed(RealCall.this, e);
      responseCallback.onFailure(RealCall.this, e);
    }
  } finally {
    client.dispatcher().finished(this);
  }
}

说到这有好几层了,其实就是你在外部调用enqueue的时候,最终会来到RealCall里的AsyncCall实现的execute()方法。

client.newCall(new Request.Builder().url("http://api.github.com").build())
        .enqueue(new okhttp3.Callback() {
            @Override
            public void onFailure(okhttp3.Call call, IOException e) {

            }

            @Override
            public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {

            }
        });

AsyncCall的execute()方法做了什么,重点全在这个方法里面:

 Response response = getResponseWithInterceptorChain();

获取响应,通过拦截器的链,还没有执行,怎么就有响应了?都在这里面,但是往里面讲就很深了,待会说。

现在要说的是,如果你是用enqueue这种方法,那么他会进入你的realcall,调用你的dispatcher这个线程管理工具的enqueue,从而触发了你的AsyncCall他的run方法,再触发他发execute方法,最终到了我稍后要讲的getResponseWithInterceptorChain()。那么这个到这就圆满了,怎么圆满了?你先是把你的请求放到后台,然后去做实际的网络请求,然后把网络请求结果返回回来。这是一个完整的过程。
除了enqueue还有一个execute,我们用得很少,但是不是完全不用,因为有的时候我们已经在后台了。比如,现在我的网络请求失败了,网络请求由于某种原因失败了,比如我的权限不足。我需要现在获取我的token,去在线获取我的token,获取token之后,继续进行请求,那么我现在是在后台的,我就不需要enqueue,我使用execute。那么我简单看一下execute做了什么,就是直接进行网络请求的情况。

@Override public Response execute() throws IOException {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace();
  eventListener.callStart(this);
  try {
    client.dispatcher().executed(this);
    Response result = getResponseWithInterceptorChain();
    if (result == null) throw new IOException("Canceled");
    return result;
  } catch (IOException e) {
    eventListener.callFailed(this, e);
    throw e;
  } finally {
    client.dispatcher().finished(this);
  }
}

直接调用getResponseWithInterceptorChain()。

3.大致结构出来了

这个就是他的大结构,
创建一个realcall,
enqueue或者execute去进行网络请求,
然后返回响应。

现在说两个东西:
okhttp在实用的角度,不管在开发的实用,还是让我们去理解okhttp和http他们的紧密关系,以及让你加深对http的理解,不管是从哪个角度,需要先看一下okhttpclient这个方法,他里面的多种配置项,都有什么?他们有什么作用?这是一个。

OkHttpClient client = new OkHttpClient();

另外就是去解读getResponseWithInterceptorChain()这个方法。这个方法是okhttp技术上的核心。如果你想理解他的原理,你想更好地使用他,或者是你想在面试的时候更加帅,这个东西是你需要理解的。

4.OkHttpClient有些什么配置

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {...}

说一下他的配置,这些项,有几个我也不是很清楚,但是我所用过的,以及我没用过但是我认为会比较有用的,我都进行深入了解了。

public static final class Builder {
  Dispatcher dispatcher;
  @Nullable Proxy proxy;
  List<Protocol> protocols;
  List<ConnectionSpec> connectionSpecs;
  final List<Interceptor> interceptors = new ArrayList<>();
  final List<Interceptor> networkInterceptors = new ArrayList<>();
  EventListener.Factory eventListenerFactory;
  ProxySelector proxySelector;
  CookieJar cookieJar;
  @Nullable Cache cache;
  @Nullable InternalCache internalCache;
  SocketFactory socketFactory;
  @Nullable SSLSocketFactory sslSocketFactory;
  @Nullable CertificateChainCleaner certificateChainCleaner;
  HostnameVerifier hostnameVerifier;
  CertificatePinner certificatePinner;
  Authenticator proxyAuthenticator;
  Authenticator authenticator;
  ConnectionPool connectionPool;
  Dns dns;
  boolean followSslRedirects;
  boolean followRedirects;
  boolean retryOnConnectionFailure;
  int connectTimeout;
  int readTimeout;
  int writeTimeout;
  int pingInterval;
  ...
  }

1.Dispatcher 线程调度

首先,这个Dispatcher,上面有涉及到了。他是去控制线程,调度线程,然后使用不同的线程进行网络请求的,另外他还会有一个性能的平衡,在达到一定数量之后我不做了,我歇一歇,等一等。

2.Proxy 代理

第二个是Proxy,Proxy是你自己可以配置的一个代理。对他的理解,其实主要是对概念要有理解。什么是Proxy?比如现在我想要访问一个国外的网站,这个国外的网站由于一些原因,从我家的网络连不过去,然后我需要从另外一个地方连过去。国外有两个服务器,一个是从我家不知道什么原因连不到,但是我知道这个服务器并没有坏掉,我希望连过去,我从我的主机去连到另外一台主机,这台主机可能是国外的,也可能是国内的,无所谓,总之,这台中介机器,他可以连到目标机器,那么我去找我的中介机器,他就是我的代理,我把我要做的事情,我想要访问谁,我写得清清楚楚,去交给代理服务器,然后代理服务器把我请求做了,这个就是一个合理的,合法的,合要求,并且我自己清清楚楚他是什么作用的中间人。

3.Protocol 协议版本

Protocol是什么?点进去就知道了。他是你列给你客户端okhttpclient,你所支持的协议的版本。这样你的okhttp他的工作过程中他就知道哪些是他的选项,他自己会去做调配。
SPDY说一下,SPDY跟http2很像,他是http2的一个前身,http2借鉴了SPDY很多东西。spdy初是Google在用的,Google自己用,后来慢慢大家都吸收一下,改进一下,成为标准。这个标准叫做http2。所以spdy现在其实已经被废弃了。

public enum Protocol {
  HTTP_1_0("http/1.0"),
  HTTP_1_1("http/1.1"),
  SPDY_3("spdy/3.1"),
  HTTP_2("h2"),
  ...
  }

Protocol就是这样一个东西,所以她应该是一个列表,是多个选项。你的客户端可以现场去选择,就像我们的浏览器,我们的浏览器会同时支持http1.0、1.1、1.2、2.0。可能旧一点的浏览器就无法支持2.0。OkHttpClient,什么Client?客户端,跟浏览器一个级别的东西。

4.ConnectionSpec 连接标准

看名字,连接规格?点进去。

public final class ConnectionSpec {

  // This is nearly equal to the cipher suites supported in Chrome 51, current as of 2016-05-25.
  // All of these suites are available on Android 7.0; earlier releases support a subset of these
  // suites. https://github.com/square/okhttp/issues/1972
  private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] {
      CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
      CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
      CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
      CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
      CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
      CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,

      // Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll
      // continue to include them until better suites are commonly available. For example, none
      // of the better cipher suites listed above shipped with Android 4.4 or Java 7.
      CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
      CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
      CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
      CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
      CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
      CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
      CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
  };

  /** A modern TLS connection with extensions like SNI and ALPN available. */
  public static final ConnectionSpec MODERN_TLS = new Builder(true)
      .cipherSuites(APPROVED_CIPHER_SUITES)
      .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
      .supportsTlsExtensions(true)
      .build();

  /** A backwards-compatible fallback connection for interop with obsolete servers. */
  public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS)
      .tlsVersions(TlsVersion.TLS_1_0)
      .supportsTlsExtensions(true)
      .build();

  /** Unencrypted, unauthenticated connections for {@code http:} URLs. */
  public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
  ...
  }

这些就是配置的,你是要使用http还是https?如果你要是使用https的话,你的版本ssl3.0,还是tls1.0、1.1、1.2,他也是一个列表。如果你使用的是https的话,你的可用tls也是一个列表,这个在之前讲https的时候有讲过,你在客户端向服务器去通知你想建立一个安全连接的时候,你会发什么?你会发一个CIPHER_SUITES(密码组件),CIPHER_SUITES里面有什么呢?

CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,

RSA:非对称加密算法
AES_256:对称加密算法
SHA384:哈希算法

除了CIPHER_SUITES还会发一个tls版本。1.3,1.2,1.1,1.0

  public static final ConnectionSpec MODERN_TLS = new Builder(true)
      .cipherSuites(APPROVED_CIPHER_SUITES)
      .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
      .supportsTlsExtensions(true)
      .build();

这些是一个统一的支持,因为他是一个可接受方案,你能够支持什么,你能够接受什么?至于对方怎么给你,对方能够接受什么,你们最终商讨一个方案,他们是固定的。比如最终你们决定要么用1.1,要么用1.2,肯定不能是既用1.1,又用1.2,但是商讨过程,发的肯定是一个列表,让他去选。

COMPATIBLE_TLS是给比较旧的服务器用的,一般也不用。

public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS)
    .tlsVersions(TlsVersion.TLS_1_0)
    .supportsTlsExtensions(true)
    .build();

CLEARTEXT是什么意思,需要单独解释一下。

public static final ConnectionSpec CLEARTEXT = new Builder(false).build();

CLEARTEXT是明文的意思。所谓明文,对于ConnectionSpec,对于这个连接标准,完全等于http。
因为http就是你的数据是透明的,就是敞开的,不加密的。反过来说,什么情况下就是明文呢?不加密嘛。

5.interceptors、networkInterceptors

这两个Interceptor暂时先不说。还是稍后讲到的方法,getResponseWithInterceptorChain()。而interceptors和networkInterceptors他们是可以在这个chain里面插入的两个Interceptor列表。他们分别做什么?有什么区别?等会再讲。

6.eventListenerFactory

这个更不用说了,eventListener是用来做记录的,factory是用来创建listener的东西。

7.proxySelector

不知道是什么,没有用过。

8.cookieJar

cookieJar这个东西有的时候真的感觉到我们中国学程序是挺吃亏的。cookieJar这个词根本不用解释,但是中国人就得解释一下。首先cookie是浏览器跟服务器之间一个完整的机制,服务器要存东西,他让服务器来存,是用cookie。但是实际上cookie他的本意是什么?小饼干,曲奇饼干。什么是jar呢?我们Java程序员看到这,就觉得是java的jar包。java的打包不就是jar吗?后来android的打包不是aar吗?jar不就是java的包吗?我们会这样感觉,你们cookieJar我们就理解不了了。但实际上jar是什么呢?jar是罐子,而且外国的小朋友,有一个自己的饼干罐,他的妈妈给他用烤箱给他做了很多饼干之后,吃不完,装哪?装在他的cookieJar里面。这其实是一个非常形象的东西,他其实是一个我们cookie的存储器,我们cookie存到本地,还是存到内存里面?这个okhttp是没有默认实现的。他不管,这根本就跟他的性格有关。okhttp他们是觉得,我们做客户端的没有必要去是实现cookie,那么你们要实现的话,你们实现吧,我不实现,我们做android的我们不用cookie。他们有这样的性格,但是他会给你实现出来这样的接口,你可以自己去发展。你们最终就是这么一个半生半熟的状态。可以用,但是你想用好的话,自己去指定他应该怎么存,怎么取。

9.Cache

这个比较好理解,就是我们http的cash,不过下面的InternalCache,我没有看懂他是干嘛的。

10.SocketFactory

我们做android的人,可能对Socket了解不是很多,有些人可能用到,Socket是什么呢?socket就是我们tcp的端口。
有个东西提一下,默认端口是80
https://hencoder.com:80/
另外你也可以写成8080、8088,这个你可以随便写,只要不超过他的上限就可以了。这个端口是tcp端口。
你在解析完之后,可能会给你解析成某个ip
https://222.222.222.222:80/
222.222.222.222:80,这一部分他们两个其实不是一级的
222.222.222.222是ip地址
80的端口

SocketFactory就是用来创建这个端口的。什么叫创建端口呢?其实就是和对方服务器连接,获取到连接端口,获取到可以往服务器写东西,可以从服务器读东西的那个所谓的端口,就是这个socket。
同理SSLSocketFactory他是ssl连接的factory,为什么要分开呢?因为socket是tcp的东西,而ssl socket其实是ssl的东西,都不是一层了。所以他们要分开做,他们原理都不一样。包括建立连接的方式也不不一样。tcp怎么建立?三次握手,ssl怎么建立?一大堆,什么密钥交换呀… 他们过程完全不一样。
http是没有端口的,没有所谓的http端口。因为他没有面向连接,他不面向连接。有时候我们说的比较顺嘴,说什么会http端口,其实他说的是tcp端口,http是没有端口的。

11.CertificateChainCleaner

他是我们从服务器拿下来的那些证书,有时候会包含很多很多内容,会包含好几个证书。证书嵌证书,一个证书链。有时候发下来会有一些无关的证书。那么CertificateChainCleaner会做什么呢?他会把你的这些东西都整理了,整理完之后就是一个链,或者一个序列。这些序列第一个就是对方网站的证书,然后一个一个往下,最后一个是你信任的本地根证书。这样是方便验证的,但是跟我们没有什么关系,已经很下层了。我只是让你知道他是什么。我为什么要让你知道他是什么东西,为什么?回顾http。只讲原理记不住。你配合代码会发现,okhttp不就是这么做的吗?

12.HostnameVerifier 主机名验证器

他是给https用的。怎么用的?你要去验证对方的host是不是你要访问的host。点进去看一下你会更清楚。

public interface HostnameVerifier {
    /**
     * Verify that the host name is an acceptable match with
     * the server's authentication scheme.
     *
     * @param hostname the host name
     * @param session SSLSession used on the connection to host
     * @return true if the host name is acceptable
     */
    public boolean verify(String hostname, SSLSession session);
}

HostnameVerifier有一个verify方法。option+command+鼠标左键,点进去看接口实现

@Override
public boolean verify(String host, SSLSession session) {
  try {
    Certificate[] certificates = session.getPeerCertificates();
    return verify(host, (X509Certificate) certificates[0]);
  } catch (SSLException e) {
    return false;
  }
}

对第一个证书做验证。为什么对第一个证书做验证?因为这是对方网站的证书

    Certificate[] certificates = session.getPeerCertificates();
    return verify(host, (X509Certificate) certificates[0]);

这个时候要分是IpAddress还是Hostname

public boolean verify(String host, X509Certificate certificate) {
  return verifyAsIpAddress(host)
      ? verifyIpAddress(host, certificate)
      : verifyHostname(host, certificate);
}

再点进去,最终经过各种各样的处理和排错,会比较对方的服务器名跟我们访问的是不是一样。
如果一样,通过,这肯定不是第三方中间人攻击。

return hostname.equals(pattern);

13.CertificatePinner

这个可能是对我们比较有用的一个点,他是用来做自签名的。有人可能会在公司用子签名,我说一种使用自签名比较简单的方法。 直接使用CertificatePinner就可以实现。
首先Certificate是什么?是证书。Pinner图钉。CertificatePinner就是一个证书固定器。有时候你的在线证书有时候会在本地验证不通过,也许他是自签名的,也许是你的本地的证书机构没有更新。总之在你本地验证不通过。但是你很清楚我是这个网站的开发者,或者说我是这个网站的程序员,我们公司的工程师能够很清楚地把这个证书的公钥告诉我,而且他告诉我,这个绝对就是我们的证书公钥。我把这个证书的信息记录在本地,我去下载证书,下完以后我去对比,两个证书只要一样不就完了,跟证书机构什么的没关系了。我只需要一个,什么密码学什么的都跟我无关,我只需要比对他们是一个。那么我怎么比对呢?
我把从远端下载下来的证书链,他的每一个证书的公钥记录下来,记在我的本地。然后我在实时请求的时候去比对。在我们连接建立的过程中,这个过程还没有到http,只是tls他的连接建立过程中,当我获取到这个证书的时候,我去比对一下他的公钥信息和我本地所存的公钥信息是不是一样,如果一致就可以了。
这个东西怎么用?示例里面的代码直接贴过来。改一下

String hostname = "publicobject.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
//这里可以加多个,越多越容易
        .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
        .build();
OkHttpClient client =new OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .build();
Request request = new Request.Builder()
        .url("https://" + hostname)
        .build();
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
});

14.Authenticator:proxyAuthenticator、authenticator

他们都是用来写登录授权的Authorization的header的。加这个做什么呢?当你的权限不足的时候,他会给你报错,会给你返回一个401…这个错okhttp会自动给你拦住,如果你使用authenticator()配了Authenticator,那么你稍后再遇到这种错的时候他就会自动弹过来,自动调用authenticator的回调方法,这个时候,你再去请求获取你的token,或者添加你的password和username组成的basic Authorization,把他们添加进去就可以了。
怎么用?

OkHttpClient client =new OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .authenticator(new Authenticator() {
            @javax.annotation.Nullable
            @Override
            public Request authenticate(Route route, Response response) throws IOException {
                return response.request().newBuilder().addHeader("Authorization","Basic 用户名密码的base64").build();
            }
        })
        .build();

15.ConnectionPool 连接池

连接池、线程池,都是一个带缓存的集合。比如连接池我设置他的最大值是20,初始值是5,那么刚上来我就有五个可以随时拿着就用的线程,或者随时拿着就用的连接,我有这样的连接池的好处就是,我可以随时有空了,我不需要现场创建就可以用。同样的,他有上限就不会让我的资源耗用过多。

16.Dns

dns是什么?简单看一下他的实现

 Dns SYSTEM = new Dns() {
  @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
    if (hostname == null) throw new UnknownHostException("hostname == null");
    try {
      return Arrays.asList(InetAddress.getAllByName(hostname));
    } catch (NullPointerException e) {
      UnknownHostException unknownHostException =
          new UnknownHostException("Broken system behaviour for dns lookup of " + hostname);
      unknownHostException.initCause(e);
      throw unknownHostException;
    }
  }
};

关键是这一行

Arrays.asList(InetAddress.getAllByName(hostname));

他是用系统的dns方法去获取dns,根据你的域名去拿到一个ip列表。是个列表。一个域名可能对应多个ip。这就是dns,你直接用就可以了。有时候你需要自己配是什么时候?是某个ip你不想解析,你想自己给他指,这种情况很少。

17.(boolean)followSslRedirects、followRedirects

boolean followRedirects,就是我遇到重定向需要跳转的时候, 我是不是要跳转,因为再跳转就是两次请求了。那么okhttp给我两个选择你是不是要跳转,默认就是跳转。
followSslRedirects:这个可不是https遇到的时候要不要跳,而是当你访问的是http,但要你跳的是hhtts,或者反过来,这样互跳的时候,是不是跳转。
为什么要单独设置这个呢?因为他们是有一些安全上的风险的。

18.(boolean)retryOnConnectionFailure

名字上来看是,当你的连接建立失败的时候你是否要重试?不过实际上不止是连接建立失败,就是你的请求失败了你要不要重新请求一下。不过请求返回404,500这种不算。这种属于别的例子。

19.(int)connectTimeout、readTimeout、writeTimeout

connectTimeout:tcp连接时间超时报错。
readTimeout:下载响应的时候等待时间
writeTimeout:写入一个请求的时候他的时间

20.(int)pingInterval

pingInterval是针对WebSocket的,WebSocket是用http的方式来做一个可以推送的,可以双向交互的一个通道,这个东西就需要长连接了,他需要去发送小的心跳的消息,去做确认,让那个连接能够持久建立,pingInterval就是ping的间隔,多长时间ping一次。为什么叫ping呢?他们做连接有两个东西,一个叫ping,一个叫pong。乒乓乒乓,实质上就是这么发的。我发一个ping过来,你接收以后发一个pong过来,让我知道你那边没断开。

5.核心:getResponseWithInterceptorChain()里的proceed机制

刚才我们看到不管是同步请求还是异步请求都有这个, 点进去具体看一下是怎么实现的。

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

这里面是什么内容,回顾一下,就是把你准备好的请求去做一个网络请求,然后得到响应。这个东西做的工作挺多的,他连tcp连接都是自己建立。那么整个过程是怎么回事?
你看这么几行,行数不多,但是每一行都有不同的工作,所以东西还是很丰满的。这几行代码就是okhttp在技术上的核心。大体说一下。

1.分三步

首先前面这几行都是去创建这个list,丰富这个list。

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

接下来是另外一行代码,他创建了一个chain,这个chain叫RealInterceptorChain。

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

然后在chain创建完以后调用了他的proceed方法。

  return chain.proceed(originalRequest);

接下来说一下这三步是什么,具体干什么了。

2.链

这是我一个Interceptor,取名叫I,接着创建6个,这是我的一个链。那个list是什么不用说,list是用来创建链里面每一个节点的。最终list准备好了之后就会把他创建成一个chain。接着会调用chain的proceed。
在这里插入图片描述

我的整个过程这个chain他是干嘛用的?他是用来做网络请求的。但实际网络请求他是怎么干的?是有方向的。我沿着这个方向一路走过去,就像一条生产线一样。发过去,再发回来。
在这里插入图片描述

为什么要一个链呢?举个例子说一下这个链是什么作用。
汉堡店有人订餐,他给我打电话了,行,你地址是哪跟我说,我是那个做汉堡的师傅,也是店老板,我把汉堡做好了,我把汉堡给店里负责分发汉堡的人,他等着,过一会我们店的送餐员来了,他把这个汉堡给了送餐员,送餐员骑车送到订餐人的家里面,然后那人开门,把汉堡取走,他钱给了送餐员,然后送餐员扣掉一块钱送餐费以后把剩下的钱交给了负责分发汉堡的人,然后她把神仙的钱交给我了,我是店老板。
这就是一个链,我就是链的起始端。我负责做汉堡,交出汉堡以及最后把钱拿到手。店里的店员负责交出去,交给送餐员,以及把送餐员拿回的扣过的钱交给我。送餐员负责把汉堡送给对方,以及收到钱,雁过拔毛,留下一块钱,然后把剩下的钱再给店里的店员。而终点是订餐的那个人,他做了两件事,一个是接收汉堡,一个是把汉堡钱给送餐员。这是个链。
你的结构比较复杂的时候,你把他拆成链,每一个节点做不同的事情。每一个节点就是拦截器。为什么叫chain,为什么叫Interceptor.Chain?就是这么一个作用,并不是说他要挡着你的路,而是他需要做一些额外的事情。停一下,让我加工加工。在做之前加工一下,做之后也加工一下,这就是拦截器的他的模型。
具体说一下,我们网络拦截器Interceptor.Chain具体做的是什么。

3.proceed()

chain.proceed()他是什么呢?
我从这个点,到这,然后回来。

在这里插入图片描述
这个什么意思呢?就是从出发到这,工作从第一个Interceptor,交给第二个Interceptor,然后第一个Interceptor等待,等工作回来,做返回的加工。chain.proceed()就是你的链往前的意思,前进一步。对于我来说就是把汉堡交给下一个人,你做你的工作吧。对于每一个节点来说,就是做事、等待、做事。

接下来讲一下每个节点。
这两个先不说,因为这两个东西是自己配的,本来是没有的,先把有的东西讲了。

interceptors.addAll(client.interceptors());
interceptors.addAll(client.networkInterceptors());

4.Interceptor拦截器

1.第一个拦截器RetryAndFollowUpInterceptor + proceed机制介绍

第一个,retry就是重试,FollowUp就是跟进,去跟进重定向的链接。

 interceptors.add(retryAndFollowUpInterceptor);

跳过去看一下。他的关键在于intercept方法。

public final class RetryAndFollowUpInterceptor implements Interceptor {
...
@Override public Response intercept(Chain chain) throws IOException {
  Request request = chain.request();
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Call call = realChain.call();
  EventListener eventListener = realChain.eventListener();

  StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
      createAddress(request.url()), call, eventListener, callStackTrace);
  this.streamAllocation = streamAllocation;

  int followUpCount = 0;
  Response priorResponse = null;
  while (true) {
    if (canceled) {
      streamAllocation.release();
      throw new IOException("Canceled");
    }

    Response response;
    boolean releaseConnection = true;
    try {
      response = realChain.proceed(request, streamAllocation, null, null);
      releaseConnection = false;
    } catch (RouteException e) {
      // The attempt to connect via a route failed. The request will not have been sent.
      if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
        throw e.getLastConnectException();
      }
      releaseConnection = false;
      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, streamAllocation, requestSendStarted, request)) throw e;
      releaseConnection = false;
      continue;
    } finally {
      // We're throwing an unchecked exception. Release any resources.
      if (releaseConnection) {
        streamAllocation.streamFailed(null);
        streamAllocation.release();
      }
    }

    // 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();
    }

    Request followUp = followUpRequest(response, streamAllocation.route());

    if (followUp == null) {
      if (!forWebSocket) {
        streamAllocation.release();
      }
      return response;
    }

    closeQuietly(response.body());

    if (++followUpCount > MAX_FOLLOW_UPS) {
      streamAllocation.release();
      throw new ProtocolException("Too many follow-up requests: " + followUpCount);
    }

    if (followUp.body() instanceof UnrepeatableRequestBody) {
      streamAllocation.release();
      throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
    }

    if (!sameConnection(response, followUp.url())) {
      streamAllocation.release();
      streamAllocation = new StreamAllocation(client.connectionPool(),
          createAddress(followUp.url()), call, eventListener, callStackTrace);
      this.streamAllocation = streamAllocation;
    } else if (streamAllocation.codec() != null) {
      throw new IllegalStateException("Closing the body of " + response
          + " didn't close its backing stream. Bad interceptor?");
    }

    request = followUp;
    priorResponse = response;
  }
}
...

intercept方法他会做什么呢?他做三件事情,就是刚才说的事前准备、交给下一个并等待回来、回来之后的后续处理。
第一件是最初的准备StreamAllocation,StreamAllocation注意听一下,他是关于OKhttp的一个很关键的概念。

 StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
      createAddress(request.url()), call, eventListener, callStackTrace);

点进去看StreamAllocation的注释

Connections:</strong> physical socket connections to remote servers. These are
potentially slow to establish so it is necessary to be able to cancel a connection
currently being connected.
Streams:</strong> logical HTTP request/response pairs that are layered on
connections. Each connection has its own allocation limit, which defines how many
concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
at a time, HTTP/2 typically carry multiple.
Calls:</strong> a logical sequence of streams, typically an initial request and
its follow up requests. We prefer to keep all streams of a single call on the same
connection for better behavior and locality.

okhttp有三个关键概念:
Connection:指的是我和服务器之间的一个连接。
Stream:我做一次请求,可能要和服务器发消息,接收消息,这么一个对,这一个对叫stream。
Call:我现在想要访问一个https的url,我进行请求最终得到一个响应这就是一个call。
我们的call可能会做跳转,可能会做重定向。所以call可能会有多个stream。

接下来RetryAndFollowUpInterceptor的前置工作,基本上没有什么前置工作,因为作为重试和重定向的东西,要什么前置工作?主要是后置工作。

this.streamAllocation = streamAllocation;

  int followUpCount = 0;
  Response priorResponse = null;
  while (true) {
    if (canceled) {
      streamAllocation.release();
      throw new IOException("Canceled");
    }

    Response response;
    boolean releaseConnection = true;

什么是后置呢?你得先把这个事干了。也就是你在这也调用一次proceed

response = realChain.proceed(request, streamAllocation, null, null);

proceed流程是这样,前面的节点预处理完,调用proceed,但是proceed没有结束,他会等待,等到后面的节点依次预处理,并且依次proceed完以后,最前面的proceed才算结束,然后走后置的代码,有点像一个递归。
在这里插入图片描述

所以RetryAndFollowUpInterceptor里面所有proceed这一行之前的是前置工作,proceed之后的是后置工作,而proceed这一行的作用是把工作交给下一个节点,并且等待。
把这个理解明白以后,okhttp的链的工作结构就明白了。
看一下他的后置工作,他的后置工作是什么呢?想一想就能想出来,重试嘛,比如下面这个,RouteException,路由异常,也就是说找这个ip找失败了,怎么办呢?如果有多个ip换一个ip嘛。但是 他也是有前前提的,点进去看一下这个recover。

catch (RouteException e) {
  // The attempt to connect via a route failed. The request will not have been sent.
  if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
    throw e.getLastConnectException();
  }

他会看一下,先看你是不是有这个配置retryOnConnectionFailure(),连接失败是否重试,然后hasMoreRoutes(),是否还有别的路由方式。总之这个就是去判断你是不是可以对这个情形进行尝试性恢复。

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

其他后置工作也和这个大同小异,就是对异常的各种判断。
最终你会走到这,

Request followUp = followUpRequest(response, streamAllocation.route());

你去获取一个你的followUpRequest,如果你请求成功的话这儿会返回空的,

if (followUp == null) {
  if (!forWebSocket) {
    streamAllocation.release();
  }
  return response;
}

如果你失败了,而且符合重试条件,或者跳转条件,你就会获得一个新的followUpRequest,一个新的请求赋值进去,然后循环。

request = followUp;

往上看,这是一个死循环。
要么你拿到结果,return,请求成功了;
要么你获取到301、302,你给他一个新的请求,再循环一次,

while (true) {...}

Interceptor他主要是后置逻辑。

2.BridgeInterceptor

重点也是intercept方法

public final class RetryAndFollowUpInterceptor implements Interceptor {
...
@Override 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");
  }

  // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
  // the transfer stream.
  boolean transparentGzip = false;
  if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
    transparentGzip = true;
    requestBuilder.header("Accept-Encoding", "gzip");
  }

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

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

先找到proceed方法,然后用这个来判断前置后置。
看一下前置做了什么工作。

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

如果你有body,往里面添加Content-Type。

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

然后Content-Length,你之前不知道你的内容有多长,在创建过程中你是不知道body有多长的,现在你知道了,你可以写他的长度。另外你可以用Content-Length写,也可以用Transfer-Encoding写,两种都可以,他会自动判断应该写哪种。Content-Length是固定长,Transfer-Encoding是不定长。

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

添加host,主机名称。

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

Accept-Encoding是什么?你接受的编码格式。
而且会加一个gzip,如果你没有加的话,表示,我作为客户端,可以接受你给我发一个经过gzip压缩的编码格式的数据。这个很有意思,我作为开发者还没有说我接不接受这种编码格式,你就帮我接受了,万一我处理不了怎么办呢?为什么这样做呢?因为这个东西okhttp他给我做了。okhttp他本身有了对这种数据格式的支持。因此他会主动加上这个encoding。你作为软件开发者根本不用管。服务器发了gzip数据,我负责解压缩数据,这样就省了带宽。所有服务器,只要他支持,那么你用http的情况下,他一定帮你压缩数据的。解压缩的方式就在下面的后置工作里面。

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

另外说一点,cookieJar是需要自己写的。

List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
  requestBuilder.header("Cookie", cookieHeader(cookies));
}

对于cookiejar简单说一点点

public interface CookieJar {
  /** A cookie jar that never accepts any cookies. */
  CookieJar NO_COOKIES = new CookieJar() {
    @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
    }

    @Override public List<Cookie> loadForRequest(HttpUrl url) {
      return Collections.emptyList();
    }
  };
  ...
}

就两个方法:
saveFromResponse:拿到的响应把他存起来。
loadForRequest:当我需要做请求的时候,我从我的cookiejar里面取出来。我对应的这个域名他有没有存过。
我怎么写?我只需要去指定这个东西怎么写就可以了。
你看默认事件是这个是吧,我不这么写。
我有一个Map,存怎么存,cookies.put

CookieJar NO_COOKIES = new CookieJar() {
    Map map = new HashMap();
  @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
      map.put(url,cookies)
  }

  @Override public List<Cookie> loadForRequest(HttpUrl url) {
    return cookies.get(url);
  }
};

这个是cookiejar,需要自己实现的。默认是没实现的。默认存和写都是空代码。

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

接下来就是用户代理,如果你没有设置用户代理,他会加一个字符串。

public static String userAgent() {
  return "okhttp/3.10.0";
}

接下来他就会去做proceed,

Response networkResponse = chain.proceed(requestBuilder.build());

proceed之后就会去做解码,去做数据的解析。

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

那么什么是BridgeInterceptor?
就是在要发射之前,我把发射所需要的数据准备好,还有压缩我去提供一下支持。后续事件就是把这些东西都解一解,然后压缩的解压缩一下。

3.CacheInterceptor

CacheInterceptor非常简单。
他的intercept是对cash的处理,把cash存下来。存cash关键是这一行。

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

点进去看一下。

public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = cacheResponse;

  if (cacheResponse != null) {
    this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
    this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
    Headers headers = cacheResponse.headers();
    for (int i = 0, size = headers.size(); i < size; i++) {
      String fieldName = headers.name(i);
      String value = headers.value(i);
      if ("Date".equalsIgnoreCase(fieldName)) {
        servedDate = HttpDate.parse(value);
        servedDateString = value;
      } else if ("Expires".equalsIgnoreCase(fieldName)) {
        expires = HttpDate.parse(value);
      } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
        lastModified = HttpDate.parse(value);
        lastModifiedString = value;
      } else if ("ETag".equalsIgnoreCase(fieldName)) {
        etag = value;
      } else if ("Age".equalsIgnoreCase(fieldName)) {
        ageSeconds = HttpHeaders.parseSeconds(value, -1);
      }
    }
  }
}

他会根据你收到的cash,以及当前的日期、你的数据情况之类的,去判断你cash是否过期,如果没有过期,就直接把数据返回了。

if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

如果数据没有过期,就直接创建一个假的响应,返回回来了。
假如第三个就是CacheInterceptor,他的cash没有过期,就直接返回了,后面的都不走了。根本就没有进行网络交互,这个是cash的作用,后面完全省略了,而且不影响结果,这个就是http他所预期的。这虽然绕过了,但是没有任何损失。这个也是intercept他的好处,你的节点,对于你的前面和后面其实都是透明的,你们互相之间只负责自己的事情。前面的节点不需要知道这个信息是来之cash还是网络请求。

在这里插入图片描述

没什么好说的,具体代码还是很复杂,但是我们知道核心在哪。

4.ConnectInterceptor

ConnectInterceptor我们肯定是读不懂的,起码说不花时间是读不懂的。但是大致结构还是要明白。
这个地方就是他和https,ssl来做交互了。关键是这么两行。

HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();

有一行是newStream,什么是steam?就是你跟网络这一次交互。创建一个newStream,然后返回一个HttpCodec对象。
那么什么叫Codec?Codec是编码解码器的意思,code是编码,decode是解码。对什么编码解码?不是对音乐视频,而是对我们网络的数据。他的接口有两个实现,为什么有两个?一个http1,一个是http2,http2是二进制的形式,而1、1.0、1.1、1.9他们全都是用的文本形式。所以他们编码解码应该完全是两套算法。
你的newStream创建的codec只是其一,他还会创建一个connection,创建一个连接,这个连接不是下面那一行创建的,下面那一行只是拿到。其实核心的只有一行代码。
newStream我们跳转进去看一下。我们会跳转好几次,不需要懂,只需要知道我们讲到了什么东西。我想让你知道他是怎么跟tcp交互,怎么跟ssl交互。所以我们继续找,newStream里面有一个findHealthyConnection。

RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
    writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);

他会尝试去获取一个健康的连接,就是现在就可以用的连接。然后再点进去,他有一个findConnection,

RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
    pingIntervalMillis, connectionRetryEnabled);

先找到,再确定他是否健康。在findConnection里面又有一个connect

result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
    connectionRetryEnabled, call, eventListener);

再往后又有一个connectSocket,跟tcp连接的那玩意叫socket。

connectSocket(connectTimeout, readTimeout, call, eventListener);

再点进来connectStart,我开始做连接了,然后connectSocket,到这连接就建立完成。然后下面会有buffer,后面说io的时候再说buffer。
这个东西就是跟socket做对接的。这里面就获取到socket了。并且你的socket就连上对方了。不需要知道他是怎么连上的。

总之可以在这看到,okhttp他做了tcp连接。这跟我们前几年用的http库完全不一样,他们全都用的要么是Apache的HttpClient,或者是用了android自带的HttpUrlConnection。他们都没有自己做连接的,但是okhttp做了。

eventListener.connectStart(call, route.socketAddress(), proxy);
rawSocket.setSoTimeout(readTimeout);
try {
  Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
  ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
  ce.initCause(e);
  throw ce;
}

// The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
// More details:
// https://github.com/square/okhttp/issues/3245
// https://android-review.googlesource.com/#/c/271775/
try {
  source = Okio.buffer(Okio.source(rawSocket));
  sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
  if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
    throw new IOException(npe);
  }
}

接着拐回去,connectSocket的下面有一个establishProtocol

} else {
  connectSocket(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);

Protocol是http,http1.1这些东西,点进去

connectTls(connectionSpecSelector);

这里就是建立https的tls连接的地方。再点进来。
SSLSocketFactory这些东西都来了,什么ConnectionSpec,之前我们那些设置,到这全都用上了。
最终Handshak,什么hostnameVerifier都用上了,这里是用来建立一个ssl连接,或者叫tls连接。

sslSocket.startHandshake();

总之,在ConnectInterceptor里面,他干的事情,说下来就一点,建立连接。建立一个tcp连接或者一个tcp连接上面再叠加一个tls连接。
他没有后置工作。

5.CallServerInterceptor

CallServerInterceptor也是粗略看一下,了解代码做了什么。
他是最终的一个,他不需要proceed。他把事情做完,返回就可以了。
他做了几个实质工作,只需要看几个点就知道什么叫实质工作。
首先看这个writeRequestHeaders,

httpCodec.writeRequestHeaders(request);

writeRequestHeaders他是httpcodec做的事情,那个编码解码器,编码解码器干嘛用的?他和socket直接沟通的,那么writeRequestHeaders,写请求的headers是什么意思?点进去。是个接口,看一下他的实现,http1和2的都可以。再往下跳writeRequest。

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

这几行很有意思,你可能不知道sink什么意思,因为他是okio的东西,不是java自己io的东西,但是光从名字就可以看出来writeUtf8,写一段utf8的字符串。写requestLine,你的请求行。写到socket,也就是往网络上去写,往你的tcp或者tls的端口上面去写,最终写到网络对岸。okhttp他是对网络进行直接操作。他建立连接,他负责去沟通。写了这个之后,写了个换行writeUtf8("\r\n")。
然后把每一个header的名字写完,写一个冒号,再把值写下来,再换行。
然后再去写body。
就是http的格式,这样硬生生实现的。

回到CallServerInterceptor,写完请求之后,就会读,读响应里面的东西。

responseBuilder = httpCodec.readResponseHeaders(false);

读完之后,把他放到responseBody里面。然后把response返回。

response = response.newBuilder()
    .body(httpCodec.openResponseBody(response))
    .build();

返回之后就做其他节点的后置方法。
在这里插入图片描述

ConnectInterceptor结束返回,
ConnectInterceptor的后置工作,他没有后置工作,然后
CacheInterceptor,他拿到之后会尝试把cash存下来,存完之后
BridgeInterceptor,他会做gzip的解压,以及各种数据的解读完了之后,再到
retryAndFollowUpInterceptor,他看这个是不是301,302或者是其他什么需要跳转的,总之网络的问题不会在这跳,
网络问题在ConnectInterceptor出问题之后就会拐回来,不会往ConnectInterceptor走。

如果出问题需要重试,他会这样走。
在这里插入图片描述

6.interceptors、networkInterceptors

最后说一下这两个怎么做

interceptors.addAll(client.interceptors());
interceptors.addAll(client.networkInterceptors());

他们的实现跟其他interceptor是一样的,举个例子,如果我要加一个interceptor,但是什么都不做。

OkHttpClient client =new OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .addInterceptor(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                //前置工作
                Response response = chain.proceed(chain.request());
                //后置工作
                return response;
            }
        })
        .build();

相当于我在中间插了一个什么都没干的中间人。如果我想做什么事情,就可以插入前置工作和后置工作。
而interceptors、networkInterceptors有什么区别呢?
关键在于位置,
interceptors发生在最开始的位置,
networkInterceptors发生在结束阶段,返回的数据有可能读不懂的,数据还没有解压缩。所以,networkInterceptors一般是跟网络相关的,跟写数据相关的,对数据做一些前置工作和后置工作,你想操作这个时候再去网上查,一般情况是不用他。

okhttp他的角色是什么?他跟retrofit比起来?
他就是一个从连接建立,到tls连接建立,到http的传输,再到各种http的特性支持,比如重试,跳转,cash,cookie。他是整个接管了http的工作,然后顺便把api做得比原生舒服一点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值