11.03 网络编程-进阶与封装

2025/4/14 Android 开发基础

11.01、11.02 解决了"发请求",本节解决生产环境的三个进阶命题:实时通信(WebSocket 长连接)、弱网与缓存(不稳定网络下的体验保障)、安全(HTTPS 加固与抓包防护),最后给出网络层统一封装的思路——把零散接口收敛成一套可复用的网络栈。

# 一、WebSocket:全双工长连接

HTTP 是"一问一答"模型:服务端有新数据,只能等客户端来问。聊天、推送、协同编辑需要服务端主动推——WebSocket 把一次 HTTP 连接升级为双向长连接:

WebSocket握手与长连接示意图

# 基本使用(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;
        }
    }
}

长连接的三个必答题(对应上图 ④⑤):

  1. 心跳保活:客户端定时(如 30s)发 ping,服务端回 pong——既防中间设备(NAT/防火墙)掐掉空闲连接,也用于"感知假在线"
  2. 断线重连:onFailure 里触发重连,用指数退避(1s → 2s → 4s → …上限 60s)避免服务端被瞬时重连风暴打挂;重连成功后要重新鉴权(token 可能已过期)
  3. 消息可靠性:业务侧定义消息 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 缓存策略

三层缓存从快到慢:

  1. 内存缓存:会话内直接复用(如首页数据在 Application 级内存),刷新时用旧数据先渲染 + 后台拉新("缓存-网络"策略)
  2. HTTP 缓存:服务端下发 Cache-Control: max-age=3600 / ETag,OkHttp 的 CacheInterceptor 自动生效(见 11.01)——优先推动服务端配缓存头,客户端零代码
  3. 本地持久缓存:关键接口(配置、字典)落 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 客户端安全红线

  1. 禁止信任所有证书(X509TrustManager 空实现/checkServerTrusted 空方法)——等于裸奔 HTTPS,抓包工具畅行无阻
  2. 禁止 release 打印 BODY 日志(11.01 的 logging-interceptor),token/隐私全泄漏
  3. 敏感信息不进 URL:查询参数会进日志与历史记录,鉴权信息放 Header
  4. 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() { }
}

# 五、常见问题与陷阱

  1. WebSocket 收到消息后直接操作 UI 崩溃:onMessage 在 OkHttp 子线程,必须切主线程(10 章范式)。
  2. 重连风暴:断网时每秒重连几十次把服务器打挂——指数退避 + 全局唯一重连入口。
  3. 心跳与业务消息混淆:心跳帧与服务消息用类型字段区分,收到业务消息即证明链路活着(可重置心跳计时)。
  4. 证书锁定写死证书公钥导致线上故障:锁定公钥而非证书文件,并至少锁两把(主证书 + 备用/备份 CA),配合过期时间(pin-set expiration)留逃生通道。
  5. 缓存了登录后的敏感数据:HTTP 缓存与内存缓存对鉴权接口要谨慎(Cache-Control: no-store),避免换账号后读到旧账号数据。
  6. 抓包工具连不上就慌:生产 App 连不上 charles/mitmproxy 恰恰说明证书锁定与安全配置生效了,这是好事。

# 结束

本节是网络编程的进阶收官:WebSocket 长连接(握手升级、心跳保活、指数退避重连、消息可靠性,配握手示意图)、弱网与缓存(超时分级、幂等重试、三层缓存与"先缓存后网络"策略)、HTTPS 加固(证书锁定、NetworkSecurityConfig、客户端安全红线)、网络层统一封装(分层架构图 + 错误归一化 BaseCallback)。至此数据篇的网络部分完成,配套的"数据解析与图片加载"见下一节 12 数据解析。更多内容请参考:网络安全配置 (opens new window)。