Android 网络全栈攻略(三)—— 从三方库原理来看 HTTP

发布于:2025-05-23 ⋅ 阅读:(15) ⋅ 点赞:(0)

前面两篇文章我们介绍了 HTTP 协议的请求方法、请求码以及常用的请求头/响应头的知识。本篇会从 OkHttp 配置的角度来看这些框架是如何实现 HTTP 协议的,目的是加深对 HTTP 的理解,并学习协议是如何落地的。我们会选取 OkHttp 中与协议实现相关的源码作为切入点,而不是去全面地分析这两个框架的源码。如果对这两个框架还不是很了解,或者想查看源码分析向的文章,可以参考:

OkHttp(一)—— 整体流程与分发器

OkHttp(二)—— 拦截器

Retrofit 源码分析

使用 OkHttp 框架发送网络请求前,需要对 OkHttpClient 的成员属性进行配置。常用的有连接的超时时间 connectTimeoutMillis 以及读写超时时间 readTimeoutMillis、writeTimeoutMillis。它们只是众多配置中的冰山一角,下面我们分类来介绍这些配置。

1、框架实现的相关配置

我们将框架本身实现所需、但与 HTTP 协议没有太大关联的配置放在一起介绍:

open class OkHttpClient internal constructor(
  builder: Builder
) : Cloneable, Call.Factory, WebSocket.Factory {

  // 调度器,分发器
  @get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher

  // 连接池
  @get:JvmName("connectionPool") val connectionPool: ConnectionPool = builder.connectionPool

  /**
   * 不可变的拦截器列表,这些拦截器将观察每个调用的完整生命周期:从连接建立前(若存在连接)直到
   * 确定响应来源之后(响应来源可能是原始服务器、缓存或两者共同作用的结果)。该列表中的拦截器会
   * 监控整个调用过程的所有阶段,包括连接建立前的准备阶段以及最终确定响应来源(无论来自原始服务器、
   * 缓存还是两者的组合)之后的完整处理流程。
   */
  @get:JvmName("interceptors") val interceptors: List<Interceptor> =
      builder.interceptors.toImmutableList()

  /**
   * 不可变的拦截器列表,这些拦截器将观察单个网络请求及其响应。此类拦截器必须严格调用
   * [Interceptor.Chain.proceed] 方法一次:网络拦截器若跳过请求处理(短路)或重复发起网络请求,
   * 均会导致错误。这意味着每个网络拦截器必须确保对请求链路进行且仅进行一次传递操作
   */
  @get:JvmName("networkInterceptors") val networkInterceptors: List<Interceptor> =
      builder.networkInterceptors.toImmutableList()

  // 生产 EventListener 的工厂。EventListener 是一个请求过程中的事件监听器,比如连接建立、请求发起
  @get:JvmName("eventListenerFactory") val eventListenerFactory: EventListener.Factory =
      builder.eventListenerFactory
}

首先是调度器/分发器 Dispatcher,它主要负责网络请求任务的执行(同步、异步)以及任务的调度,详尽的分析可以参考文章开头给出参考文章。

然后是连接池 ConnectionPool。与连接池、线程池类似,这些池化概念都是对同类对象进行批量管理的工具,通过重用和自动回收来平衡性能与资源占用。连接池的工作模式如下:

  • 需要连接时优先从池中获取现成可用连接
  • 若无可用连接则创建新连接
  • 使用完毕的连接会返回池中等待复用
  • 长时间闲置的连接会被自动销毁

对于连接复用,不同的 HTTP 版本的情况有所不同:

  • HTTP/1:请求完全结束后可复用连接(复用已完成请求的空闲连接)
  • HTTP/2:支持多路复用(Multiplexing),同一 TCP 连接上可并行多个未完成的请求

连接池的性能优势:

  • 减少 TCP 握手开销
  • 降低内存占用
  • 提高请求响应速度

接下来是拦截器集合 interceptors、networkInterceptors。前者是观察完整调用过程的拦截器列表,比如 OkHttp 框架内置的重试与重定向拦截器、桥接拦截器等,可以帮助框架将开发者发来的网络请求函数转换成一个具体的 HTTP 请求发送出去。而 networkInterceptors 是仅观察单个网络请求和响应的拦截器列表,位于责任链的末端,处理的是经过 OkHttp 内部处理(如自动添加 Content-LengthHost 等头信息)后的最终发送到服务器的请求,以及从服务器返回的原始响应(例如未经自动解压的 gzip 数据)、通常可用于记录原始网络流量修改网络层头信息监控网络性能统一添加网络层参数访问底层网络连接细节

两者类型相同但作用范围不同,networkInterceptors 专门用于网络层拦截。如果只是想处理业务逻辑(如全局鉴权、日志记录处理后的响应),应优先使用应用层拦截器 interceptors。

关于拦截器的具体内容,会在下一节介绍 OkHttp 的责任链时详解。

最后是事件监听器工厂 EventListener.Factory,EventListener 是对请求过程各种事件的监听,比如连接建立、请求发起。

2、连接配置

与 HTTP 连接相关的配置:

// 连接失败时是否重试
@get:JvmName("retryOnConnectionFailure") val retryOnConnectionFailure: Boolean =
      builder.retryOnConnectionFailure

// 是否跟随重定向
@get:JvmName("followRedirects") val followRedirects: Boolean = builder.followRedirects

// 是否
@get:JvmName("followSslRedirects") val followSslRedirects: Boolean = builder.followSslRedirects

retryOnConnectionFailure 默认为 true,会在连接失败以及请求失败时进行重试。请求失败是指 TCP 连接没有成功,或者 TCP 连接成功了,对方无响应,以及同⼀个域名的多个 IP 切换重试。但是发送请求后收到 4xx 状态码的这种失败不算是请求失败,这种算客户端错误,不属于连接问题范畴。

followRedirects 表示是否遵循服务器的要求进行重定向,默认为 true 开启重定向,这样在收到 3xx 的响应状态码时,就会自动向重定向的 URL 发起一个新请求获得最终结果。如果关闭此功能,在接收到重定向响应时,就会返回这个状态码为 3xx 的响应作为结果,但通常这个中间结果给我们也没什么用,而且还要再手动发送重定向请求,因此这个功能通常都是打开的。

followSslRedirects 并不像名字那样看起来似乎是在进行 HTTPS 请求时是否允许重定向,而是在 followRedirects 开启的情况下,是否允许原请求的 URL 与重定向的 URL 发生了协议切换的情况下仍然允许重定向(比如原 URL 是一个 HTTPS 协议的,但重定向的 URL 是 HTTP 协议):

  • 特殊功能:控制协议切换时的重定向行为
  • 安全风险:防止 HTTPS → HTTP 降级攻击
  • 默认值:true(默认允许协议切换重定向)

3、Authenticator

OkHttp 的 Authenticator 接口是一个比较方便的进行自动认证修正的工具:

@get:JvmName("authenticator") val authenticator: Authenticator = builder.authenticator

前面介绍 HTTP 状态码时说过,如果你想访问一个无权访问的服务器资源,服务器会给客户端返回 401 表示未授权。在做安卓客户端时很少会直接遇到 401,因为我们通常会要求客户先登录再使用,不会去直接强行请求那些我们没权限的资源。也就是我们手里有了 token,才会让用户去使用。但是 token 是有有效期的,可能是 3 天、 7 天或者 15 天。在这个有效有效期到了之后,我们需要使用 refresh token 去获取新的 token。

此时可以使用 Authenticator 来完成刷新 token 并将其添加到请求头中:

val client = OkHttpClient.Builder()
    .authenticator(object : Authenticator {
        override fun authenticate(route: Route?, response: Response): Request? {
            // 1.先刷新 token,各家的接口与过程不同,省略具体代码仅保留步骤...
            
            // 2.将上一步拿到的新 token 添加到请求头的 Authorization 属性中
            return response.request.newBuilder()
            .header("Authorization", "Bearer xxxxx....")
            .build()
        }
    })

关于 HTTP 请求头的 Authenticator 字段,我们在系列的上一篇文章中有详细介绍,这里就不再赘述了。

4、CookieJar

@get:JvmName("cookieJar") val cookieJar: CookieJar = builder.cookieJar

Jar 有罐子的意思,CookieJar 就是饼干罐、饼干盒,这里引申为存储、管理 Cookie 的控制器。

CookieJar 提供了存取的判断⽀持(即什么时候需要存 Cookie,什么时候需要读取 Cookie),但没有给出具体的存取实现,只提供了默认实现对象的接口,平时我们用不到:

interface CookieJar {

  fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)

  fun loadForRequest(url: HttpUrl): List<Cookie>

  companion object {
    /** A cookie jar that never accepts any cookies. */
    @JvmField
    val NO_COOKIES: CookieJar = NoCookies()
    // 空实现
    private class NoCookies : CookieJar {
      override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
      }

      override fun loadForRequest(url: HttpUrl): List<Cookie> {
        return emptyList()
      }
    }
  }
}

如果需要存取 Cookie,需要自己实现,例如⽤ Map 存在内存⾥,或者⽤别的⽅式存在本地存储或者数据库中。

5、Cache

@get:JvmName("cache") val cache: Cache? = builder.cache

本地存储服务器响应数据,后续请求时优先从本地获取,减少网络请求以提升应用响应速度,减少服务器压力。

默认是 null,通过 OkHttpClient.Builder().cache() 方法配置 Cache 存储的⽂件位置以及存储空间上限。

6、DNS

@get:JvmName("dns") val dns: Dns = builder.dns

DNS(Domain Name System),域名系统。HTTP 请求必须知道目标主机的 IP 地址,需要通过 DNS 解析域名转换为 IP 地址。

上面 OkHttp 框架的 Dns 接口内部其实是用的 Java 原生的 InetAddress 提供的 DNS 解析功能进行域名解析的:

interface Dns {
  @Throws(UnknownHostException::class)
  fun lookup(hostname: String): List<InetAddress>

  companion object {
    @JvmField
    val SYSTEM: Dns = DnsSystem()
    private class DnsSystem : Dns {
      override fun lookup(hostname: String): List<InetAddress> {
        try {
          // 用 getAllByName() 获取域名对应的所有 IP(v4/v6) 地址,再转成 List
          return InetAddress.getAllByName(hostname).toList()
        } catch (e: NullPointerException) {
          throw UnknownHostException("Broken system behaviour for dns lookup of $hostname").apply {
            initCause(e)
          }
        }
      }
    }
  }
}

7、代理配置

@get:JvmName("proxy") val proxy: Proxy? = builder.proxy

@get:JvmName("proxySelector") val proxySelector: ProxySelector =
    when {
        // Defer calls to ProxySelector.getDefault() because it can throw a SecurityException.
        builder.proxy != null -> NullProxySelector
        else -> builder.proxySelector ?: ProxySelector.getDefault() ?: NullProxySelector
    }

@get:JvmName("proxyAuthenticator") val proxyAuthenticator: Authenticator =
    builder.proxyAuthenticator

Proxy 是 Java 原生的表示代理服务器的类,在 HTTP 请求中作为中间转发服务器,将请求转发给目标服务器后再将响应返回给客户端。

使用场景:

  • 网络管制:内网主机通过代理服务器访问外网
  • 突破限制:通过未被限制的 IP 代理访问受限网站

类型区分:

  • 正向代理:客户端主动配置的代理(当前讨论的类型)
  • 反向代理:服务端配置的代理(与本主题无关)

Proxy 有三种类型:

public class Proxy {
    public static enum Type {
        DIRECT, // 直连(无代理)
        HTTP, // HTTP/HTTPS 协议代理
        SOCKS; // SOCKS(V4/V5) 协议代理
    }
}

proxy 变量默认值为 null 表示不使用代理。当发送网络请求时,如果 proxy 为 null,即没有显式设置的 Proxy 时,会遍历 proxySelector 的 select() 返回的列表:

	public abstract List<Proxy> select(URI uri);

从 proxySelector 的声明处能判断出,如果 builder.proxy 不为空,即显式配置了代理时,proxySelector 就是 NullProxySelector。此时它的 select() 返回 NO_PROXY 表示无代理:

object NullProxySelector : ProxySelector() {
  override fun select(uri: URI?): List<Proxy> {
    requireNotNull(uri) { "uri must not be null" }
    return listOf(Proxy.NO_PROXY)
  }

  override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
  }
}

而假如 builder.proxy 为空,则 proxySelector 依次取 builder.proxySelector、ProxySelector.getDefault()、NullProxySelector。其中 ProxySelector.getDefault() 取到的 ProxySelector 对象可能会抛出 SecurityException。

最后,当代理服务器需要授权才能使用时,通过 proxyAuthenticator 进行身份验证,与前面讲过的常规 authenticator 类似,其 token 存在失效机制。

8、Socket 工厂

@get:JvmName("socketFactory") val socketFactory: SocketFactory = builder.socketFactory

private val sslSocketFactoryOrNull: SSLSocketFactory?

@get:JvmName("sslSocketFactory") val sslSocketFactory: SSLSocketFactory
  get() = sslSocketFactoryOrNull ?: throw IllegalStateException("CLEARTEXT-only client")

SocketFactory 与 SSLSocketFactory 是 Java 原生的 Socket 实现,分别用于创建 HTTP、HTTPS 请求所需的底层连接。其中 SSLSocketFactory 用于加密通信,需要在 TCP 连接上建立 TLS/SSL 连接,SSLContext 通过 SSLSocketFactory 创建加密连接。

常规 Socket 和 SSLSocket 采用相同的工厂模式,两者分别处理明文和加密通信场景。

9、证书验证配置

// X509 证书验证管理器,验证证书是否合法
@get:JvmName("x509TrustManager") val x509TrustManager: X509TrustManager?

// 主机名验证器,用于验证被验证的证书是否来自于要访问的网站
@get:JvmName("hostnameVerifier") val hostnameVerifier: HostnameVerifier = builder.hostnameVerifier

@get:JvmName("certificatePinner") val certificatePinner: CertificatePinner

@get:JvmName("certificateChainCleaner") val certificateChainCleaner: CertificateChainCleaner?

9.1 X509TrustManager

核心功能:作为 HTTPS 连接中的证书验证器,在 HTTPS 建立连接的时候,验证服务器发送至客户端的证书合法性。

X509 代表国际通用的证书标准格式,所有 TLS/SSL 连接使用的证书都采用 X509 格式。因此 X509TrustManager 就是验证证书的管理器,是系统提供的标准类(Java/Android 等平台),它们经过多年行业实践形成的统一标准实现,替代了早期各种不兼容的证书验证方案。它的验证机制为:

  • 检查证书签发机构是否在系统可信列表中
  • 验证证书签名是否正确(防止伪造签名)
  • 需要证书签发机构的公钥与系统存储的公钥匹配

工作流程:

  • HTTPS 握手时接收服务器证书
  • 执行两级验证(可信列表检查 + 签名验证)
  • 验证通过则建立安全连接,否则终止连接

9.2 HostnameVerifier

主机名验证器 HostnameVerifier 的核心作用是验证证书是否属于当前访问的网站,防止域名欺骗攻击。它是 javax 的接口,只有一个 verify() 用于验证。OkHttp 框架用 OkHostnameVerifier 实现这个接口:

@Suppress("NAME_SHADOWING")
object OkHostnameVerifier : HostnameVerifier {

  override fun verify(host: String, session: SSLSession): Boolean {
    return if (!host.isAscii()) {
      false
    } else {
      try {
        // 取 SSLSession 内 Certificate 数组的第一个元素作为被验证的证书
        verify(host, session.peerCertificates[0] as X509Certificate)
      } catch (_: SSLException) {
        false
      }
    }
  }
    
  fun verify(host: String, certificate: X509Certificate): Boolean {
    return when {
      // host 是 IP 地址
      host.canParseAsIpAddress() -> verifyIpAddress(host, certificate)
      // host 是域名
      else -> verifyHostname(host, certificate)
    }
  }

第一个 verify() 会取 SSLSession 内 Certificate 数组的第一个元素作为被验证的证书,原因是只有证书链的第一个证书是服务器自身证书,其他的都是 CA 机构证书。

第二个 verify() 则根据参数 host 的主机名类型采用不同的验证策略。如果 host 是 IP 地址:

private val VERIFY_AS_IP_ADDRESS =
    "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)".toRegex()

fun String.canParseAsIpAddress(): Boolean {
  return VERIFY_AS_IP_ADDRESS.matches(this)
}

则验证 host 的 IP 地址是否与证书的 IP 地址列表中的任意一项匹配:

  private fun verifyIpAddress(ipAddress: String, certificate: X509Certificate): Boolean {
    val canonicalIpAddress = ipAddress.toCanonicalHost()

    return getSubjectAltNames(certificate, ALT_IPA_NAME).any {
      canonicalIpAddress == it.toCanonicalHost()
    }
  }

否则用 verifyHostname() 验证 host 的域名是否与证书域名列表中的任意一项匹配:

  private fun verifyHostname(hostname: String, certificate: X509Certificate): Boolean {
    val hostname = hostname.asciiToLowercase()
    return getSubjectAltNames(certificate, ALT_DNS_NAME).any {
      // 验证域名字符串
      verifyHostname(hostname, it)
    }
  }

9.3 CertificatePinner

证书固定,用于强制客户端只信任特定的证书或公钥,防止中间人攻击(MITM)。即使设备信任了其他根证书,应用仍会严格验证服务器证书是否匹配预设的指纹。

核心机制:

  • 配置证书指纹:开发者预置服务器证书的公钥哈希(如 SHA-256),在 TLS 握手时,OkHttp 会检查服务器证书链中是否存在至少一个与预设指纹匹配的证书。
  • 动态更新:允许在运行时更新固定规则,例如应对证书轮换。

在 CertificatePinner 这个类的注释上给出了它的使用方法。比如你要为 “publicobject.com” 这个网站设置固定的证书,那么在构建 CertificatePinner 对象时先为这个 host 随意添加一个 sha256 的公钥哈希,不正确也没关系,因为错了的话会产生一个 SSLPeerUnverifiedException 告诉你正确的 sha256 是多少:

    String hostname = "publicobject. com";
	// 第一次先随便设置一个,错了抛的异常会告诉你正确的 sha256 是什么,再设置就好
    CertificatePinner certificatePinner = new CertificatePinner.Builder()
            .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
            .build();
    OkHttpClient client = OkHttpClient.Builder()
            .certificatePinner(certificatePinner)
            .build();
    Request request = new Request.Builder()
            .url("https://" + hostname)
            .build(); 
    
    client.newCall(request).execute();

因为第一次硬编码配置公钥哈希,我们也不知道这个哈希值是多少,随意配置的验证失败就会抛出 SSLPeerUnverifiedException:

javax.net. ssl. SSLPeerUnverifiedException: Certificate pinning failure! 
Peer certificate chain:     
	sha256/ afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject. com, OU=PositiveSSL     
	sha256/ klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA     
	sha256/ grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority
	sha256/ lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root Pinned certificates for publicobject. com:     
	sha256/ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=   
  at okhttp3.CertificatePinner. check(CertificatePinner. java)   
  at okhttp3.Connection. upgradeToTls(Connection. java)   
  at okhttp3.Connection. connect(Connection. java)   
  at okhttp3.Connection. connectAndSetOwner(Connection. java)

log 给出了正确的 sha256,按照这个设置就好:

CertificatePinner certificatePinner = new CertificatePinner. Builder()     
	.add("publicobject. com", "sha256/ afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")     
	.add("publicobject. com", "sha256/ klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")     
	.add("publicobject. com", "sha256/ grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")     
	.add("publicobject. com", "sha256/ lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")     
	.build();

对 CertificatePinner 概括如下几点:

  • 安全机制:通过硬编码证书哈希值增强安全性,可以防止公共 WIFI 中的恶意证书攻击
  • 工作原理:要求服务器证书必须匹配预先配置的 SHA-256 哈希值
  • 使用场景:防御 CA 机构被攻击或中间人攻击,适用于高安全需求场景,如金融、医疗应用
  • 风险提示:证书更新时需要同步更新客户端配置,否则会导致服务不可用,因此要慎用 CertificatePinner

9.4 CertificateChainCleaner

主要作用是证书链清理与验证,负责在 TLS 握手过程中对服务器返回的证书链进行标准化处理,确保其符合验证要求。具体功能包括:

  • 链式结构修复:补充缺失的中间证书,构建完整的证书链。
  • 冗余证书移除:删除重复或无关的证书。
  • 兼容性处理:适配不同 TLS 实现(如 Conscrypt、Bouncy Castle)。

10、ConnectionSpec 列表

@get:JvmName("connectionSpecs") val connectionSpecs: List<ConnectionSpec> =
    builder.connectionSpecs

connectionSpecs 是 OkHttp 用于定义连接规范的列表,包含客户端与服务器建立连接时所需的各种协议参数。在 HTTPS 握手阶段,客户端通过 connectionSpecs 向服务器声明自己支持的加密协议和算法组合。

ConnectionSpec 的主要内容包括:

  • TLS 版本:如 TLS 1.0、TLS 1.1、TLS 1.3 等
  • 加密套件:包含非对称加密算法、对称加密算法和哈希算法组合

ConnectionSpec 的类定义:

class ConnectionSpec internal constructor(
  // 是否为 TLS
  @get:JvmName("isTls") val isTls: Boolean,
  // 是否在 TLS 握手过程中启用扩展
  @get:JvmName("supportsTlsExtensions") val supportsTlsExtensions: Boolean,
  // 加密套件
  private val cipherSuitesAsString: Array<String>?,
  // TLS 版本
  private val tlsVersionsAsString: Array<String>?
) {
  
  @get:JvmName("cipherSuites") val cipherSuites: List<CipherSuite>?
    get() {
      return cipherSuitesAsString?.map { CipherSuite.forJavaName(it) }?.toList()
    }
    
  @get:JvmName("tlsVersions") val tlsVersions: List<TlsVersion>?
    get() {
      return tlsVersionsAsString?.map { TlsVersion.forJavaName(it) }?.toList()
    }    
}

下面会讲解一下 CipherSuite 与 TLS 相关配置。

10.1 CipherSuite

CipherSuite 即加密套件,是网络安全中用于定义加密通信时所用算法组合的核心概念。它决定了客户端与服务器之间如何协商密钥、加密数据以及验证完整性,是 TLS/SSL 协议中保障通信安全的核心机制。一个完整的 CipherSuite 通常由以下四类算法组合而成,用类似 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 的格式表示:

  1. 密钥交换算法(Key Exchange)
    • 作用:协商对称加密所需的密钥,确保密钥安全传输。
    • 常见算法:
      • RSA:基于非对称加密,密钥交换依赖服务器的私钥。
      • ECDHE(Elliptic Curve Diffie-Hellman Ephemeral):基于椭圆曲线的临时密钥交换,支持前向保密(PFS)。
      • DHE(Diffie-Hellman Ephemeral):临时 DH 算法,同样支持前向保密。
  2. 身份认证算法(Authentication)
    • 作用:验证服务器(或客户端)的身份,通常使用数字证书。
    • 常见算法:
      • RSA:证书的公钥用于签名验证。
      • ECDSA:基于椭圆曲线的数字签名算法,更高效。
  3. 对称加密算法(Bulk Encryption)
    • 作用:加密实际传输的数据。
    • 常见算法:
      • AES_256_GCM:256 位 AES 加密,结合 GCM 模式(支持认证加密)。
      • CHACHA20_POLY1305:适用于移动设备的轻量级加密算法。
      • (已淘汰的算法如 DES3DESRC4 存在安全隐患,应避免使用)。
  4. 消息认证码算法(MAC)
    • 作用:验证数据完整性,防止篡改。
    • 现代实现:
      • 通常由加密算法的模式直接提供(如 GCMPOLY1305 包含完整性校验)。
      • 旧版本可能使用 SHA256SHA384 作为 HMAC 的哈希算法。

CipherSuite 的作用:

  1. 保障通信安全
    • 通过组合多种算法,确保数据在传输过程中满足:
      • 机密性(加密防窃听)
      • 完整性(数据防篡改)
      • 身份认证(通信方身份可信)。
  2. 支持前向保密(Perfect Forward Secrecy, PFS)
    • 使用临时密钥交换算法(如 ECDHE),即使长期私钥泄露,历史通信也无法被解密。
  3. 性能优化
    • 根据硬件能力选择高效算法(如 CHACHA20_POLY1305 更适合移动设备)。

在 ConnectionSpec 中可以通过 CipherSuite.forJavaName() 将构造函数上的 cipherSuitesAsString 转换成具体的 CipherSuite:

    @JvmStatic
    @Synchronized fun forJavaName(javaName: String): CipherSuite {
      var result: CipherSuite? = INSTANCES[javaName]
      if (result == null) {
        result = INSTANCES[secondaryName(javaName)]

        if (result == null) {
          result = CipherSuite(javaName)
        }

        // Add the new cipher suite, or a confirmed alias.
        INSTANCES[javaName] = result
      }
      return result
    }

CipherSuite 内也定义了多种加密套件常量:

	@JvmField val TLS_RSA_WITH_NULL_MD5 = init("SSL_RSA_WITH_NULL_MD5", 0x0001)
    @JvmField val TLS_RSA_WITH_NULL_SHA = init("SSL_RSA_WITH_NULL_SHA", 0x0002)
    @JvmField val TLS_RSA_EXPORT_WITH_RC4_40_MD5 = init("SSL_RSA_EXPORT_WITH_RC4_40_MD5", 0x0003)
    @JvmField val TLS_RSA_WITH_RC4_128_MD5 = init("SSL_RSA_WITH_RC4_128_MD5", 0x0004)
    @JvmField val TLS_RSA_WITH_RC4_128_SHA = init("SSL_RSA_WITH_RC4_128_SHA", 0x0005)

10.2 TLS 相关

TlsVersion 内定义了支持的 TLS 版本:

enum class TlsVersion(
  @get:JvmName("javaName") val javaName: String
) {
  TLS_1_3("TLSv1.3"), // 2016.
  TLS_1_2("TLSv1.2"), // 2008.
  TLS_1_1("TLSv1.1"), // 2006.
  TLS_1_0("TLSv1"), // 1999.
  SSL_3_0("SSLv3"); // 1996.

10.3 快捷配置

ConnectionSpec 的伴生对象中定义了几种现成的 ConnectionSpec 实例免去创建的麻烦:

	@JvmField
    val RESTRICTED_TLS = Builder(true)
        .cipherSuites(*RESTRICTED_CIPHER_SUITES)
        .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2)
        .supportsTlsExtensions(true)
        .build()

    @JvmField
    val MODERN_TLS = Builder(true)
        .cipherSuites(*APPROVED_CIPHER_SUITES)
        .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2)
        .supportsTlsExtensions(true)
        .build()

    @JvmField
    val COMPATIBLE_TLS = 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()

    /** Unencrypted, unauthenticated connections for `http:` URLs. */
	// Builder 参数是 Boolean 类型表示是否使用 TLS,这里传 false 表示不使用 TLS,
	// 也就是不使用 HTTPS,而使用 HTTP 发送明文报文
    @JvmField
    val CLEARTEXT = Builder(false).build()

RESTRICTED_TLS、MODERN_TLS、COMPATIBLE_TLS 的安全性由高至低,而兼容性由低至高,通常选择 MODERN_TLS 可以满足大多数需求。而最后的 CLEARTEXT 即明文发送报文,实际上就是使用 HTTP 发送报文而不是 HTTPS。

11、Protocol 列表

@get:JvmName("protocols") val protocols: List<Protocol> = builder.protocols

protocols 是支持的协议列表,Protocol 枚举类定义了支持的协议版本:

enum class Protocol(private val protocol: String) {

  HTTP_1_0("http/1.0"),

  HTTP_1_1("http/1.1"),

  /**
   * Chromium(谷歌浏览器)的二进制分帧协议,包含头部压缩、在同一连接上多路复用多个请求及服务器
   * 推送功能。HTTP/1.1 的语义层建立在 SPDY/3 协议之上。
   * requests on the same socket, and server-push. HTTP/1.1 semantics are layered on SPDY/3.
   *
   * 当前版本的 OkHttp 已不再支持此协议。
   */
  @Deprecated("OkHttp has dropped support for SPDY. Prefer {@link #HTTP_2}.")
  SPDY_3("spdy/3.1"),

  /**
   * IETF(互联网工程任务组)制定的二进制分帧协议,包含头部压缩、在同一连接上多路复用多个请求及
   * 服务器推送功能。
   * HTTP/1.1 的语义层建立在 HTTP/2 协议之上。
   *
   * HTTP/2 要求基于 TLS 1.2 的部署必须支持 [CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256] 
   * 加密套件,该套件在 Java 8+ 和 Android 5+ 中可用。
   */
  HTTP_2("h2"),

  /**
   * 明文传输的 HTTP/2 协议,无需进行“升级”握手流程。此选项要求客户端已预先确认服务器支持明文 HTTP/2。
   */
  H2_PRIOR_KNOWLEDGE("h2_prior_knowledge"),
}

SPDY_3 是 Google 在 HTTP 1.1 上进行多番尝试优化所产生的一个协议,后面 HTTP_2 借鉴了其中的内容。因此在正式支持 HTTP_2 之后,SPDY_3 可以退出历史舞台了,被标记为 @Deprecated。

H2_PRIOR_KNOWLEDGE 是明文版本的 HTTP2,而 HTTP_2 是建立在 TLS 基础上的 HTTP2。分开是因为 HTTP2 并没有强制要求加密传输报文,因此将两种规格分开。

12、时间相关配置

  /**
   * 全局调用超时,默认无限制,覆盖整个调用周期(包含连接、读写等所有阶段)
   */  
  @get:JvmName("callTimeoutMillis") val callTimeoutMillis: Int = builder.callTimeout

  /** 连接超时,默认 10 秒 */
  @get:JvmName("connectTimeoutMillis") val connectTimeoutMillis: Int = builder.connectTimeout

  /** 读超时,默认 10 秒 */
  @get:JvmName("readTimeoutMillis") val readTimeoutMillis: Int = builder.readTimeout

  /** 写超时,默认 10 秒 */
  @get:JvmName("writeTimeoutMillis") val writeTimeoutMillis: Int = builder.writeTimeout

  /** Ping Interval 是 WebSocket 和 HTTP/2 协议的心跳检测机制,用于维持长连接状态 */
  @get:JvmName("pingIntervalMillis") val pingIntervalMillis: Int = builder.pingInterval

pingIntervalMillis:客户端定期发送 Ping 信号(类似乒乓球中的"ping"),服务端返回 Pong 响应(对应"pong"),通过这种交互确认连接存活。 默认不发送心跳包(值为 0),通过 Builder 模式设置:builder.pingInterval。


网站公告

今日签到

点亮在社区的每一天
去签到