【网络】OkHttp的设计理念和原理

本文介绍了流行框架okhttp的设计理念和实现原理
OkHttp 是一个由 Square 公司开发的、功能强大且高效的 HTTP 客户端库,广泛用于 Java 和 Android 应用中进行网络请求。它被认为是 Android 生态系统中进行网络通信的首选库,许多其他高级网络库(如 Retrofit)都是基于 OkHttp 构建的。
之前的网络请求
OkHttp 通过其高效的连接管理、强大的拦截器机制和对现代 HTTP 协议的支持,极大地简化了 Android 网络通信的开发。OkHttp出来之后,可以说是迅速抢占了大部分市场,在之后,更是成为了Android应用网络请求的标准范例。
我记得刚开始学习Android时,使用的还是原生的 HttpURLConnection 。在使用 HttpURLConnection 的时候,我们需要 手动管理连接、输入输出流、设置请求头、处理响应 等。这一过程繁琐且容易出错,特别是在处理复杂的网络请求时。
public class HttpUtil {
public static String sendGetRequest(String url) {
HttpURLConnection connection = null;
BufferedReader reader = null;
String result = null;
try {
// 打开连接
URL requestUrl = new URL(url);
connection = (HttpURLConnection) requestUrl.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
// 读取响应
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
result = response.toString();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭连接和流
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
}
一次完整的网络请求流程
在安卓应用中完成一次 HTTP 请求与响应的完整流程,会涉及多个网络分层,下面按照 TCP/IP 四层模型,结合 OSI 模型对应层次来详细介绍各层在请求和响应阶段完成的工作。
请求过程
- 应用层 ,开发者使用 HTTP 客户端构建 HTTP 请求,指定请求方法(GET、POST 等)、URL、请求头和请求体等信息。按照 HTTP 规范组织请求数据,生成符合格式的 HTTP 请求报文,之后将请求报文传递给传输层。
- 传输层 ,首先建立连接,客户端和服务器通过三次握手建立 TCP 连接。客户端发送 SYN 报文,服务器返回 SYN + ACK 报文,客户端再发送 ACK 报文完成连接建立。连接连理之后,将应用层的 HTTP 请求报文分割成合适大小的报文段,并为每个报文段添加 TCP 首部(包含源端口、目标端口、序列号等信息)。通过序列号和确认应答机制保证数据可靠传输,若发送的报文段未收到确认应答,会进行重传。
- 网络层 ,基于 IP 协议,将传输层的 TCP 报文段封装成 IP 数据报,添加 IP 首部(包含源 IP 地址和目标 IP 地址),根据目标 IP 地址进行路由选择,确定数据报从客户端到服务器的传输路径。将目标 IP 地址解析为对应的 MAC 地址,以便数据在链路层传输。
- 数据链路层 ,将网络层的 IP 数据报封装成帧,添加帧首部(包含源 MAC 地址和目标 MAC 地址)和帧尾部(包含校验信息)。控制设备对物理介质的访问,避免数据冲突。例如,以太网使用 CSMA/CD(载波监听多路访问/冲突检测)协议。通过物理介质(如 Wi-Fi、移动网络)将帧发送到下一个节点。
- 物理层 ,信号转换,将数据链路层的二进制数据转换为物理信号(如电信号、光信号),通过物理介质进行传输。
响应过程
- 物理层 ,在接收端,将物理信号转换为二进制数据,传递给数据链路层。
- 数据链路层 ,接收物理层传来的帧,检查帧的校验信息,若校验正确,则去掉帧首部和帧尾部,将 IP 数据报传递给网络层。
- 网络层 ,接收数据链路层传来的 IP 数据报,检查 IP 首部信息,若目标 IP 地址是本机,则去掉 IP 首部,将 TCP 报文段传递给传输层。ICMP 协议,在网络出现错误(如网络不可达、超时等)时,发送错误报告和控制消息。
- 传输层 ,接收TCP的响应报文段,对报文段进行排序和重组。向服务器发送确认应答,告知服务器数据已成功接收。数据传输完成后,客户端和服务器通过四次挥手断开 TCP 连接。
- 应用层 ,接收传输层传来的 HTTP 响应报文,解析响应状态码、响应头和响应体。从 HTTP 客户端获取解析后的响应数据,根据业务逻辑进行处理。
OkHttp设计理念和核心特性
OkHttp 的设计目标是让 HTTP 网络请求 更快、更稳定、更易用 ,并且提供丰富的可配置性。
高效的网络传输
连接池 (Connection Pooling) 设计。OkHttp 维护一个连接池,对同一主机的多个请求可以 复用已建立的 TCP 连接 。这大大减少了连接建立和断开的开销,尤其是在进行大量小请求时,能显著提高性能。
OkHttp 完全支持 HTTP/2协议 。HTTP/2 允许多个请求和响应在单个 TCP 连接上进行 多路复用 ,解决了 HTTP/1.x 中队头阻塞(Head-of-Line Blocking)的问题,进一步提高了并发性和效率。
OkHttp 默认支持透明的 GZIP 压缩。当服务器返回 GZIP 压缩的数据时,OkHttp 会自动解压,减少了传输的数据量,节省了带宽。
OkHttp 支持响应缓存,可以将服务器返回的响应缓存到本地磁盘。对于重复的请求,如果缓存有效,OkHttp 可以直接从缓存中读取数据,而无需再次进行网络请求,从而加速响应并减少网络流量。
OkHttp 还能够通过拦截器自动处理常见的网络问题,如连接失败的重试和 HTTP 重定向 (3xx 状态码)。这使得网络请求更加健壮。
简洁易用的 API
链式构建器 (Builder Pattern) 的设计:OkHttp 使用构建器模式来配置 OkHttpClient 和 Request 对象,使得 API 调用非常流畅和直观。
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("https://api.example.com/data")
.header("User-Agent", "OkHttp Demo")
.get() // GET, POST, PUT, DELETE, PATCH
.build();
OkHttp 同时支持同步和异步请求。
- 同步请求:通过
client.newCall(request).execute()方法执行,会阻塞当前线程直到收到响应。注意:在 Android 主线程中严禁执行同步网络请求,会引发 ANR (Application Not Responding)! - 异步请求:通过
client.newCall(request).enqueue(callback)方法执行,请求在后台线程进行,结果通过回调接口Callback返回到指定线程(通常是主线程)。
强大的可扩展性:拦截器 (Interceptors)
Interceptors 拦截器是 OkHttp 最强大的特性之一,它基于责任链设计模式。你可以在请求发送前和响应接收后插入自定义的逻辑。
主要在以下方面应用:
- 日志记录 (Logging):打印请求和响应的详细信息,方便调试。
- 身份验证 (Authentication):自动添加认证头,如 OAuth token。
- 离线缓存 (Offline Caching):在没有网络时从缓存中获取数据。
- 重试机制 (Retry Logic):自定义失败请求的重试策略。
- 参数添加/修改:统一添加公共参数或修改请求头。
- 数据压缩/加密:对请求或响应数据进行额外的处理。
拦截器主要分为两种:
- 应用拦截器 (Application Interceptors):通过
addInterceptor()添加。它们运行在网络请求之前,并且在重定向、缓存和重试操作之间只调用一次。适用于应用级别的逻辑。 - 网络拦截器 (Network Interceptors):通过
addNetworkInterceptor()添加。它们直接操作网络请求,可以观察到底层的网络连接、重定向和重试。适用于监控网络行为或底层协议的修改。
与 Okio 的紧密集成
OkHttp 的底层 I/O 操作是基于 Okio 库实现的。Okio 提供了高效的 Buffer (分段缓冲区) 和 Source/Sink (读写流) 抽象,使得 OkHttp 在处理数据流时能够避免不必要的内存拷贝,从而提高了性能。
安全性
OkHttp 原生支持 HTTPS,并可以配置自定义的 SSLSocketFactory 和 HostnameVerifier 来进行证书信任和主机名验证。
证书固定 (Certificate Pinning)特性,允许开发者指定信任的服务器证书,防止中间人攻击。即使根证书被篡改,也能识别出伪造的服务器。
WebSocket 支持
OkHttp 不仅支持传统的 HTTP/HTTPS 请求,还提供了对 WebSocket 的完整支持,用于实现双向、持久的通信。
OkHttp 的架构概览
一个典型的 OkHttp 请求流程通常涉及以下几个关键组件:
OkHttpClient
HTTP 请求的执行者。它是线程安全的。
val client = OkHttpClient.Builder().xxx.build()
由上述调用方式,我们便可以猜出,这里使用了 构建者模式 去配置默认的参数,所以直接去看 OkHttpClient.Builder 支持的参数即可。
需要注意的是,在使用过程中,对于 OkHttpClient 我们还是应该缓存下来或者使用单例模式以便后续复用,因为其相对而言还是比较重,可以充分利用连接池和其他资源。
Request
指客户端发送到服务器的 HTTP请求。在 OkHttp 中,可以使用 Request 对象来构建请求,然后使用 OkHttpClient 对象来发送请求。
通常情况下,一个请求包括了 请求头、请求方法、请求路径、请求参数、url地址 等信息。主要是用来请求服务器返回某些资源,如网页、图片、数据等。
val request = Request.Builder()
.url("https://api.example.com/data") // 请求的 URL
.header("User-Agent", "OkHttp Demo") // 添加请求头
.get() // HTTP 方法,这里 是 GET
.build() // 构建请求对象
Call and Response
当我们使用 OkHttpClient.newCall() 方法时,实际是创建了一个新的 RealCall 对象,它代表了一个准备好执行的 HTTP 请求。用于 应用层与网络层之间的桥梁,用于处理连接、请求、响应以及流 ,其默认构造函数中需要传递 okhttpClient 对象以及 request 。Call 可以同步执行 (execute()) 或异步执行 (enqueue())。
Response是服务器返回的HTTP响应,包含状态码、响应头和响应体。我们从中可以获取服务器返回的数据。
同步执行
调用 execute() 方法开始发起同步请求,该方法内部会将当前的 call 加入我们 Dispatcher 分发器内部的 runningSyncCalls 队列中取,等待被执行。接着调用 getResponseWithInterceptorChain() ,使用拦截器获取本次请求响应的内容,这也即我们接下来要关注的步骤。
异步执行
当我们调用 RealCall.enqueue() 执行异步请求时,会先将本次请求加入 Dispather.readyAsyncCalls 队列中等待执行,如果当前请求是 webSocket 请求,则查找与当前请求是同一个 host 的请求,如果存在一致的请求,则复用先前的请求。
接下来调用 promoteAndExecute() 将所有符合条件可以请求的 Call 从等待队列中添加到 可请求队列 中,再遍历该请求队列,将其添加到 线程池 中去执行。当我们将任务添加到线程池后,当任务被执行时,即触发 run() 方法的调用。该方法中会去调用 getResponseWithInterceptorChain() 从而使用拦截器链获取服务器响应,从而完成本次请求。请求成功后则调用我们开始时的 callback对象的 onResponse() 方法,异常(即失败时)则调用 onFailure() 方法。
Interceptor 链
请求在发送到网络之前,会依次经过一系列的拦截器处理。这些拦截器可以修改请求、处理缓存、记录日志等。响应返回时,也会逆序经过拦截器链。
val client = OkHttpClient.Builder()
.addInterceptor { chain -> // 添加一个简单的日志拦截器
val request = chain.request()
println("Sending request: ${request.url}")
val response = chain.proceed(request)
println("Received response for: ${response.request.url} with code: ${response.code}")
response
}
.build()
ConnectionPool
连接池,用于管理 TCP 连接的复用。维护一个连接的缓存池,当请求相同主机的资源时,可以重复使用已建立的连接,从而减少连接建立和销毁的开销。
Dispatcher
Dispatcher 是一个线程安全的类,用于管理异步请求的执行。它维护了一个请求队列和一个线程池,用于执行请求。
Dispatcher 中的主要方法有:
- enqueue(RealCall call):将一个 RealCall 对象添加到请求队列中。
- promoteAndExecute():从请求队列中取出符合条件的 Call 对象,并将其添加到线程池中执行。
- cancelAll():取消所有的请求。
- finished(RealCall call):当一个请求完成时,调用该方法。
- runningCallsCount():获取当前正在执行的请求数量。
责任链模式
责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递。

上述逻辑如下:
当 getResponseWithInterceptorChain() 方法内部最终调用 RealInterceptorChain.proceed() 时,内部传入了一个默认的index ,这个 index 就代表了当前要调用的 拦截器item ,并在方法内部每次创建一个新的 RealInterceptorChain 链,index+1,再调用当前拦截器 intercept() 方法时,然后将下一个链传入;
最开始调用的是用户自定义的 普通拦截器,如果上述我们添加了一个 CustomLogInterceptor 的拦截器,当获取 response 时,我们需要调用 Interceptor.Chain.proceed() ,而此时的 chain 正是下一个拦截器对应的 RealInterceptorChain;
上述流程里,index从0开始,以此类推,一直到链条末尾,即 拦截器集合长度-1处;
当遇到最后一个拦截器 CallServerInterceptor 时,此时因为已经是最后一个拦截器,链条肯定要结束了,所以其内部肯定也不会调用 proceed() 方法。
相应的,为什么我们在前面说 它 是真正执行与服务器建立实际通讯的拦截器?
因为这个里会获取与服务器通讯的 response ,即最初响应结果,然后将其返回上一个拦截器,即我们的网络拦截器,再接着又向上返回,最终返回到我们的普通拦截器处,从而完成整个链路的路由。
常用拦截器
RetryAndFollowUpInterceptor
这是 OkHttp 中的一个拦截器,用于处理 HTTP 重定向和重试。当服务器返回一个 3xx 状态码时,该拦截器会根据服务器的指示进行重定向。如果服务器返回一个 4xx 或 5xx 状态码,该拦截器会根据配置的重试策略进行重试。即用于 请求失败 的 重试 工作以及 重定向 的后续请求工作,同时还会对 连接 做一些初始化工作。
BridgeInterceptor
这是 客户端和服务器 之间的沟通 桥梁 ,负责将用户构建的请求转换为服务器需要的请求。比如添加 content-type、cookie 等,再将服务器返回的 response 做一些处理,转换为客户端所需要的 response,比如移除 Content-Encoding 等。
CacheInterceptor
管理缓存相关,比如 读取缓存、写入缓存 等。开发者可以通过 OkHttpClient.cache() 方法来配置缓存,在底层的实现处,缓存拦截器通过 CacheStrategy 来判断是使用网络还是缓存来构建 response。具体的 cache 策略采用的是 DiskLruCache 。
具体如下:

ConnectInterceptor
负责 建立连接、发送请求、接收响应 等。在底层的实现处,连接拦截器通过 StreamAllocation 来管理连接的复用,通过 RealConnection 来管理连接的生命周期。
CallServerInterceptor
负责 发送请求、接收响应 等。在底层的实现处,调用服务器拦截器通过 HttpCodec 来管理连接的读写流,通过 RealConnection 来管理连接的生命周期。
示例用法(Kotlin)
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Callback
import okhttp3.Response
import java.io.IOException
import java.util.concurrent.TimeUnit
fun main() {
// 1. 创建 OkHttpClient 实例
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // 连接超时
.readTimeout(30, TimeUnit.SECONDS) // 读取超时
.addInterceptor { chain -> // 添加一个简单的日志拦截器
val request = chain.request()
println("Sending request: ${request.url}")
val response = chain.proceed(request)
println("Received response for: ${response.request.url} with code: ${response.code}")
response
}
.build()
// 2. 构建 Request 对象
val request = Request.Builder()
.url("https://api.github.com/users/octocat") // 请求的 URL
.get() // HTTP 方法,这里是 GET
.header("User-Agent", "OkHttp-Example/1.0") // 添加请求头
.build()
// 3. 执行异步请求
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: okhttp3.Call, e: IOException) {
println("Request failed: ${e.message}")
e.printStackTrace()
}
override fun onResponse(call: okhttp3.Call, response: Response) {
response.use { // 确保响应体被关闭
if (!response.isSuccessful) {
println("Request failed with code: ${response.code}")
return
}
val responseBody = response.body?.string()
println("Response Body: $responseBody")
}
}
})
// 为了让主线程不立即退出,给异步请求一些时间
Thread.sleep(5000)
}
在 Android 中使用 OkHttp:
虽然上述示例是通用的 Kotlin 代码,但在 Android 应用中,你需要注意以下几点:
- 权限:在
AndroidManifest.xml中添加INTERNET权限。 - 异步处理:务必在后台线程执行网络请求。如果使用同步
execute()方法,必须在Thread、AsyncTask(不推荐)、ExecutorService或 Kotlin 协程 (Dispatchers.IO) 中调用。使用enqueue()方法则会将回调放到线程池中执行,你可以利用Handler或协程将结果切换回主线程更新 UI。 - 单例模式:推荐将
OkHttpClient创建为单例,并在整个应用中复用,以最大化连接池的效益。 - 与 Retrofit 结合:对于更复杂的 RESTful API 交互,通常会将 OkHttp 与 Retrofit 结合使用。Retrofit 提供了一个声明式的 API 来定义网络请求,并利用 OkHttp 作为底层的 HTTP 引擎。
Okio部分
Okio 是 Square 公司(也是 OkHttp 的开发者)开源的一个 I/O 库,它旨在补充和改进 Java 平台原生的 java.io 和 java.nio API,使其在处理数据访问、存储和处理时更易用、更高效。Okio 的设计理念可以用以下几个核心点来概括。
1. 统一和简化 I/O API
Java 原生的 java.io 和 java.nio 在处理流式数据时,API 有些复杂和零散。例如,InputStream 和 OutputStream 是基于字节的,而 Reader 和 Writer 是基于字符的,它们的错误处理和缓冲区管理方式各不相同。java.nio 虽然提供了非阻塞 I/O,但使用起来更繁琐,需要手动管理 ByteBuffer。
为此,Okio 引入了两个核心接口:
Source(源): 用于读取数据,类似于InputStream。Sink(槽): 用于写入数据,类似于OutputStream。
这两个接口提供了一个统一的、直观的 API 来处理字节流,无论数据是来自文件、网络还是内存。
例如,使用 Okio 的 Source 和 Sink 接口,从电脑上读取一个文件的流程如下:
// 1. 创建一个文件输入流
File file = new File("path/to/file");
FileInputStream fis = new FileInputStream(file);
// 2. 将文件输入流包装为 Okio 的 Source
Source source = Okio.source(fis);
// 3. 读取数据
try (BufferedSource bufferedSource = Okio.buffer(source)) {
String data = bufferedSource.readUtf8(); // 读取 UTF-8 编码的字符串
System.out.println("File content: " + data);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4. 确保资源被关闭
source.close();
}
// 5. 关闭文件输入流
fis.close();
2. 强大的缓冲区 (Buffer)
在传统的 Java I/O 中,频繁的小读写操作会导致大量的系统调用和内存分配,性能较低。开发者需要手动管理 byte[] 缓冲区,容易出错。
Okio 的解决方案:
Buffer 类: 这是 Okio 的核心,一个可变、动态大小的字节序列,类似 ByteArrayOutputStream 但更高效。
分段缓冲区: Buffer 不是一个连续的 byte[],而是由一个双向链表连接的多个小段 (Segment) 组成。每个 Segment 是一个固定大小的 byte[]。这种设计带来了显著的优势:
避免内存拷贝: 当数据从一个 Source 读入 Buffer,或者从 Buffer 写入 Sink 时,通常只需要在 Segment 之间进行引用传递,而不是复制整个字节数组。这大大减少了内存拷贝,提升了性能。
高效的数据追加和移除: 链表结构使得在 Buffer 的头部或尾部添加/移除数据非常高效,而不需要移动整个数组。
内存池(内部机制): Segment 可以被回收并重复利用,减少了垃圾回收的压力,尤其是在频繁进行 I/O 操作的场景下。
3. 可组合性和可扩展性
传统的 I/O 操作往往是线性的,很难在中间插入额外的处理逻辑(如压缩、加密、哈希)。
Okio 的解决方案:
装饰器模式: Okio 的 Source 和 Sink 设计允许你轻松地将它们包装(decorate)起来,形成一个处理链。例如,你可以将一个 GzipSource 包装在一个文件 Source 上,或者将一个 HashingSink 包装在一个网络 Sink 上。
// 读取一个经过 Gzip 压缩的文件
Source fileSource = Okio.source(new File("compressed.gz"));
Source gzipSource = new GzipSource(fileSource);
BufferedSource bufferedSource = Okio.buffer(gzipSource);
String data = bufferedSource.readUtf8();
// 写入数据并计算 SHA-256 校验和
Sink fileSink = Okio.sink(new File("output.txt"));
HashingSink hashingSink = HashingSink.sha256(fileSink);
BufferedSink bufferedSink = Okio.buffer(hashingSink);
bufferedSink.writeUtf8("Hello, Okio!");
bufferedSink.close();
ByteString hash = hashingSink.hash();
这种设计使得 I/O 转换(如压缩、加密、编码解码、哈希)变得非常模块化和可插拔。
4. 易于测试
由于 Buffer 和 Source/Sink 是纯 Java/Kotlin 对象,它们可以在没有任何文件系统或网络依赖的情况下进行单元测试。这使得 I/O 相关的业务逻辑更容易测试。
总结
Okio 的核心设计理念可以归结为:提供一个更强大、更高效、更易用的 I/O 抽象层,以解决 Java 原生 java.io 和 java.nio 的痛点。 它通过引入 Buffer(分段缓冲区)、Source 和 Sink 的统一接口以及强大的装饰器模式,实现了高性能、可组合、易于测试的 I/O 操作。它的目标是让开发者能够更专注于业务逻辑,而不是底层繁琐的 I/O 细节和性能优化。