likes
comments
collection
share

OkHttp源码分析

作者站长头像
站长
· 阅读数 11

1. OkHttp简介

OkHttp 是一个默认高效的 HTTP 客户端

  • HTTP/2 支持允许对同一主机的所有请求共享一个套接字;
  • 连接池减少了请求延迟(如果 HTTP/2 不可用);
  • 透明 GZIP 缩小了下载大小;
  • 响应缓存完全避免了网络重复请求; 当网络出现问题时,OkHttp 坚持不懈:它会默默地从常见的连接问题中恢复。如果您的服务有多个 IP 地址,如果第一次连接失败,OkHttp 将尝试备用地址。这对于 IPv4+IPv6 和冗余数据中心中托管的服务是必要的。 OkHttp 支持现代 TLS 功能(TLS 1.3、ALPN、证书固定)。它可以配置为回退以实现广泛的连接。 使用 OkHttp 很容易。它的请求/响应 API 设计有流畅的构建器和不变性。它支持同步阻塞调用和带有回调的异步调用。

2.url基本知识

2.1 url基本组成

http://www.example.com:8080/path/to/resource?param1=value1&param2=value2#fragment

组成含义介绍
http://协议(Scheme)URL 的协议部分,通常是 httphttpsftp 等。协议部分以冒号和两个斜杠开头(例如:http://
www.example.com主机表示资源所在的主机或服务器的域名或 IP 地址。主机部分紧随协议部分,以冒号或斜杠分隔
8080端口号(Port)表示服务器正在监听的端口号。端口号是可选的,如果未指定,默认使用协议的默认端口(例如,http 默认端口是 80,https 默认端口是 443)
path/to/resource路径(Path)表示服务器上资源的路径。路径部分紧随主机部分,以斜杠开头。它可以是一个层次结构,用斜杠分隔不同的目录或文件
param1=value1&param2=value2查询参数包含在 URL 中,用于向服务器传递参数。查询参数以问号 ? 开头,多个参数之间使用 & 符号分隔
fragment片段标识符(Fragment)表示资源中的一个特定片段或位置。片段标识符以井号 # 开头

2.2 HTTP请求的基本链路

  • 整个过程中,DNS 解析、建立 TCP 连接、发送请求、服务器处理请求和发送响应、接收响应等环节都涉及到协议层和网络层的操作。HTTP 协议定义了请求和响应的格式,而 TCP/IP 协议栈提供了底层的网络传输支持
  1. DNS 解析: 客户端首先通过 DNS 解析获取服务器的 IP 地址,将主机名转换为 IP 地址
  2. 建立 TCP 连接: 客户端通过 IP 地址和端口号与服务器建立 TCP 连接。这个过程通常包括 TCP 的三次握手
  3. 组装HTTP 请求: 客户端构建 HTTP 请求报文,包括请求行(方法、URL、协议版本)、请求头(Headers)、请求体(如果是 POST 请求等带有主体的请求)等
  4. 发送请求: 客户端将构建好的 HTTP 请求报文通过建立的 TCP 连接发送给服务器
  5. 服务器处理请求: 服务器收到请求后,根据请求报文中的信息进行相应的处理。这可能包括解析请求、执行请求对应的处理逻辑、生成响应内容等
  6. 服务器发送响应: 服务器构建 HTTP 响应报文,包括状态行(协议版本、状态码、状态消息)、响应头(Headers)、响应体(实际的响应内容)等
  7. 接收响应: 客户端通过 TCP 连接接收服务器发送的 HTTP 响应报文
  8. 解析响应: 客户端解析响应报文,获取状态码、响应头和响应体等信息
  9. 关闭连接: 根据 HTTP 版本和服务器端的要求,决定是否保持连接或断开连接。在 HTTP/1.1 中,默认是保持连接,而在 HTTP/1.0 中默认是断开连接
  10. 处理响应: 客户端根据收到的响应进行相应的处理,可能包括渲染页面、执行后续操作等

3. Get请求

3.1 如果我们使用Socket不使用框架,代码流程如下:

  • 代码思路理解 我们可以把网络请求比喻成西游记取经:
    1. 请求和发起者: 李世民发起了请求,类似于客户端向服务器发送请求。域名是灵山,路径和参数相当于藏经阁
    2. 灵山和服务器: 灵山可以被看作是服务器,它负责响应请求并提供所需的资源(经书)。服务器是存储和处理信息的地方,就像灵山中的经书
    3. 师徒四人和数据传输: 师徒四人可以被视为在请求和响应之间传递数据的途径。他们是信息的载体,负责将经书从灵山带回到东土大唐,就像数据在网络中传输
fun main() {
    try {
        // 获取系统信任的证书
        val trustManagerFactory = javax.net.ssl.TrustManagerFactory.getInstance(
            javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()
        )
        trustManagerFactory.init(null)

        val systemTrustedCerts =
            (trustManagerFactory.trustManagers[0] as javax.net.ssl.X509TrustManager).acceptedIssuers

        val trustManager = object : javax.net.ssl.X509TrustManager {
            override fun checkClientTrusted(x509Certificates: Array<X509Certificate>, s: String) {
                // Do nothing, just accept any client certificates
            }

            override fun checkServerTrusted(x509Certificates: Array<X509Certificate>, s: String) {
                try {
                    // 在这里进行自定义的证书验证逻辑
                    val expectedCertHash = "sha256/expected_hash_value"

                    val serverCert = x509Certificates[0] // 假设服务器只返回一个证书
                    val sha256 = MessageDigest.getInstance("SHA-256")
                    val encodedCert = serverCert.encoded
                    val hashedCert = sha256.digest(encodedCert)
                    val certHash = "sha256/" + java.util.Base64.getEncoder().encodeToString(hashedCert)

                    // 检查服务器证书的哈希值是否与预期值匹配
                    if (serverCert != null && expectedCertHash != certHash) {
                        println("Server certificate does not match expected hash.")
                    }
                } catch (e: NoSuchAlgorithmException) {
                    throw RuntimeException(e)
                }
            }

            override fun getAcceptedIssuers(): Array<X509Certificate> {
                return systemTrustedCerts
            }
        }

        // 创建普通Socket连接
        val socket = Socket("example.com", 443)

        // 创建SSLSocket
        val sslContext = javax.net.ssl.SSLContext.getInstance("TLS")
        sslContext.init(null, arrayOf(trustManager), null)

        val sslSocketFactory = javax.net.ssl.SSLSocketFactory.getDefault() as javax.net.ssl.SSLSocketFactory

        val sslSocket = sslSocketFactory.createSocket(
            socket, "example.com", 443, true
        ) as javax.net.ssl.SSLSocket

        // 构建GET请求
        val request = "GET /path/to/resource HTTP/1.1\r\n" +
                "Host: example.com\r\n" +
                "Connection: close\r\n\r\n"

        // 发送请求
        val outputStream: OutputStream = sslSocket.getOutputStream()
        outputStream.write(request.toByteArray())
        outputStream.flush()

        // 读取响应
        val reader = BufferedReader(InputStreamReader(sslSocket.getInputStream()))
        val response = StringBuilder()
        var line: String?
        while (reader.readLine().also { line = it } != null) {
            response.append(line).append("\n")
        }
        // 输出响应体
        println(response.toString())
        // 关闭连接
        sslSocket.close()

    } catch (e: Exception) {
        e.printStackTrace()
    }
}

3.2 OKHttp调用流程如下:

  • 创建OkHttpClient对象
  • 组装Request请求对象
  • 得到返回结果 从这里看出OKHttp已经对于Http请求链路做了全面的封装,我们不需要关心上面任何一个流程,就可以完成一次正常的请求,我们就看看框架是如何完成Http请求流程的
public static void main(String... args) throws Exception {
  OkHttpClient client = new OkHttpClient();

  // Create request for remote resource.
  Request request = new Request.Builder()
      .url(ENDPOINT)
      .build();

  // Execute the request and retrieve the response.
  try (Response response = client.newCall(request).execute()) {
    // Deserialize HTTP response to concrete type.
    ResponseBody body = response.body();
    List<Contributor> contributors = CONTRIBUTORS_JSON_ADAPTER.fromJson(body.source());

    // Sort list by the most contributions.
    Collections.sort(contributors, (c1, c2) -> c2.contributions - c1.contributions);

    // Output list of contributors.
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + ": " + contributor.contributions);
    }
  }
}

从代码量来看,OkHttp确实完胜Socket调用,确实方便了很多,我们完全看不到Socket的影子

4. 源码分析

4.1 请求调度

OkHttp提供了两种方式一个是同步excute 另一个是异步 enqueue,请求调度是针对异步方式

OkHttp源码分析

  • 其实查看这部分异步逻辑非常简单,利用了线程池来实现此功能。但是代码加了很多限制
  1. 同步excute 和异步 enqueue方法都加了原子操作,不允许同一个对象多次请求
  2. (方法7)如果请求池的数量大于maxRequests,直接拒绝当次请求,继续在请求池中等待
  3. (方法6)中如果判断这次请求的Host,跟请求池里面的Host一样。(方法7)如果请求池里面有maxRequestsPerHost个那么就不允许请求,而是等待,这样的限制对于控制对同一主机的请求并发数非常重要,可以帮助维护系统的稳定性和性能
  4. Dispatcher中的线程池中最大支持数是Int.MAX_VALUE,限制是最大并发数通过全局变量maxRequests,这样做的好处是可以动态控制并发数,根据当前系统的负载来调度,以便达到最佳体验

4.2 请求业务实现逻辑

  • OkHttp采用了拦截器模式,允许开发人员在请求处理的不同阶段插入自定义的处理逻辑。这种模式的目标是允许在不修改核心处理逻辑的情况下,通过添加或更改拦截器来修改或扩展系统的行为。这边就很类似于 面向切面编程(AOP) ,每一个拦截器相当于切面,而切点规则(Pointcut) 相当于匹配所有的intercept方法,通知(Advice)inercept方法体,正序增强了request,逆序增强了reponse

OkHttp源码分析

拦截器介绍
client.interceptors客户端自定义拦截器
RetryAndFollowUpInterceptor处理请求的重试和重定向的拦截器,在请求失败时负责决定是否进行重试,并在重定向时更新请求的 URL
BridgeInterceptor主要用于桥接连接,将用户的请求转换为网络请求,以及将网络响应转换为用户响应,处理一些请求头的添加,以及响应头的处理
CacheInterceptor缓存拦截器,用于处理缓存相关的逻辑,在发起请求前,检查是否有缓存可用,如果有则返回缓存的响应;在获得网络响应后,将响应缓存起来
ConnectInterceptor处理与服务器建立连接的拦截器,负责发起 TCP 连接、进行 TLS 握手等工作
CallServerInterceptor处理与服务器进行通信的拦截器,发送请求到服务器,并获取服务器的响应,然后将响应传递给上层的拦截器

4.3 RetryAndFollowUpInterceptor

  • 重试逻辑: 当一个请求发生失败时,拦截器判断是否可以进行重试。它会考虑请求是否允许重试、是否达到最大重试次数等因素,并在需要时创建新的请求进行重试

  • 跟进逻辑: 在某些情况下,服务器可能返回重定向或其他需要客户端跟进的状态码。RetryAndFollowUpInterceptor 会负责创建新的请求,以便进行跟进操作。这确保了在需要时客户端能够正确地处理服务器端的跳转要求

  • 错误码封装: 拦截器还负责对一些 response 的错误码进行二次封装。这可能涉及到将某些错误转换为特定的异常或错误类型,以便更好地向调用端传递信息

OkHttp源码分析

4.4 BridgeInterceptor

  • 用于在网络请求和网络响应之间桥接转换数据。它主要负责将用户构建的请求转换为底层网络层可以理解的请求,并将从网络层接收到的响应转换为用户友好的响应
HTTP请求头含义介绍
Content-Type指定请求体的媒体类型通常包括两部分:媒体类型(Media Type): 表示请求体的数据类型,比如文本、图像、音频、视频等;字符集(Charset): 可选的参数,表示请求体中文本数据的字符编码方式
Content-Length指定请求体的长度(以字节为单位)准确的长度信息: Content-Lengthh 头部允许接收端准确地知道消息体的长度,从而能够准确地解析和处理请求。没有这个头部,服务器可能需要不断地读取数据,直到检测到连接关闭,才能确定消息体的结束性能优化: 如果服务器知道消息体的长度,它可以更有效地分配和管理资源,而无需等待完整的消息体。这在处理大文件或流式传输时尤为重要校验完整性: 通过 Content-Length,服务器可以校验接收到的消息体是否与发送端声明的长度相匹配,从而确保消息体的完整性。这有助于防止意外的消息截断或过长的情况
Transfer-EncodingHttp/1用于指示消息主体的传输编码方式在 HTTP/2 中不再使用 Transfer-Encoding,而是通过帧的方式进行数据传输。如果你要传输二进制数据,可以考虑使用 multipart/form-data 格式
Host标识请求的目标服务器和端口号Host 请求头的格式通常是 hostname:port,其中 hostname 是主机名,port 是端口号
Connection用于控制是否在当前连接上保持持久性(keep-alive)的标准 HTTP 请求头通常使用默认的 keep-alive,以便在同一连接上发送多个请求,减少连接建立和关闭的开销。当然,这也取决于服务器和客户端是否支持持久性连接
Accept-Encoding用于指示客户端可以接受的内容编码(压缩算法)。服务器可以使用这个信息来压缩响应体,以减小传输的数据量,提高性能gzip: 使用 GZIP 压缩算法deflate: 使用 DEFLATE 压缩算法br: 使用 Brotli 压缩算法
Cookie用来在客户端和服务器之间传递 HTTP Cookies 的标头是服务器放置在客户端浏览器上的小型文本文件,用于跟踪用户的身份、状态信息以及在不同请求之间保持会话信息
Authorization用于传递身份验证令牌(Token)在用户登录时,服务器会生成一个令牌(Token)并将其发送给客户端。客户端在之后的请求中通过在请求头中携带这个令牌来进行身份验证。令牌可以存储在客户端的本地存储中,也可以在每个请求中动态生成并发送
User-Agent用于标识客户端的应用程序和版本信息不同的移动应用程序通常会设置自己独特的 User-Agent,以便服务器能够识别请求的来源

4.5 CacheInterceptor

  • 处理缓存相关的逻辑。在发起网络请求前,CacheInterceptor会检查是否存在可用的缓存,并在满足条件时直接返回缓存的响应,避免实际的网络请求

  • 同时也负责处理服务器返回的响应,将符合缓存策略的响应存储到缓存中,以备后续使用。这有助于提高应用性能,减少对网络的依赖

    类似下拉刷新,用户信息这类场景,当同一请求在短时间内多次发起时,CacheInterceptor 可以通过命中缓存,避免实际的网络请求,可以有效地节省用户流量和减轻服务器负担

4.5.1 代码设计

  • 设计思路:将请求信息存储到文件中,拦截请求头,通过校验合法性判断是否可以不发起新的请求
  • 设计难点:
  1. 如何提高读写效率
  2. 如何保证文件可靠性完整性
  3. 如何优化缓存

4.5.2 代码调用

  • cache不会主动打开,需要自己主动调用才行
val client = OkHttpClient.Builder()
    .cache(Cache(File(cacheDir, "okhttp_cache"), (10 * 1024 * 1024).toLong()))
    .build()

4.5.3 产物介绍

  • 首次发起GET网络请求,会生成三个文件
文件名介绍
journal这个文件记录了缓存中每个条目的元数据信息。这个文件在初始化缓存时创建,用于跟踪缓存的状态和操作记录
xxx.0这个文件存储了请求体,可以是文本、表单数据等,按照UTF-8编码
xxx.1个文件存储了响应体,响应体文件实际上就是一个包含了响应体数据的二进制文件。这个文件可能存储了一张图片、一个文本文档,或者其他任何类型的数据,具体的内容由服务器返回的响应体决定

OkHttp源码分析

4.5.6 时序

  • 首先要理解作者设计的意图,整体思路就是缓存,然后校验数据合法性,合法就直接返回,不合法就继续请求,同时更新缓存文件。

  • 提高读写效率: 根据方法6,我们会发现在增删改查文件时,都会调用该方法。这个方法就是组装以文件名为键,Entry对象为值。为了提高缓存系统的性能和效率,作者设计了字典表journal,里面存储少量信息,等到需要用到时,才去读取文件,这样大大减少了IO开销

  • 保证文件可靠性完整性:

  • journal 文件

  1. 备份机制:在修改字典表(journal 文件)时,会先将新的数据写入临时文件,然后创建 journal 文件的备份,并将临时文件拷贝到 journal 文件。这种做法确保了在写入新的操作记录时,即使在写入过程中发生异常或中断,也不会损坏原有的 journal 文件,因为备份文件是上一版本的完整副本。
  2. 恢复机制:在 initialize 方法中,会检查是否存在备份文件,如果有备份文件但没有相应的 journal 文件,就会将备份文件拷贝到 journal 文件,以防止数据丢失。这是一种很好的防御性编程,考虑到了异常情况下的容错机制。
  3. 当进行缓存写入journal操作时,首先会记录一条 DIRTY 记录,表示该条目正在编辑状态。在提交(commit)阶段,会检查是否存在对应的请求体和响应体文件,如果不存在,会将该条目从lruEntries中移除,并写入一条 REMOVE 记录。如果存在,则写入一条 CLEAN 记录,其中包含了请求体和响应体的长度信息。这样的设计保证了在写入过程中出现异常或中断时,不会留下不完整或无效的数据。下次操作时,会根据当前的lruEntries重建journal,避免了旧的无效数据的残留
  • 请求体响应体文件
  1. 安全机制:写入请求体和响应体时都采用了先写入临时文件的方式,以确保写入的原子性和安全性。这样可以防止写入过程中的中断或异常情况导致文件数据损坏或不一致。

如何优化缓存:

  1. DiskLruCache采用了LRU(Least Recently Used)的思想。它使用lruEntries这个有序的LinkedHashMap来维护缓存条目的顺序。在构造函数中,我们传入了maxSize参数,表示缓存的最大空间限制。 2.每当缓存的lruEntries发生变化时,会重新计算包字典表完整存储数据的长度总和。 3.在适当的时机调用方法trimToSize,例如进行缓存写入时,我们检查当前大小是否超过了最大空间限制(maxSize)。如果超过了限制,就会触发删除操作。我们遍历lruEntries,循环寻找完整的数据来删除,直到空间满足要求。这样,DiskLruCache通过LRU策略保持缓存大小不超过设定的最大值,实现了高效的缓存管理。 OkHttp源码分析 OkHttp源码分析

4.6 ConnectInterceptor

  • 在 HTTP 的情况下,TCP 连接成功后,可以直接进行数据传输,因为 HTTP 不涉及加密,数据是明文传输的

  • 而在 HTTPS 的情况下,必须在 TCP 连接的基础上建立 SSL/TLS 连接,进行握手过程,确保通信的安全性,然后才能进行数据传输。一旦 SSL/TLS 握手成功,数据就可以在加密的通道中传输,确保在网络中的安全性

  • 从这个拦截器开始,才开始使用socket完成TCP连接,TLS连接和证书校验

  • 代码思路: 我们按照买鞋子来理解:

    1. 购物需求: 你想要购买一双正品的鞋子,我们担心要买到假货我们怎么做
    2. 防伪标签: 类比为服务器的TLS证书。在购物过程开始时,你先查看鞋盒上的防伪标签,这相当于服务器在握手过程中提供的证书
    3. 验证标签的真实性: 你检查防伪标签上的特定特征,确保它是真实的,这对应于客户端对服务器证书的验证过程
    4. 确认是正品: 如果防伪标签是真实有效的,你确认这双鞋子是正品。在TLS握手中,如果服务器证书验证通过,客户端确认与合法的服务器建立了安全连接
    5. 购买业务: 确认商品是正品后,你可以安心进行购买业务。在TLS握手完成后,双方可以进行安全的数据传输

4.6.1 安全检查

  • OKHttp还支持X.509协议证书校验,通过遍历获得X509TrustManagergetAcceptedIssuers传入的证书合集 是否有跟SSLSocket连接服务器传过来的证书 SHA-256哈希值或者SHA-1哈希值一样,则校验通过

  • X509TrustManager还支持进一步安全验证,万一不是X509协议,或者某些异常导致上述校验通过,还可以在checkServerTrusted继续校验

OkHttp源码分析

4.6.2 HTTP/2连接向导

  • 方法16中,HTTP/2 协议规定在建立连接时,客户端和服务器需要相互发送一个特定的前导字符串,以确认对方支持 HTTP/2
方法作用
connectionPreface()发送 HTTP/2 连接前导帧。在建立 HTTP/2 连接时,客户端和服务器端都要发送一个特殊的帧,即连接前导帧,以确认对方支持 HTTP/2 协议
settings(okHttpSettings: Settings)发送 HTTP/2 SETTINGS 帧。SETTINGS 帧用于协商和通知连接的各种配置参数,例如流的最大数量、窗口大小等
windowUpdate(streamId: Int, windowSizeIncrement: Long)发送 HTTP/2 WINDOW_UPDATE 帧。WINDOW_UPDATE 帧用于调整流或连接级别的窗口大小,允许对端发送更多的数据

4.7 CallServerInterceptor

  • 在HTTP/2协议中,通信的基本单位是帧(Frame)。帧是对数据的最小单元划分,每个帧都包含了一些特定类型的信息,如HTTP头部、负载数据等。这种细粒度的划分使得多个请求和响应可以并发交错地在一个连接上进行,在整个HTTP/2连接中,不同流的帧可以并发传输,且顺序可以乱序。这样设计的好处是提高了并发性能,允许多个流同时进行传输,而不必等待一个流完全传输完毕。从而提高了性能

  • 在HTTP/2中,每个流都有一个唯一的stream ID,Header帧和Data帧通过这个stream ID 关联在一起。服务器在接收到Header帧后,会使用stream ID 来识别这个流,并在之后的Data帧中处理相应的数据。

类型作用
DATA帧(TYPE_DATA)用于携带实际的数据负载,比如请求和响应的主体部分
HEADERS帧(TYPE_HEADERS)携带HTTP头部信息,包括请求头和响应头
PRIORITY帧(TYPE_PRIORITY)用于指定帧的优先级,有助于服务器和客户端更有效地处理多个并发流
RST_STREAM帧(TYPE_RST_STREAM)用于终止一个流,表示某个流的处理出现错误或不再需要
SETTINGS帧(TYPE_SETTINGS)用于传递通信参数的配置信息,如最大并发流数量、流的初始窗口大小等
PUSH_PROMISE帧(TYPE_PUSH_PROMISE)用于服务器向客户端推送资源的承诺
PING帧(TYPE_PING)用于检测连接的存活性,通常由一方发送,另一方回复
GOAWAY帧(TYPE_GOAWAY)用于通知对方,表示连接即将关闭,同时指定最后一个有效流的标识符
WINDOW_UPDATE帧(TYPE_WINDOW_UPDATE)用于调整流的窗口大小,以控制流量

OkHttp源码分析

  • 方法17中,调用方法时开启阻塞等待接受响应体
@Synchronized @Throws(IOException::class)
fun takeHeaders(callerIsIdle: Boolean = false): Headers {
  while (headersQueue.isEmpty() && errorCode == null) {
    val doReadTimeout = callerIsIdle || doReadTimeout()
    if (doReadTimeout) {
      readTimeout.enter()
    }
    try {
      waitForIo()
    } finally {
      if (doReadTimeout) {
        readTimeout.exitAndThrowIfTimedOut()
      }
    }
  }
  if (headersQueue.isNotEmpty()) {
    return headersQueue.removeFirst()
  }
  throw errorException ?: StreamResetException(errorCode!!)
}
  • 方法7中,while循环解解析帧,在 HTTP/2 中,DATA 帧的结束标志 END_STREAM 表示该帧是整个数据流的最后一帧,即响应体的结束。在 OkHttp 中,readResponseHeaders 方法通常会等待直到接收到包含 END_STREAMDATA 帧,这样才能确保整个响应体的数据都已经接收完毕。
@Throws(IOException::class)
private fun readData(handler: Handler, length: Int, flags: Int, streamId: Int) {
  if (streamId == 0) throw IOException("PROTOCOL_ERROR: TYPE_DATA streamId == 0")

  // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED
  val inFinished = flags and FLAG_END_STREAM != 0
  val gzipped = flags and FLAG_COMPRESSED != 0
  if (gzipped) {
    throw IOException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA")
  }

  val padding = if (flags and FLAG_PADDED != 0) source.readByte() and 0xff else 0
  val dataLength = lengthWithoutPadding(length, flags, padding)

  handler.data(inFinished, streamId, source, dataLength)
  source.skip(padding.toLong())
}
  • 这时候就拿到了responseBuilder,再通过建造者组装一个reponse对象,至此就拿到了响应体,但是流没有关闭,流关闭时等到你业务处理拿到对象,或者调用string方法时,会调用close方法

5. 自定义拦截器

目前拦截器用的比较多的是两种一个是 TokenRefreshInterceptor,还有一个是LoggerInterceptor

  • 简化客户端逻辑: 在客户端使用 Token 刷新拦截器后,客户端代码无需手动处理 Token 过期和刷新的逻辑。这样大大简化了客户端的代码结构,提高了代码的可维护性。
class TokenRefreshInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        var response = chain.proceed(originalRequest)

        // 检查响应是否需要进行 Token 刷新
        if (isTokenExpired(response)) {
            // 刷新 Token,获取新的 Token
            val newToken = refreshToken()

            // 使用新的 Token 重新构建请求
            val newRequest = originalRequest.newBuilder()
                .header("Authorization", "Bearer $newToken")
                .build()

            // 重新发起请求
            response = chain.proceed(newRequest)
        }

        return response
    }
}

  • 调试和排查问题: 记录请求和响应的详细日志信息,包括请求方法、URL、请求头、请求体、响应码、响应体等,有助于开发者在调试阶段更容易定位和排查问题
class LoggingInterceptor : Interceptor {

    private val UTF8 = Charset.forName("UTF-8")
    private val TAG = "OkHttpLogger"

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
      
        val orgRequest = chain.request()
        val response = chain.proceed(orgRequest)
        val body = orgRequest.body
        val sb = StringBuilder()
        if (orgRequest.method == "POST" && body is FormBody) {
            for (i in 0 until body.size) {
                sb.append(body.encodedName(i) + "=" + body.encodedValue(i) + ",")
            }
            sb.delete(sb.length - 1, sb.length)
            //打印post请求的信息
            Log.d(
                "code= ${response.code} |method= ${orgRequest.method}" +
                        " |url= ${orgRequest.url}"
                        + "\n" + "headers: ${orgRequest.headers.toMultimap()}"
                        + "\n" + "postRequestBody:${sb}"
            )
        } else {
            Log.d(
                "code= ${response.code} |method= ${orgRequest.method}" +
                        " |url= ${orgRequest.url}"
                        + "\n" + "headers: ${orgRequest.headers.toMultimap()}"
            )
        }
        //返回json
        val responseBody = response.body
        val contentLength = responseBody?.contentLength()
        val source = responseBody?.source()
        source?.request(java.lang.Long.MAX_VALUE)
        val buffer = source?.buffer
        var charset: Charset = UTF_8
        val contentType = responseBody?.contentType()

        try {
            var contentTypeCharset = contentType?.charset(charset)

            if (contentTypeCharset != null)
                charset = contentTypeCharset

        } catch (e: UnsupportedCharsetException) {
            return response
        }

        if (contentLength != 0L) {
            Log.d(TAG, "Response Body: ${buffer.clone().readString(UTF8)}")
        }
        return response

    }
}

6. 总结

  • OkHttp并没有改变Socket请求本质,不像MMKVSharedPreferences理念完全不同。但是优化了代码的调用难度,同时追加了重试机制,缓存机制。它的设计目标是在保持 HTTP 协议基本特性的同时,提供更好的性能和开发体验。这样的设计理念使得开发者可以更方便地进行网络请求,并在需要时利用 OkHttp 的一些特性来优化和定制网络通信。
  • OkHttpAndroid使用中存在哪些问题:
  1. 我们拿到响应体以后我们通常会刷新UI,但是OkHttpjava库,虽然兼容了Android,但是没有引入主线程的概念,那我们怎么刷新UI?
  2. OkHttp虽然提供了建造者模式帮助我们构建请求体,但是还是觉得写这部分代码很冗长,那我们有什么方式可以简化工作量?
转载自:https://juejin.cn/post/7311632859085357056
评论
请登录