开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情
OkHttp 4.X 及其以上的版本的源码为 kotlin 语言编写。本文讲解的 OkHttp 源码版本为 3.14.9,是 Java 语言编写的最后一个版本。因为 OkHttp 使用了 Okio 这个 IO 库,所以阅读本文之前,我非常建议大家先阅读我之前写的两篇关于 Okio 的文章,再去阅读 OkHttp 的源码,就会扫清一道障碍。
Http 请求/响应报文格式
okhttp 是一个 Http 协议的网络请求框架,专为 Java 和 Android 精心设计。所以我们有必要了解一些关于 Http协议的基础知识。Http 协议工作在 OSI 模型的应用层,基于TCP/IP通信协议来传递数据,使用 Socket 实现通信。
请求报文格式
Http 协议规定请求报文格式由请求行+请求头字段+请求体组成。
- 请求行:由请求方法+URL+协议版本组成,如
POST /api/listNotice HTTP/1.1
。 - 请求头字段:键值对的形式组成,完整的头字段可以在 www.iana.org/assignments… 找到。
- 请求体:请求发送的数据。
响应报文格式
Http 协议规定响应报文格式由响应行+响应头字段+响应体组成。
- 响应行:由协议版本+状态码+状态描述组成,如
HTTP/1.1 200 ok
- 响应头字段:键值对的形式组成,完整的头字段可以在 www.iana.org/assignments… 找到。
- 响应体:响应返回的数据
重要的头字段
头字段 | 说明 | 取值 |
---|---|---|
Content-Type | 1. 在请求中 ,客户端告诉服务器实际发送的数据类型。2. 在响应中,Content-Type 标头告诉客户端实际返回的内容的内容类型 | www.iana.org/assignments… |
Content-Length | 发送给接收方的消息主体的大小 | 十进制整数,字节的数目 |
Connection | 当前的事务完成后,是否会关闭网络连接 | keep-alive 或 close |
Cache-Control | 单向缓存指令,被用于在 http 请求和响应中,通过指定指令来实现缓存机制。 | developer.mozilla.org/zh-CN/docs/… |
Authorization | 用于提供服务器验证用户代理身份的凭据,允许访问受保护的资源 | developer.mozilla.org/zh-CN/docs/… |
回想一下 OkHttp 请求网络的过程
在正式开始分析源码之前,我们在脑海里回顾一下 OkHttp 发送一个网络请求要写哪些代码?对于一个简单的 Post 请求来说,要经历如下几个步骤。
- 使用 Builder 模式创建一个全局使用的
OkHttpClient
对象,可以用它来设置对所有请求都生效的超时时间、缓存、自定义拦截器等。 - 使用 Builder 模式创建一个
Request
,设置Request
请求的 url,头字段,请求体等。 - 使用全局的
OkHttpClient
对象发送同步或异步请求,之后可以获取来自服务器的响应。
本文分析的源码,会以上述一个完整的网络请求流程为主线进行。
Headers 类的设计-头字段
OkHttp 中,Headers
类表示 Http 请求或响应报文中的头字段,使用Builder
模式来创建对象。
Headers 成员变量
private final String[] namesAndValues;
namesAndValues
是一个字符串数组,用来存储头字段。因为头字段是键值对的形式,所以在namesAndValues
中index*2
位置存储key
,index*2 + 1
位置存储value
。如下面的形式。
Headers 成员方法
下面是一些比较重要的成员方法介绍
// 根据 key(name) 获取头字段的 value
public @Nullable String get(String name)
// 返回头字段的个数
public int size()
// 返回在数组中第 index 个头字段的 key
public String name(int index)
// 返回在数组中第 index 个头字段的 value
public String value(int index)
// 返回整个请求头或响应头的字节数
public long byteCount()
RequestBody 类的设计-请求体
RequestBody
是一个抽象类,内部没有成员变量。它表示 Http 请求报文中的请求体,在 Post 请求方式中,需要使用到RequestBody
来构建请求体。根据请求体内容编码类型的不同,RequestBody
有两个子类分别是FormBody
和MultipartBody
,对应application/x-www-form-urlencoded
和multipart/form-data
两种编码方式。
RequestBody 成员方法
// 返回媒体类型
public abstract @Nullable MediaType contentType()
// 写入数据到输出流
public abstract void writeTo(BufferedSink sink) throws IOException
// 返回请求体的字节数
public long contentLength() throws IOException
// 创建一个请求体
public static RequestBody create(params)
自定义媒体类型和输出流
RequestBody
有两个抽象方法contentType
和writeTo
,允许我们创建匿名类重写这两个方法自定义媒体类型(Content-Type)和写入到输出流的数据,更加灵活。
create 创建一个请求体
create
有多个重载的方法,是RequestBody
类创建RequestBody
对象的默认实现。大多数时候,我们可以使用create
方法来方便的创建一个请求体。
Request 类的设计-请求报文
我们已经熟悉了Headers
和RequestBody
,它们是Request
类的重要组成部分。Request
类表示一个请求报文,同样使用Builder
模式来创建对象。
Request 成员变量
// 请求 url
final HttpUrl url;
// 请求方法(get, head, post, put, delete 等)
final String method;
// 请求头字段
final Headers headers;
// 请求体
final @Nullable RequestBody body;
// 浏览器缓存控制,存储了 Cache-Control 头字段相关的指令信息。
private volatile @Nullable CacheControl cacheControl; // Lazily initialized.
Request
类的设计严格遵守了 Http 协议的请求报文格式,由请求行,请求头,请求体三大部分组成。同时Request
类还对头字段Cache-Control
的值做了封装。Request
类的成员方法都是用来获取这些字段值的,这里不做介绍。
ResponseBody 类的设计-响应体
ResponseBody
表示 Http 响应报文中的响应体,它被设计成一个抽象类,没有成员变量。它表示 Http 响应报文中的响应体。
ResponseBody 成员方法
// 返回媒体类型
public abstract @Nullable MediaType contentType();
// 返回响应体的字节数
public abstract long contentLength();
// 返回输入流,用于读取响应体的数据
public abstract BufferedSource source();
// 将响应体以字节数组的形式返回
public final byte[] bytes() throws IOException;
// 创建一个响应体
public static ResponseBody create(@Nullable MediaType contentType, String content);
实例化一个ResponseBody
需要重写contentType
,contentLength
,source
3个抽象方法。create
有多个重载方法,是ResponseBody
类创建ResponseBody
实例的默认实现,我们可以使用create
系列方法轻松创建出一个响应体。
Response 类的设计-响应报文
Response
类表示响应报文,使用Builder
模式来创建对象。
Response 成员变量
protocol
,code
,message
3个字段组成了响应行,headers
字段表示响应头字段,body
字段表示响应体。Response
类成员方法都是用来获取字段值的,这里不做介绍。到此我们已经分析完了 OkHttp 中与请求报文和响应报文相关的类,了解这些基础类的设计有利于我们后面源码的阅读。
// 为响应 Http 重定向或身份验证生成的 request
final Request request;
// Http 协议版本
final Protocol protocol;
// Http 响应状态码
final int code;
// Http 状态描述
final String message;
// TLS 握手记录,Https 相关
final @Nullable Handshake handshake;
// 响应头字段
final Headers headers;
// 响应体
final @Nullable ResponseBody body;
// 从网络接收到的原始响应报文,若使用了缓存将会返回 null
final @Nullable Response networkResponse;
// 从缓存获取的原始响应报文,若没用使用缓存将会返回 null
final @Nullable Response cacheResponse;
// Http 重定向或身份验证的响应报文
final @Nullable Response priorResponse;
// 发送请求报文时的时间戳
final long sentRequestAtMillis;
// 收到响应时的时间戳
final long receivedResponseAtMillis;
// 单个 Http 请求和响应的管理类
final @Nullable Exchange exchange;
// 缓存控制,存储了 Cache-Control 头字段相关的指令信息。
private volatile @Nullable CacheControl cacheControl; // Lazily initialized.
Call 接口的设计-已经准备好的请求
Call
是一个接口,它表示一个已经准备好被执行的请求(调用)。一个Call可以在执行过程中被取消,但是只能够执行一次,不允许被重复执行。
Call 接口方法
// 返回请求报文
Request request();
// 同步执行当前请求,会阻塞直到响应返回。返回后需要关闭资源,以防止资源泄露
Response execute() throws IOException;
// 异步执行当前请求
void enqueue(Callback responseCallback);
// 取消当前正在执行的请求
void cancel();
// 返回当前请求是否正在执行
boolean isExecuted();
// 返回当前请求是否被取消
boolean isCanceled();
// 返回超时对象
Timeout timeout();
// 复制一个一样的请求
Call clone();
RealCall 类的设计-Call 接口的实现类
Call
是一个接口,RealCall
类实现了这个接口。
RealCall 成员变量
// OkHttp 客户端
final OkHttpClient client;
// 应用层和网络层的数据传输媒介类
private Transmitter transmitter;
// 原始请求报文
final Request originalRequest;
// 是否为 WebSocket
final boolean forWebSocket;
// Guarded by this.
// 是否正在执行
private boolean executed;
RealCall 成员方法
RealCall
除了实现Call
接口中的方法外,还新增了newRealCall
,getResponseWithInterceptorChain
两个重要方法。下面会对重要的方法进行讲解。
// 静态方法,返回一个 RealCall 对象
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket);
// 返回被拦截器链处理过的响应报文
Response getResponseWithInterceptorChain() throws IOException;
newRealCall-实例化一个RealCall对象
newRealCall
会对实例化一个RealCall
对象,并对client
,transmitter
,originalRequest
,forWebSocket
4个字段赋值初始化。
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.transmitter = new Transmitter(client, call);
return call;
}
execute-同步执行当前请求
execute
会开始同步执行当前的请求,并进行调用超时检测,即在 OkHttpClient 中设置的callTimeout
。调用超时包含的过程有:DNS解析,建立连接,写入请求报文,服务器处理,读取响应报文的全过程。如果在规定的超时时间内,这些过程没有完成,则认为是调用超时。
@Override public Response execute() throws IOException {
synchronized (this) { // 加同步锁,将 executed 置为 true
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
// 进入调用超时检测
transmitter.timeoutEnter();
// 请求开始事件回调
transmitter.callStart();
try {
client.dispatcher().executed(this);
// 同步执行本次请求,获取响应结果
return getResponseWithInterceptorChain();
} finally {
// 结束本次请求
client.dispatcher().finished(this);
}
}
同步执行并不会开启一个线程去执行请求任务。如果你在主线程调用execute
方法执行一个请求,那么主线程会一直阻塞等待直到结果返回。
enqueue-异步执行当前请求
enqueue
是异步执行,请求会被线程池中的某个线程执行。该方法需要传入Callback
类型的参数,响应结果将会在Callback
接口的函数中回调,回调函数同样是在子线程中执行,所以不要在Callback
的onFailure
和onResponse
函数中对 UI 进行操作。
@Override public void enqueue(Callback responseCallback) {
synchronized (this) { // 加同步锁,将 executed 置为 true
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
// 请求开始事件回调
transmitter.callStart();
// 异步执行本次请求,
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
getResponseWithInterceptorChain- 获取经拦截器链处理过的响应报文
无论是同步请求还是异步请求,最终都会调用getResponseWithInterceptorChain
方法来执行网络请求任务。这说明在 OkHttp 中任何一个请求都必须经过拦截器链的处理。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
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) {
// 第五步,若不是 WebSocket,则添加用户自定义的网络拦截器
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());
boolean calledNoMoreExchanges = false;
try {
// 让拦截器链开始工作,返回响应结果
Response response = chain.proceed(originalRequest);
if (transmitter.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if (!calledNoMoreExchanges) {
transmitter.noMoreExchanges(null);
}
}
}
可以清晰的看见,一个请求会先后经过自定义应用拦截器->失败重试重定向拦截器->桥接拦截器->缓存拦截器->连接拦截器->自定义网络拦截器->调用拦截器,最终发送到服务器。而响应结果经过的拦截器,刚好与上面的顺序相反,是自定义网络拦截器->连接拦截器->缓存拦截器->桥接拦截器->失败重试重定向拦截器->自定义应用拦截器。
Dispatcher 类的设计-异步请求的调度员
Dispatcher
类设计了一个线程池,负责管理所有异步请求的执行,注意它并不负责同步请求的执行,在前面的execute
同步请求方法中我们已经看见它直接执行了getResponseWithInterceptorChain
方法来获取响应结果,并没有放在一个线程中去运行。
Dispatcher 成员变量
Dispatcher
是个“调度员”,默认情况下最多可以同时执行 64 个异步请求任务,并且最多允许 5 个请求同时访问同一个主机。在一个异步或同步请求结果返回后,Dispatcher
会检查还有没有请求在执行,若没有请求任务了,Dispatcher
会去执行一个名为idleCallback
的空闲任务,我们可以使用setIdleCallback
方法来设置具体的空闲任务。同步请求和异步请求都使用双端队列保存。
// 默认最多可以同时执行 64 个请求
private int maxRequests = 64;
// 默认最多允许 5 个请求同时访问同一个主机
private int maxRequestsPerHost = 5;
// 没有请求任务时(所有请求执行完毕),Dispatcher 执行的空闲任务
private @Nullable Runnable idleCallback;
// 线程池
private @Nullable ExecutorService executorService;
// 准备被执行的异步请求
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
// 正在被执行的异步请求
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
// 正在执行的同步请求
// 再次说明, Dispatcher 只负责保存和取消同步请求任务,并不会执行它
// 因为在调用 execute 方法后,同步任务就已经开始执行了
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
Dispatcher 成员方法
Dispatcher
类可以用来执行保存在队列中的异步任务,并可以随时取消执行。
// 创建线程池
public synchronized ExecutorService executorService();
// 设置能同时执行的最大请求数
public void setMaxRequests(int maxRequests);
// 设置对每个主机的最大请求数
public void setMaxRequestsPerHost(int maxRequestsPerHost);
// 设置空闲任务
public synchronized void setIdleCallback(@Nullable Runnable idleCallback);
// 添加异步请求任务到队列中并准备执行
void enqueue(AsyncCall call);
// 在异步请求队列中查找是否已存在对目标主机 host 的请求
@Nullable private AsyncCall findExistingCallWithHost(String host);
// 取消所有请求的执行,包括同步和异步请求
public synchronized void cancelAll();
// 执行请求
private boolean promoteAndExecute();
// 添加同步请求任务到队列中
synchronized void executed(RealCall call);
// 请求任务完成
private <T> void finished(Deque<T> calls, T call);
// 返回当前等待被执行的请求
public synchronized List<Call> queuedCalls();
// 返回当前正在执行的请求
public synchronized List<Call> runningCalls();
promoteAndExecute-将异步任务放到线程池中执行
RealCall
的enqueue
方法最终会调用到promoteAndExecute
,这个方法会将readyAsyncCalls
队列中的异步任务移动到runningAsyncCalls
中,然后将这些任务放到executorService
线程池中执行。
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
// 将任务从 readyAsyncCalls 移动到 runningAsyncCalls
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
// 若正在执行的异步任务数 >= maxRequests,退出循环
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
// 若对当前主机的请求数 >= maxRequestsPerHost,刚请求本次不执行
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 方法
asyncCall.executeOn(executorService());
}
return isRunning;
}
AsyncCall 类的设计-执行异步请求的真正人
在promoteAndExecute
方法我们看到最后的异步请求任务会交给AsyncCall
。AsyncCall
是RealCall
的一个内部类,它继承自NamedRunnable
,而NamedRunnable
实现了Runnable
接口,所以AsyncCall
可以被线程执行。AsyncCall
中的execute
方法正是被线程执行的方法。与RealCall
同步请求的execute
方法对比来说,整体流程基本一致。首先进入超时检测,接着启动拦截器链来处理请求,最后获取响应结果。
@Override protected void execute() {
boolean signalledCallback = false;
// 进入调用超时检测
transmitter.timeoutEnter();
try {
// 经拦截器链处理后,获取响应结果
Response response = getResponseWithInterceptorChain();
signalledCallback = true;
// 请求成功回调 onResponse 方法
responseCallback.onResponse(RealCall.this, response);
} catch (IOException e) {
// 出现 IO 异常
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
// 请求失败,回调 onFailure
responseCallback.onFailure(RealCall.this, e);
}
} catch (Throwable t) {
// 出现其他异常
cancel();
if (!signalledCallback) {
IOException canceledException = new IOException("canceled due to " + t);
canceledException.addSuppressed(t);
// 请求失败,回调 onFailure
responseCallback.onFailure(RealCall.this, canceledException);
}
throw t;
} finally {
// 告诉 Dispatcher 本次请求任务结束,你可以去执行其他待执行的任务或空闲任务
client.dispatcher().finished(this);
}
}
总结
本文介绍了 OkHttp 中与 Http 协议相关的类,异步请求任务的调度员Dispatcher
,执行同步请求任务的RealCall
,执行异步请求任务的AsyncCall
。相信通过本文的分析,大家对 OkHttp 这个框架的工作流程有了基本的了解。本文所讲解的 OkHttp 源码侧重于一个网络请求的全过程,还有很多未涉及的地方,如拦截器、连接池、缓存等并没有深入分析他们的源码和应用,对于 OkHttp 更深一点的问题,后面我会写新的文章讲解,欢迎大家持续关注。
写在最后
如果你对我感兴趣,请移步到 blogss.cn ,或关注公众号:程序员小北,进一步了解。
- 如果本文帮助到了你,欢迎点赞和关注,这是我持续创作的动力 ❤️
- 由于作者水平有限,文中如果有错误,欢迎在评论区指正 ✔️
- 本文首发于掘金,未经许可禁止转载 ©️