Android 网络与安全面试题:OkHttp原理都说不清,怎么拿Offer?

2 阅读11分钟

Android 网络与安全面试题:OkHttp原理都说不清,怎么拿Offer?

这是"Android面试全景"专栏的第4篇

记住,高级工程师不仅要会用,还要能讲清楚为什么这么设计。

一、HTTP 1.1 vs 2.0 vs 3.0:每次升级到底解决了什么?

核心:三次迭代解决的是同一个问题——网络传输效率,但切入点不同。1.1解决连接复用,2.0解决并发复用,3.0解决丢包阻塞。

HTTP/1.1 的两个致命伤

// 问题1:Head of Line Blocking(队头阻塞)
// 假设你请求 a.js(10KB)、b.css(100KB)、c.png(1MB)
// 按顺序返回,你得等 a 和 b 全部到达才能开始解析
// 即使 b.css 已经在下载了,c.png 也得排队等

// 问题2:keep-alive 默认60秒超时
// 每个域名最多6个连接(浏览器限制)
// 大量小文件请求时,连接建立的开销比传输还大

HTTP/2 的多路复用:用一个连接干所有事

核心:把多个"请求-响应"打包成二进制帧(Frame),打上Stream ID混在一起发送,接收端按ID组装。

HTTP/1.1 思维(串行):
请求A ──────────▶ ──────────▶ 
请求B ──────▶ ──────▶ 
请求C ─▶ ─▶ 

HTTP/2 思维(并行):
连接:▶▶▶AAABBBCCC▶▶▶  // 帧交错,ID识别

OkHttp 中的体现

// OkHttp 3.5+ 默认启用 HTTP/2
val client = OkHttpClient.Builder()
    .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
    .build()

// 实际表现:同一个域名的多个请求共用一个 TCP 连接
// 可以通过 Chrome 的 net-internals 观察 stream ID

面试加分

  • HTTP/2 强制 HTTPS(基于 ALPN 协议协商)
  • Header 也要压缩(HPACK),避免重复传输
  • 服务器推送(Server Push)在 OkHttp 中不支持,因为 Android 生态碎片化严重

HTTP/3:把 TCP 换成 QUIC

核心:TCP 的队头阻塞发生在传输层,换 UDP 自己实现可靠传输就能绕过。

TCP 队头阻塞:丢一个包,所有 Stream 都等
QUIC 队头阻塞:丢一个包,只有丢包那个 Stream 等,其他继续

// OkHttp 通过平台支持 HTTP/3
// Android 10+ 系统级支持,但需要服务器也支持 QUIC

面试加分

  • QUIC 在丢包率高的网络(移动网络)优势明显
  • 连接建立只需 1 RTT(0-RTT),比 TCP+TLS 的 1.5~3 RTT 快
  • OkHttp 的 HTTP/3 支持依赖 Cronet 引擎

二、HTTPS 握手过程:证书链是怎么工作的?

核心:HTTPS 不是简单加密,是身份认证 + 密钥协商 + 数据加密三位一体。

完整握手流程(以 RSA 为例)

Client                                              Server
   │                                                    │
   │  1. Client Hello(支持的TLS版本、加密套件、随机数)   │
   │ ─────────────────────────────────────────────────▶│
   │                                                    │
   │  2. Server Hello(选定TLS版本、加密套件、随机数)     │
   │  3. Server Certificate(服务器证书链)               │
   │  4. Server Key Exchange(DH参数)                   │
   │ ◀─────────────────────────────────────────────────│
   │                                                    │
   │  5. Client Key Exchange(PreMaster Secret)         │
   │  6. Certificate Verify(用私钥签名证明身份)         │
   │ ─────────────────────────────────────────────────▶│
   │                                                    │
   │  7. ChangeCipherSpec(之后用协商的密钥加密)        │
   │  8. Finished(握手摘要,验证双方密钥一致)           │
   │ ◀─────────────────────────────────────────────────│
   │                                                    │
   │              对称加密通信开始                        │

证书链验证:三层结构

// 典型的证书链:Root CA → Intermediate CA → Server Certificate
//
// 用户证书(你的服务器)
//     ↑ 签发
// 中间证书(CA机构)
//     ↑ 签发
// 根证书(预装在系统和浏览器中)

// OkHttp 的证书校验流程
fun verifyCertificate(chain: X509CertificateChain) {
    // 1. 遍历证书链,验证每一级的签名
    for (i in 0 until chain.size - 1) {
        val cert = chain[i]
        val issuer = chain[i + 1]
        cert.verify(issuer.publicKey)  // 用签发者公钥验证签名
    }
    
    // 2. 检查证书有效期
    val now = System.currentTimeMillis()
    require(chain.leaf.notBefore <= now && now <= chain.leaf.notAfter)
    
    // 3. 验证根证书(系统信任库)
    val rootCA = getTrustedRoot()
    chain.last().verify(rootCA.publicKey)
    
    // 4. 检查证书域名
    require(hostnameMatches(chain.leaf, domain))
}

Android 实战

// OkHttp 默认校验
val client = OkHttpClient.Builder()
    .certificateChainCleaner(CertificateChainCleaner.getDefault())
    .hostnameVerifier(HostnameVerifier { hostname, session ->
        // 验证 hostname 是否匹配证书 CN/SAN
        HttpsURLConnection.getDefaultHostnameVerifier()
            .verify(hostname, session)
    })
    .build()

// 自定义校验:抓包工具检测
val debugClient = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val request = chain.request()
        val response = chain.proceed(request)
        
        // 检查证书是否被代理篡改
        val certs = response.handshake?.peerCertificates
        if (certs != null) {
            // Charles/Fiddler 会在证书中插入自己的根证书
            validateCertificateChain(certs, request.url.host)
        }
        response
    }
    .build()

面试加分

  • 证书链验证失败通常意味着被中间人攻击(抓包场景除外)
  • Android 7+ 只信任系统证书,新版抓包需要 root 或 Charles 自签证书
  • 证书固定(Certificate Pinning)是防止抓包的有效手段
  • OCSP(在线证书状态协议)可以检查证书是否被吊销

三、OkHttp 拦截器链:责任链模式怎么实现的?

核心:拦截器链是责任链模式的经典实现,每个拦截器负责一段处理逻辑,链式传递Request和Response。

拦截器执行顺序

                    Request
                       │
         ┌─────────────┼─────────────┐
         ▼             ▼             ▼
    ┌─────────┐  ┌─────────┐  ┌─────────┐
    │  App    │  │  App    │  │  App    │
    │自定义1  │→ │自定义2  │→ │RetryAnd │
    │         │  │         │  │FollowUp │
    └─────────┘  └─────────┘  └─────────┘
         │             │             │
         ▼             ▼             ▼
    ┌─────────────────────────────────────┐
    │         OkHttpCore(BridgeInterceptor)│
    │         补充Content-Type、Host等头    │
    └─────────────────────────────────────┘
         │
         ▼
    ┌─────────┐  ┌─────────┐  ┌─────────┐
    │Cache    │→ │Connect  │→ │Network  │
    │Interceptor│ │Interceptor│ │CallServer│
    └─────────┘  └─────────┘  └─────────┘
         │             │             │
         ▼             ▼             ▼
    ┌─────────────────────────────────────┐
    │            TCP / TLS                │
    └─────────────────────────────────────┘
         │
         │        Response 向上返回
         ▼

源码实现

// 简化版 RealInterceptorChain
class RealInterceptorChain(
    private val interceptors: List<Interceptor>,
    private val index: Int
) : Interceptor.Chain {
    
    override fun proceed(request: Request): Response {
        // 取下一个拦截器
        val next = RealInterceptorChain(interceptors, index + 1)
        val interceptor = interceptors[index]
        
        // 调用当前拦截器,传入下一个链
        return interceptor.intercept(next)
    }
}

// 应用拦截器 vs 网络拦截器的区别
object CustomInterceptor {
    // 应用拦截器:在请求发送前、响应后各执行一次
    // 适合:日志、参数修改、Mock数据
    val appInterceptor = Interceptor { chain ->
        val request = chain.request()
        println("应用层:准备请求 ${request.url}")
        
        val response = chain.proceed(request)
        println("应用层:收到响应 ${response.code}")
        
        response
    }
    
    // 网络拦截器:真正与服务器通信时执行
    // 适合:获取网络层信息、监控连接状态
    val networkInterceptor = Interceptor { chain ->
        val request = chain.request()
        val startNanos = System.nanoTime()
        
        val response = chain.proceed(request)
        val endNanos = System.nanoTime()
        
        println("网络层:耗时 ${(endNanos - startNanos) / 1_000_000}ms")
        response
    }
}

自定义拦截器能做什么?

class TokenInterceptor : Interceptor {
    
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        
        // 1. 统一添加 Token
        val tokenRequest = originalRequest.newBuilder()
            .header("Authorization", "Bearer ${getToken()}")
            .build()
        
        val response = chain.proceed(tokenRequest)
        
        // 2. 处理 Token 过期(401)
        if (response.code == 401) {
            synchronized(this) {
                // 双重检查:可能其他线程已经在刷新
                if (isTokenExpired()) {
                    refreshToken()
                }
            }
            
            // 3. 重试原请求
            val newToken = getToken()
            val retryRequest = originalRequest.newBuilder()
                .header("Authorization", "Bearer $newToken")
                .build()
            
            response.close()
            return chain.proceed(retryRequest)
        }
        
        return response
    }
}

// 添加到 OkHttpClient
val client = OkHttpClient.Builder()
    .addInterceptor(TokenInterceptor())  // 应用拦截器
    // .addNetworkInterceptor(LoggingInterceptor())
    .build()

面试加分

  • 应用拦截器调用 proceed() 多次会重复请求;网络拦截器多次调用会创建多个连接
  • BridgeInterceptor 负责将应用层Request转成网络层Request(压缩、编码、分块)
  • ConnectInterceptor 真正建立Socket连接
  • 拦截器链是同步递归实现,不是循环,这是容易搞混的点

四、OkHttp 连接池复用:keep-alive 是怎么回事?

核心:TCP 连接建立有三次握手、HTTPS 还有 TLS 握手,建立成本很高。连接池让多个请求复用同一个连接。

keep-alive 的工作原理

无 keep-alive:
建立连接 → 请求 → 关闭连接 → 建立连接 → 请求 → 关闭连接
   RTT         数据         RTT         数据

有 keep-alive:
建立连接 → 请求 → 请求 → 请求 → 关闭连接
   RTT    数据   数据   数据

// HTTP/1.1 默认开启 keep-alive
// HTTP/2 只有一条连接,多路复用也是复用这条连接

OkHttp 连接池配置

val connectionPool = ConnectionPool(
    maxIdleConnections = 5,      // 最大空闲连接数
    keepAliveDuration = 5, TimeUnit.MINUTES  // 空闲连接存活时间
)

val client = OkHttpClient.Builder()
    .connectionPool(connectionPool)
    .build()

// 连接池内部维护:
// - Deque<RealConnection> 存放连接
// - 引用计数:每个请求使用后 +1,完成后 -1
// - 引用为0且空闲时间超时 → 清理

连接复用判断逻辑

// RealConnection.canReuse() 判断逻辑
fun canReuse(connection: RealConnection, route: Route): Boolean {
    // 1. 协议匹配(HTTP/1.1 vs HTTP/2 不混用)
    if (connection.protocol() != route.protocol()) return false
    
    // 2. 域名匹配(不同域名不能复用)
    if (connection.route().proxy != route.proxy) return false
    
    // 3. 地址匹配(IP + 端口 + TLS配置)
    if (connection.route().socketAddress != route.socketAddress) return false
    
    // 4. HTTP/2 需要 TLS SNI 匹配
    if (protocol == Protocol.HTTP_2) {
        if (connection.connectionSpecs != route.connectionSpecs) return false
    }
    
    return true
}

Android 实战

// 典型问题:连接泄漏
// 响应体必须关闭,否则连接无法归还连接池
val response = client.newCall(request).execute()
try {
    val body = response.body
    // 处理 body
} finally {
    response.close()  // 关键!否则连接泄漏
}

// 或者用 use 自动关闭
client.newCall(request).execute().use { response ->
    // 自动关闭响应体
}

// Kotlin 协程版本
suspend fun get(url: String): String {
    return client.newCall(Request.Builder().url(url).build())
        .await()
        .use { response -> response.body!!.string() }
}

// 协程扩展
suspend fun Call.await(): Response = suspendCoroutine { 
    enqueue(object : Callback {
        override fun onResponse(call: Call, response: Response) {
            it.resume(response)
        }
        override fun onFailure(call: Call, e: IOException) {
            it.resumeWithException(e)
        }
    })
}

面试加分

  • 连接池默认 5 分钟清理空闲连接,活跃连接不受影响
  • HTTP/2 一条连接服务所有请求,连接池意义不大
  • Android 上 HTTP/2 的优势需要服务器也支持
  • 连接复用前提是 同一个 OkHttpClient 实例,不同实例不共享连接池

五、Retrofit 和 OkHttp:傻傻分不清?

核心:Retrofit 是外观模式,给 OkHttp 套了一层皮——把接口定义变成网络请求。OkHttp 负责底层,Retrofit 负责高层抽象。

职责划分

┌─────────────────────────────────────────────┐
│                   Retrofit                  │
│  ┌───────────────────────────────────────┐  │
│  │  1. 接口 → Request 转换(注解解析)    │  │
│  │  2. Response → 泛型对象转换(Converter)│  │
│  │  3. 生命周期绑定(CallAdapter)         │  │
│  │  4. 错误处理封装                        │  │
│  └───────────────────────────────────────┘  │
│                      │                       │
│                      ▼                       │
│  ┌───────────────────────────────────────┐  │
│  │                  OkHttp                │  │
│  │  1. TCP/TLS 连接管理                   │  │
│  │  2. 缓存策略                           │  │
│  │  3. 拦截器链                           │  │
│  │  4. 连接池复用                         │  │
│  └───────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

Retrofit 的核心能力

// Retrofit 做的事 OkHttp 不会做
interface GitHubService {
    @GET("users/{user}/repos")
    fun listRepos(@Path("user") user: String): Call<List<Repo>>
    
    @POST("repos")
    suspend fun createRepo(@Body repo: Repo): Repo
}

// 注解解析 → Request
// "users/{user}/repos" + user="octocat" → "users/octocat/repos"

// Converter: Gson → 对象
// Response.body.string() → List<Repo>(通过 GsonConverterFactory)
// ConverterFactory 是 Retrofit 独有的

// CallAdapter: Call → suspend
// suspend fun → 自动在协程执行,由 CallAdapter 处理

实际配合

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .client(okHttpClient)  // 传入 OkHttpClient
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(CoroutineCallAdapterFactory())
    .build()

val service = retrofit.create(GitHubService::class.java)

// Retrofit 最终生成 OkHttp 请求
// 生成的 Request 大概是:
// GET https://api.github.com/users/octocat/repos
// Header: Content-Type: application/json
// Header: Accept: application/json

面试加分

  • Retrofit 本质是动态代理(Proxy.newProxyInstance),接口方法调用触发拦截
  • ServiceMethod 缓存:每个接口方法只解析一次注解
  • OkHttp 可以单独使用,但 Retrofit 简化了接口定义
  • Retrofit 2.0 后,Call.execute()Call.enqueue() 直接用的是 OkHttpCall
  • 想用 WebSocket?WebSocket.Factory 是 OkHttp 的接口,Retrofit 通过 CallAdapter 支持

六、Token 刷新:无感知续命怎么做?

核心:Token 刷新不是简单重试,而是并发控制 + 队列等待 + 自动重试的协调过程。

问题分析

1. Token 过期 → 401
2. 刷新 Token → 新 Token
3. 重试原请求

问题:
- 并发请求同时收到 401,都去刷新 Token 浪费资源
- 刷新期间的新请求要等待
- 刷新失败要跳转登录

完整方案

class TokenManager(
    private val authService: AuthService,
    private val tokenStorage: TokenStorage
) {
    private var refreshing = false
    private val waitingRequests = mutableListOf<Pair<Request, Continuation<Response>>>()
    
    @Volatile
    private var currentToken: String? = tokenStorage.getToken()
    
    suspend fun executeWithAuth(request: Request, chain: Interceptor.Chain): Response {
        val authRequest = request.newBuilder()
            .header("Authorization", "Bearer $currentToken")
            .build()
        
        val response = chain.proceed(authRequest)
        
        if (response.code == 401) {
            // Token 过期,尝试刷新
            if (refreshTokenIfNeeded()) {
                // 刷新成功,重试
                response.close()
                val newRequest = request.newBuilder()
                    .header("Authorization", "Bearer $currentToken")
                    .build()
                return chain.proceed(newRequest)
            } else {
                // 刷新失败,跳转登录
                onAuthFailed()
                return response
            }
        }
        
        return response
    }
    
    private suspend fun refreshTokenIfNeeded(): Boolean {
        if (refreshing) {
            // 等待其他线程刷新完成
            return suspendCoroutine { continuation ->
                synchronized(waitingRequests) {
                    waitingRequests.add(continuation as Pair<Request, Continuation<Response>>)
                }
            }.let { it?.code == 200
            }
        }
        
        refreshing = true
        return try {
            val newToken = authService.refreshToken(tokenStorage.getRefreshToken())
            currentToken = newToken.accessToken
            tokenStorage.saveToken(newToken)
            
            // 唤醒等待的请求
            synchronized(waitingRequests) {
                waitingRequests.forEach { (req, cont) ->
                    cont.resume(null)  // 通知继续
                }
                waitingRequests.clear()
            }
            true
        } catch (e: Exception) {
            false
        } finally {
            refreshing = false
        }
    }
}

并发请求时序图

请求A (Token过期) ───▶ 刷新Token ───▶ 重试成功
                                │
请求B (Token过期) ───▶ 等待... ────┤
                                │
请求C (新请求)     ───▶ 等待... ────┤
                                │
                               刷新完成,唤醒所有

面试加分

  • synchronized + suspendCoroutine 是经典等待模式
  • 也可以用 Mutex(Kotlin协程库)替代 synchronized
  • 刷新期间的新请求要加入等待队列
  • 多个设备登录同一账号,Token 互踢的处理
  • Token 刷新的黑名单机制:主动登出时通知服务器失效旧 Token

七、网络安全配置:不是你想明文就明文

核心:Android 9+ 默认禁止明文 HTTP,Network Security Configuration 让你精确控制哪些域名可以走 HTTP。

配置文件

xml

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- 信任内置系统 CA -->
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
    
    <!-- 开发环境配置 -->
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">10.0.2.2</domain>  <!-- 模拟器本地 -->
        <domain includeSubdomains="true">localhost</domain>
        <domain includeSubdomains="true">dev.internal.com</domain>
    </domain-config>
    
    <!-- 生产环境:证书固定 -->
    <domain-config trustKitConfig="trustkit.config">
        <domain includeSubdomains="true">api.production.com</domain>
        <pin-set expiration="2027-01-01">
            <pin digest="SHA-256">base64EncodedPrimaryPin=</pin>
            <pin digest="SHA-256">base64EncodedBackupPin=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

证书固定(Certificate Pinning)

// 证书固定的原理
// 固定服务器的公钥哈希,拦截者无法伪造

// 计算证书指纹
fun calculatePin(certificate: X509Certificate): String {
    val publicKey = certificate.publicKey
    val byteInputStream = ByteArrayInputStream(publicKey.encoded)
    val encodedKeySpec = X509EncodedKeySpec(
        byteInputStream.readBytes()
    )
    val keyFactory = KeyFactory.getInstance(publicKey.algorithm)
    val key = keyFactory.generatePublic(encodedKeySpec)
    val hash = MessageDigest.getInstance("SHA-256")
        .digest(key.encoded)
    return Base64.encodeToString(hash, Base64.NO_WRAP)
}

// OkHttp 证书固定
val client = OkHttpClient.Builder()
    .certificatePinner(
        CertificatePinner.Builder()
            .add("api.github.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
            .add("api.github.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")  // 备用证书
            .build()
    )
    .build()

// 信任工具库 TrustKit
// AndroidManifest.xml 中配置
// TrustKit.init(this);
// TrustKit.validateServerCertificate(...)

防抓包实战

class AntiDebugInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        
        // 1. 检测是否使用了代理
        val proxy = System.getProperty("http.proxy")
        if (proxy != null) {
            throw SecurityException("Proxy detected: $proxy")
        }
        
        // 2. 检测 Charles/Fiddler 证书
        val certs = chain.connection()?.handshake()?.peerCertificates
        certs?.forEach { cert ->
            if (cert.toString().contains("Charles") || 
                cert.toString().contains("Fiddler")) {
                throw SecurityException("Proxy certificate detected")
            }
        }
        
        return chain.proceed(request)
    }
}

面试加分

  • <domain-config> 优先级高于 <base-config>
  • includeSubdomains="true" 包含子域名
  • HPKP(HTTP Public Key Pinning)已被 Chrome 废弃,Certificate Pinning 是更安全的方案
  • 证书固定要考虑证书更新的过渡期,固定主证书 + 备用证书
  • APP 首次安装时联网获取证书列表(证书白名单)比硬编码更灵活

八、网络状态监听 + 重试策略:可靠请求层怎么设计?

核心:可靠 ≠ 一定成功,而是失败可感知、可重试、可降级

网络状态监听

class NetworkMonitor(private val context: Context) {
    
    private val connectivityManager = 
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    
    // StateFlow 方式(推荐)
    private val _isConnected = MutableStateFlow(checkConnection())
    val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
    
    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            _isConnected.value = true
        }
        
        override fun onLost(network: Network) {
            _isConnected.value = checkConnection()
        }
        
        override fun onCapabilitiesChanged(
            network: Network,
            capabilities: NetworkCapabilities
        ) {
            // 网络变化(WiFi ↔ 移动网络)
            _isConnected.value = capabilities.hasCapability(
                NetworkCapabilities.NET_CAPABILITY_INTERNET
            )
        }
    }
    
    fun register() {
        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        connectivityManager.registerNetworkCallback(request, networkCallback)
    }
    
    fun unregister() {
        connectivityManager.unregisterNetworkCallback(networkCallback)
    }
    
    private fun checkConnection(): Boolean {
        val network = connectivityManager.activeNetwork ?: return false
        val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
        return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }
}

指数退避重试策略

class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
    
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        var response: Response? = null
        var exception: IOException? = null
        var attempt = 0
        
        while (attempt <= maxRetries) {
            try {
                response?.close()
                response = chain.proceed(request)
                
                // 判断是否可重试
                if (response.isSuccessful || !isRetryable(response.code)) {
                    return response!!
                }
            } catch (e: IOException) {
                exception = e
            }
            
            attempt++
            if (attempt <= maxRetries) {
                // 指数退避:1s, 2s, 4s...
                val delay = calculateBackoffDelay(attempt)
                println("Retry attempt $attempt after ${delay}ms")
                Thread.sleep(delay)
            }
        }
        
        throw exception ?: IOException("Max retries exceeded")
    }
    
    private fun calculateBackoffDelay(attempt: Int): Long {
        // 基础延迟 1s,最大延迟 30s,指数增长
        val baseDelay = 1000L
        val maxDelay = 30_000L
        val exponentialDelay = minOf(
            baseDelay * (1 shl (attempt - 1)),
            maxDelay
        )
        // 添加 jitter(抖动)避免惊群效应
        val jitter = (Math.random() * 0.3 * exponentialDelay).toLong()
        return exponentialDelay + jitter
    }
    
    private fun isRetryable(code: Int): Boolean {
        return code in 500..599 ||  // 服务器错误
               code == 408 ||         // 请求超时
               code == 429            // 请求过多
    }
}

可靠请求层封装

class ReliableNetworkLayer(
    private val context: Context,
    private val okHttpClient: OkHttpClient
) {
    private val networkMonitor = NetworkMonitor(context)
    private val pendingRequests = Channel<Unit>(Channel.UNLIMITED)
    
    suspend fun <T> request(call: suspend () -> T): Result<T> {
        // 1. 检查网络状态
        if (!networkMonitor.isConnected.value) {
            // 等待网络恢复
            networkMonitor.isConnected.filter { it }.first()
        }
        
        return try {
            Result.success(call())
        } catch (e: Exception) {
            when (e) {
                is IOException -> {
                    // 网络错误,尝试重试
                    Result.failure(e)
                }
                is HttpException -> {
                    // 业务错误
                    Result.failure(e)
                }
                else -> Result.failure(e)
            }
        }
    }
}

面试加分

  • ConnectivityManager 在 Android 7 之前有 BUG,网络断开可能收不到回调
  • Jetpack 的 NetworkCallback 更可靠
  • 重试策略要考虑幂等性:GET/DELETE 天然幂等,POST/PUT 需要服务端支持
  • 指数退避 + jitter 是标准做法,避免大量请求同时重试
  • Android 12+ 对后台网络请求有更严格限制

九、WebSocket vs SSE vs 轮询:实时通信怎么选?

核心:没有银弹,三种方案各有适用场景。选错方案是面试常见的送命题。

对比表

表格

特性HTTP 轮询SSEWebSocket
连接方向客户端发起服务器推送双向
实时性差(依赖轮询间隔)秒级毫秒级
服务端资源高(每次新建连接)低(保持连接)
数据格式任意text/event-stream任意
防火墙友好❌(需要特殊配置)
实现复杂度

选择决策树

需要服务器主动推送?
├── 否 → HTTP 轮询(聊天列表刷新、新闻列表)
└── 是
    ├── 需要双向通信(聊天、游戏) → WebSocket
    ├── 仅服务端推送(股价、通知) → SSE
    └── 需要兼容老浏览器 → SSE + 轮询降级

OkHttp WebSocket

class WebSocketManager(private val client: OkHttpClient) {
    
    private var webSocket: WebSocket? = null
    private val reconnectDelay = 5000L
    
    fun connect(url: String) {
        val request = Request.Builder().url(url).build()
        
        webSocket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                println("WebSocket 连接建立")
                // 认证、心跳启动
            }
            
            override fun onMessage(webSocket: WebSocket, text: String) {
                println("收到消息: $text")
                // JSON 解析、业务处理
            }
            
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                webSocket.close(1000, null)  // 正常关闭
            }
            
            override fun onFailure(t: Throwable, response: Response?) {
                println("连接失败: ${t.message}")
                // 自动重连
                scheduleReconnect()
            }
        })
    }
    
    fun send(message: Any) {
        val json = Gson().toJson(message)
        webSocket?.send(json)
    }
    
    private fun scheduleReconnect() {
        // 延迟重连
        Handler(Looper.getMainLooper())
            .postDelayed({ connect("wss://...") }, reconnectDelay)
    }
    
    fun disconnect() {
        webSocket?.close(1000, "User closed")
        webSocket = null
    }
}

SSE 实现

// Retrofit SSE
interface SseService {
    @GET("events")
    fun streamEvents(): SseSource<String>
}

// OkHttp SSE(EventSource)
class SseManager {
    
    private val eventSources = mutableMapOf<String, EventSource>()
    
    fun subscribe(url: String, listener: EventSourceListener) {
        val request = Request.Builder().url(url).build()
        val eventSourceFactory = EventSources.createFactory(okHttpClient)
        eventSources[url] = eventSourceFactory.newEventSource(request, listener)
    }
    
    fun unsubscribe(url: String) {
        eventSources[url]?.cancel()
        eventSources.remove(url)
    }
}

// 服务器端格式
// data: {"type":"price","value":123.45}
// 
// data: {"type":"notification","content":"您有新消息"}
// 
// 多个 data: 表示一条消息
// data: line1
// data: line2

面试加分

  • WebSocket 底层是 HTTP Upgrade,握手后切换协议
  • SSE 本质是 HTTP 长连接,Content-Type 是 text/event-stream
  • WebSocket 断线重连需要自己实现,SSE 浏览器自动重连
  • 心跳机制:WebSocket 需要自己发 ping/pong,SSE 是 HTTP 层面
  • IM 场景首选 WebSocket;推送通知场景 SSE 足够且更简单

十、网络安全:防篡改、防重放怎么做?

核心:明文传输 + 无签名 = 裸奔。除了 HTTPS,还要在应用层加固。

常见攻击

1. 中间人攻击(MITM):抓包、篡改请求/响应
2. 重放攻击:记录请求,反复发送骗取积分
3. 参数篡改:修改订单价格
4. 暴力破解:撞库、刷接口

完整签名方案

class RequestSigner(
    private val appSecret: String = BuildConfig.APP_SECRET,
    private val timestampProvider: () -> Long = { System.currentTimeMillis() / 1000 }
) {
    
    // 签名参数来源
    enum class SignTarget {
        URL_ONLY,      // 仅签名 URL 参数
        URL_AND_BODY,   // 签名 URL + Body
        ALL            // 签名所有字段
    }
    
    fun sign(request: Request, target: SignTarget = SignTarget.URL_AND_BODY): Request {
        val timestamp = timestampProvider()
        val nonce = generateNonce()
        
        val signData = when (target) {
            SignTarget.URL_ONLY -> request.url.toString()
            SignTarget.URL_AND_BODY -> {
                val body = request.body?.let { readBody(it) } ?: ""
                "${request.url}&body=$body"
            }
            SignTarget.All -> {
                // 包含 header、时间戳等
                val headers = request.headers.toMultimap()
                    .filter { it.key in signHeaders }
                    .entries
                    .sortedBy { it.key }
                    .joinToString("&") { "${it.key}=${it.value}" }
                "${request.url}&$headers"
            }
        }
        
        val signature = calculateSignature(
            data = signData,
            timestamp = timestamp,
            nonce = nonce
        )
        
        return request.newBuilder()
            .header("X-Timestamp", timestamp.toString())
            .header("X-Nonce", nonce)
            .header("X-Signature", signature)
            .header("X-App-Id", BuildConfig.APP_ID)
            .build()
    }
    
    private fun calculateSignature(
        data: String,
        timestamp: Long,
        nonce: String
    ): String {
        // HMAC-SHA256
        val secretKey = appSecret.toByteArray()
        val message = "$data$timestamp$nonce".toByteArray()
        
        val mac = Mac.getInstance("HmacSHA256")
        mac.init(SecretKeySpec(secretKey, "HmacSHA256"))
        val hash = mac.doFinal(message)
        
        return Base64.encodeToString(hash, Base64.NO_WRAP)
    }
    
    private fun generateNonce(): String {
        val bytes = ByteArray(16)
        SecureRandom().nextBytes(bytes)
        return Base64.encodeToString(bytes, Base64.NO_WRAP)
    }
    
    private fun readBody(body: RequestBody): String {
        val buffer = Buffer()
        body.writeTo(buffer)
        return buffer.readUtf8()
    }
}

// 服务端验证
fun verifySignature(request: Request): Boolean {
    val timestamp = request.header("X-Timestamp")?.toLongOrNull() ?: return false
    val nonce = request.header("X-Nonce") ?: return false
    val signature = request.header("X-Signature") ?: return false
    
    // 1. 时间戳检查(5分钟内)
    if (System.currentTimeMillis() / 1000 - timestamp > 300) {
        return false
    }
    
    // 2. Nonce 检查(防重放)
    if (nonceCache.contains(nonce)) {
        return false  // 已使用过
    }
    nonceCache.add(nonce)
    
    // 3. 签名验证
    val expectedSignature = calculateSignature(...)
    return signature == expectedSignature
}

防重放机制

class AntiReplayInterceptor : Interceptor {
    
    private val nonceCache = object : Cache<String, Unit>(10000) {
        // LRU Cache,保存最近使用的 nonce
    }
    
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        
        val timestamp = request.header("X-Timestamp")?.toLongOrNull()
        val nonce = request.header("X-Nonce")
        
        // 时间戳检查
        if (timestamp == null) {
            return chain.proceed(request)
        }
        
        val currentSeconds = System.currentTimeMillis() / 1000
        if (kotlin.math.abs(currentSeconds - timestamp) > 300) {
            // 超过5分钟,拒绝
            return Response.Builder()
                .request(request)
                .protocol(Protocol.HTTP_1_1)
                .code(403)
                .message("Request expired")
                .build()
        }
        
        // Nonce 检查(需要在服务端缓存)
        if (nonce != null && nonceCache.containsKey(nonce)) {
            return Response.Builder()
                .request(request)
                .protocol(Protocol.HTTP_1_1)
                .code(403)
                .message("Replay detected")
                .build()
        }
        
        return chain.proceed(request)
    }
}

面试加分

  • HTTPS 是传输层安全,签名是应用层安全,两者不冲突
  • 签名参数要包含时间戳、nonce,防止重放
  • nonce 缓存要持久化,服务重启后依然有效
  • HTTPS + 证书固定 + 应用层签名 = 三重防护
  • 敏感数据(如金额)不要放在客户端计算,服务端验签

总结

这篇文章覆盖了 Android 网络层的核心技术点:

  1. HTTP 协议演进:理解多路复用、队头阻塞的根源
  2. HTTPS 握手:证书链验证是安全基础
  3. OkHttp 架构:拦截器链、连接池是高频考点
  4. Retrofit 本质:动态代理 + Converter,简化接口定义
  5. Token 管理:并发控制 + 队列等待 + 自动重试
  6. 安全配置:证书固定、防抓包是商业 APP 标配
  7. 可靠请求:网络监听 + 指数退避
  8. 实时通信:WebSocket vs SSE 各有适用场景
  9. 应用层安全:签名 + 防重放是 HTTPS 的必要补充

记住,面试不只是背答案,而是讲清楚为什么这么设计。下次有人问你 OkHttp 原理,别只说"用了责任链模式",把源码怎么实现的、拦截器的执行顺序讲出来,面试官会对你刮目相看。