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 轮询 | SSE | WebSocket |
|---|---|---|---|
| 连接方向 | 客户端发起 | 服务器推送 | 双向 |
| 实时性 | 差(依赖轮询间隔) | 秒级 | 毫秒级 |
| 服务端资源 | 高(每次新建连接) | 中 | 低(保持连接) |
| 数据格式 | 任意 | 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 网络层的核心技术点:
- HTTP 协议演进:理解多路复用、队头阻塞的根源
- HTTPS 握手:证书链验证是安全基础
- OkHttp 架构:拦截器链、连接池是高频考点
- Retrofit 本质:动态代理 + Converter,简化接口定义
- Token 管理:并发控制 + 队列等待 + 自动重试
- 安全配置:证书固定、防抓包是商业 APP 标配
- 可靠请求:网络监听 + 指数退避
- 实时通信:WebSocket vs SSE 各有适用场景
- 应用层安全:签名 + 防重放是 HTTPS 的必要补充
记住,面试不只是背答案,而是讲清楚为什么这么设计。下次有人问你 OkHttp 原理,别只说"用了责任链模式",把源码怎么实现的、拦截器的执行顺序讲出来,面试官会对你刮目相看。