11.03 网络编程-进阶与封装
11.01、11.02 解决了"发请求",本节解决生产环境的三个进阶命题:实时通信(WebSocket 长连接)、弱网与缓存(不稳定网络下的体验保障)、安全(HTTPS 加固与抓包防护),最后给出网络层统一封装的思路——把零散接口收敛成一套可复用的网络栈。
# 一、WebSocket:全双工长连接
HTTP 是"一问一答"模型:服务端有新数据,只能等客户端来问。聊天、推送、协同编辑需要服务端主动推——WebSocket 把一次 HTTP 连接升级为双向长连接:
# 基本使用(OkHttp 原生支持)
public class WebSocketManager {
private OkHttpClient mClient = OkHttpHolder.getClient();
private WebSocket mWebSocket;
public void connect() {
Request request = new Request.Builder()
.url("wss://ws.example.com/chat") // wss = TLS加密的ws
.build();
mWebSocket = mClient.newWebSocket(request, new WebSocketListener() {
// 连接建立
@Override
public void onOpen(WebSocket webSocket, Response response) {
Log.i(TAG, "WebSocket已连接");
webSocket.send("{\"type\":\"login\",\"token\":\"...\"}"); // 发文本帧
}
// 收到服务端消息(文本帧)
@Override
public void onMessage(WebSocket webSocket, String text) {
// 切主线程处理UI(10章范式)
new Handler(Looper.getMainLooper()).post(() -> dispatchMessage(text));
}
// 连接关闭
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
Log.i(TAG, "连接关闭:" + code + " " + reason);
}
// 异常(含断线)
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
Log.e(TAG, "连接异常", t);
reconnectWithBackoff(); // 触发重连(见下)
}
});
}
public void send(String json) {
if (mWebSocket != null) {
mWebSocket.send(json);
}
}
public void disconnect() {
if (mWebSocket != null) {
mWebSocket.close(1000, "normal"); // 正常关闭码
mWebSocket = null;
}
}
}
长连接的三个必答题(对应上图 ④⑤):
- 心跳保活:客户端定时(如 30s)发
ping,服务端回pong——既防中间设备(NAT/防火墙)掐掉空闲连接,也用于"感知假在线" - 断线重连:
onFailure里触发重连,用指数退避(1s → 2s → 4s → …上限 60s)避免服务端被瞬时重连风暴打挂;重连成功后要重新鉴权(token 可能已过期) - 消息可靠性:业务侧定义消息 id + 确认机制(服务端未收到 ack 则重发),客户端按 id 去重——网络层只保证送达,不保证不重复
选型提醒:复杂的推送场景直接用成熟方案(FCM 推送、厂商推送通道、商业 IM SDK);自建 WebSocket 适合自有实时业务(IM、直播弹幕、协同)。
# 二、弱网与缓存策略
# 2.1 超时分级
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // 连接:10秒足够(含TLS)
.readTimeout(20, TimeUnit.SECONDS) // 普通读:20秒
// 大文件上传/下载的请求单独clone配置,不能一刀切
.build();
// 大文件场景:clone()共享连接池,单独放宽超时
OkHttpClient uploadClient = CLIENT.newBuilder()
.writeTimeout(120, TimeUnit.SECONDS)
.build();
# 2.2 重试与幂等
结合 11.00 的幂等概念:
- GET/PUT/DELETE(幂等):可自动重试,OkHttp 默认
retryOnConnectionFailure(true)已覆盖连接级失败 - POST(非幂等):业务层设计重试——请求带唯一 requestId,服务端按 id 去重(防重复下单)
- 重试次数上限 + 指数退避,避免"失败风暴"
# 2.3 缓存策略
三层缓存从快到慢:
- 内存缓存:会话内直接复用(如首页数据在 Application 级内存),刷新时用旧数据先渲染 + 后台拉新("缓存-网络"策略)
- HTTP 缓存:服务端下发
Cache-Control: max-age=3600/ETag,OkHttp 的 CacheInterceptor 自动生效(见 11.01)——优先推动服务端配缓存头,客户端零代码 - 本地持久缓存:关键接口(配置、字典)落 SQLite/文件,断网也能展示旧数据
// "先缓存后网络"的典型实现思路:先展示本地旧数据,再请求新数据刷新
textView.setText(LocalCache.read(KEY)); // ① 立即渲染旧数据
RetrofitManager.getConfigApi().getConfig().enqueue(new Callback<Config>() {
@Override
public void onResponse(Call<Config> call, Response<Config> response) {
if (response.isSuccessful() && response.body() != null) {
LocalCache.write(KEY, response.body()); // ② 拉新后更新缓存与UI
updateUi(response.body());
}
}
// 失败不提示:已有旧数据兜底
@Override
public void onFailure(Call<Config> call, Throwable t) { }
});
# 三、HTTPS 安全加固
# 3.1 证书锁定(防中间人抓包)
正常 HTTPS 已验证系统 CA 证书链,但用户/恶意者可安装自签证书做中间人。证书锁定把"信任谁"收紧为"只信任我们自己的证书":
// 方式一:CertificatePinner锁定证书公钥(推荐锁公钥,轮换证书不影响)
CertificatePinner pinner = new CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // 备用证书
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(pinner)
.build();
// 方式二:NetworkSecurityConfig(XML配置,Android 7.0+,含明文流量管理)
// res/xml/network_security_config.xml:
// <network-security-config>
// <domain-config cleartextTrafficPermitted="false"> ← 禁止明文http
// <domain includeSubdomains="true">api.example.com</domain>
// <pin-set expiration="2027-01-01">
// <pin digest="SHA-256">AAAA...=</pin>
// </pin-set>
// </domain-config>
// </network-security-config>
// AndroidManifest: android:networkSecurityConfig="@xml/network_security_config"
# 3.2 客户端安全红线
- 禁止信任所有证书(
X509TrustManager空实现/checkServerTrusted空方法)——等于裸奔 HTTPS,抓包工具畅行无阻 - 禁止 release 打印 BODY 日志(11.01 的 logging-interceptor),token/隐私全泄漏
- 敏感信息不进 URL:查询参数会进日志与历史记录,鉴权信息放 Header
- token 不硬编码:clientSecret 类密钥打进 APK 等于公开;服务端下发的动态 token + 刷新机制是正解
# 四、网络层统一封装思路
把 OkHttp/Retrofit 的能力收敛成一套架构(思路示意,核心是"配置单例 + 拦截器分工 + 错误归一化"):
┌─ 业务层:调用 XxxApi 接口方法(只关心业务)
├─ 归一化层:BaseCallback —— 统一处理 HTTP错误 / 业务码 / token过期(401刷新重放) / 异常转提示
├─ 拦截器层:TokenInterceptor(注入鉴权) + SignInterceptor(签名防篡改) + LogInterceptor(仅debug)
├─ 执行层:OkHttp 单例(连接池/缓存/超时/证书锁定)
└─ 转换层:Retrofit + GsonConverter + 主线程 callbackExecutor
关键设计:错误归一化——业务代码永远只处理"数据或一个业务异常",HTTP 状态码、JSON 解析失败、无网等全部在 BaseCallback 内翻译:
public abstract class BaseCallback<T> implements Callback<T> {
@Override
public void onResponse(Call<T> call, Response<T> response) {
if (response.code() == 401) {
// token过期:统一触发刷新/跳登录(Authenticator也可实现自动刷新重放)
onAuthExpired();
return;
}
if (response.isSuccessful() && response.body() != null) {
T body = response.body();
// 假设业务码在body.code中(与服务端约定)
int bizCode = ((BaseResponse) body).getCode();
if (bizCode == 0) {
onSuccess(body);
} else {
onFailure(-1, ((BaseResponse) body).getMsg());
}
} else {
onFailure(response.code(), "HTTP " + response.code());
}
}
@Override
public void onFailure(Call<T> call, Throwable t) {
// 网络异常统一翻译成用户能懂的提示
onFailure(-1, NetworkUtil.describe(t)); // 超时→"网络超时";UnknownHost→"网络不可用"
}
// 业务层只需实现这两个
public abstract void onSuccess(T data);
public abstract void onFailure(int code, String message);
protected void onAuthExpired() { }
}
# 五、常见问题与陷阱
- WebSocket 收到消息后直接操作 UI 崩溃:onMessage 在 OkHttp 子线程,必须切主线程(10 章范式)。
- 重连风暴:断网时每秒重连几十次把服务器打挂——指数退避 + 全局唯一重连入口。
- 心跳与业务消息混淆:心跳帧与服务消息用类型字段区分,收到业务消息即证明链路活着(可重置心跳计时)。
- 证书锁定写死证书公钥导致线上故障:锁定公钥而非证书文件,并至少锁两把(主证书 + 备用/备份 CA),配合过期时间(pin-set expiration)留逃生通道。
- 缓存了登录后的敏感数据:HTTP 缓存与内存缓存对鉴权接口要谨慎(
Cache-Control: no-store),避免换账号后读到旧账号数据。 - 抓包工具连不上就慌:生产 App 连不上 charles/mitmproxy 恰恰说明证书锁定与安全配置生效了,这是好事。
# 结束
本节是网络编程的进阶收官:WebSocket 长连接(握手升级、心跳保活、指数退避重连、消息可靠性,配握手示意图)、弱网与缓存(超时分级、幂等重试、三层缓存与"先缓存后网络"策略)、HTTPS 加固(证书锁定、NetworkSecurityConfig、客户端安全红线)、网络层统一封装(分层架构图 + 错误归一化 BaseCallback)。至此数据篇的网络部分完成,配套的"数据解析与图片加载"见下一节 12 数据解析。更多内容请参考:网络安全配置 (opens new window)。