网络优化:从现有网络层出发

项目 trade-app

  • 网络
  • 弱网
  • 重连
  • 请求合并
  • 压缩
  • 超时

1. 背景与问题

交易 App 最容易出问题的场景不在办公室的 Wi-Fi 下,而在地铁、电梯、出门时 Wi-Fi 切到 4G 的那几秒。典型表现有三种:

  1. 连接看起来还在,其实已经死了:行情不跳,下单没反应,界面却没有任何提示。
  2. 请求叠在一起:网络恢复的瞬间,页面出现、重连成功、推送补发同时触发查询。
  3. 超时和重试没有统一口径:有的请求等很久,有的立即失败,交易指令该不该重试也说不清。

这篇分两部分写。第 2、3 节只写项目里已经有的,每一条都能在代码里找到;第 4 到 8 节每节先写现状,再写建议,建议一律标为「建议(未做)」,不代表已经实现,也不写任何收益数字。

三条通道从同一套首版骨架起步,之后逐步加上重连节流、可达性处理和单飞。资金查询的合并窗口与强制通道、页面基类统一监听网络与网关状态也在这一层;前者在请求频率控制一篇里详写,长连接本身的建连与分发见两条长连接。


2. 项目里的网络层全景

三条通道信号全局通知全局通知检查凭证有效期
主动断开有凭证连,无凭证断
网络可达性变化回前台进后台用户凭证变化HTTP 客户端 普通会话 15 秒 / 长任务会 话 300 秒行情 WebSocket交易 Socket.IO 鉴权 / 期货 / 证券

3. 已有做法(逐项有证据)

主题 项目里的做法
HTTP 会话与超时 基于 Alamofire;普通请求 15 秒超时;上传等长任务用单独的会话,300 秒超时
请求跟随页面取消 请求可以绑定一个持有者,持有者释放时取消它名下的请求,回调也不再执行
网络可达性 监听可达性,任何状态变化都发全局通知;恢复可达时检查用户凭证有效期
网络变化后的交易连接 延迟 4 秒检查,没连上才强制重连;新检查取消旧检查
回前台 交易:三个命名空间各自连接,6 秒后再检查一次;行情:计数清零后重连
进后台 行情主动断开,后台不重连
网络变化后的行情连接 计数清零;还没登录才重连
库内重连 Socket.IO 无限次重连,等待 2 到 6 秒;强制只走 WebSocket 传输
应用层重连节流 两条长连接各有节流窗口和检查定时器;鉴权失败后有冷却
建连超时 行情建连请求的超时 = 连接检查周期 − 2 秒
请求合并 HTTP 同地址单飞;资金查询 1 秒窗口 + 强制通道;主推后 1 秒合并再拉
公共请求头 HTTP:设备标识、语言、有条件的广告标识,需要时加鉴权头
握手头 交易长连接只在握手时带内容类型、鉴权、平台、设备标识、版本
编码与压缩 长连接用 Protobuf;Socket.IO 开压缩;HTTP 为 JSON,用系统默认压缩协商
弱网下的失败 交易指令未连接即丢弃;HTTP 无通用重试;续期失败不再自动重试
状态提示 页面基类统一监听网络与网关状态,子页面只覆写「断开了」「恢复了」

3.1 三条通道各自管什么

通知通知检查凭证HTTP客户端普通会话 15 秒长任务会话 300 秒持有者 → 在途请求地址 → 是否在途请求前检查凭证行情连接检查周期与节流窗口重连计数上限登录计数上限进后台断开交易连接库内重连 2 到 6 秒每个命名空间一个检查定时器网络变化 4 秒后检查回前台 6 秒后检查鉴权失败冷却可达性监听状态变化发通知恢复时检查凭证

3.2 网络变化之后发生了什么

页面基类行情连接交易连接可达性监听页面基类行情连接交易连接可达性监听网络变化;恢复可达时查凭证有效期全局通知取消旧检查,4 秒后再查全局通知计数清零,未登录才重连全局通知,更新提示4 秒到,未连接则强制重连

3.3 合并:已经有三处

做法 在哪 合并的单位 窗口
单飞 HTTP 客户端 接口地址 一个请求在途期间
合并窗口 + 强制通道 证券资金查询 查询类型 1 秒,首次调用后延迟发出
主推后合并再拉取 交易侧的行情到持仓中间层 主推类型 1 秒

单飞的实现是一个以地址为键的「在途」字典:同一地址已有请求在途时,新的调用立即收到一个「请求中」的本地返回码。这类标志最怕提前返回时忘了复位,见第 9 节。


4. Wi-Fi 与 4G / 3G 切换

现状:只区分「状态变了」,任何变化都走「延迟检查 → 没连上才重连」。处理器里没有按接口类型(Wi-Fi / 蜂窝)分支,也没有读取蜂窝制式。

观察(从代码推断):Wi-Fi 切到蜂窝时,旧的 TCP 连接可能处于半开状态,库仍然报告「已连接」,检查就会认为不用重连。交易侧收到心跳应答只是清零计数,没有用它判定连接已死,所以半开状态只能等库自己超时发现。

现状
网络变化库报告已连接,不处理库报告未连接
已连接检查中重连
建议(未做)
接口类型变化心跳连续无应答确认失效新连接就绪
在线重建可疑

建议(未做):

  • 用 NWPathMonitor 监听路径变化,读取 usesInterfaceType、isExpensive、isConstrained。接口类型一变就主动重建两条长连接,而不是只看「是否已连接」。
  • 用 CTTelephonyNetworkInfo 读取蜂窝制式,在 3G 下放宽超时、降低行情推送频率或缩小订阅范围。
  • 用应用层心跳判定半开连接:连续若干次没收到心跳应答,就主动断开重连。
交易连接行情连接连接管理路径监听交易连接行情连接连接管理路径监听以下为建议(未做)接口从 Wi-Fi 变为蜂窝主动断开并重连主动断开并重连就绪,重新订阅鉴权成功,柜台就绪通知页面恢复,按需补查

5. 请求合并

现状:见第 3.3 节。三处合并各自手写,「标记 + 延迟」的逻辑散在不同文件里。

建议(未做):

  • 抽一个通用合并器:键、窗口长度、首发还是尾发,标记用 defer 保证一定复位。
  • 读请求按「方法 + 地址 + 参数」做签名,在途期间相同签名共享同一个结果,而不是像现在的单飞那样让后来者直接拿到「请求中」。
  • 合并命中次数打点,按页面汇总,用来判断哪些触发源还在重复。
建议(未做)
否是
调用 1签名在途?调用 2发请求并登记挂到同一个结果上结果回来,通知所有等待者
现状
否是
调用 1地址在途?调用 2发请求立即返回请求中

6. 请求头瘦身

现状:每个 HTTP 请求都带设备标识、语言,有条件地带广告标识,需要凭证的再加鉴权头。长连接只在握手时带一次。

交易网关业务中台客户端交易网关业务中台客户端建议(未做):静态字段在登录时报一次,由服务端绑定到会话请求 1(设备标识、语言、鉴权……)请求 2(同样的静态字段再带一次)握手(平台、设备标识、版本,只带一次)之后的指令只带信封

建议(未做):

  • 设备标识、平台、版本这类静态字段,只在登录或握手时上报一次,由服务端绑定到会话上。
  • 去掉业务不用的字段。
  • 如果服务端支持 HTTP/2,重复的头部本身会被压缩,瘦身的收益要先量:用 URLSessionTaskMetrics 看请求头字节数,再决定值不值得改。

7. 响应压缩:gzip 与 Protobuf

现状:长连接是 Protobuf,并打开了 Socket.IO 的压缩选项;HTTP 是 JSON,依赖系统默认的压缩协商。仓库里没有 Protobuf 与 JSON 体积对比的材料,这里不写数字。

通道 编码 压缩 建议(未做)
行情 WebSocket Protobuf 未见额外压缩 若服务端支持,评估 WebSocket 的逐消息压缩扩展
交易 Socket.IO Protobuf 打开压缩选项 保持
HTTP JSON 系统默认协商 大列表接口确认服务端开启 gzip / br;高频接口评估改 Protobuf;较大的上传压缩请求体

8. 弱网超时与重试

现状:HTTP 统一 15 秒,长任务 300 秒;交易指令不补发;行情登录在检查周期内没成功就再发一次,有上限;续期失败后的自动重试被删掉,等下一个触发点再续。

建议(未做):

  • 按接口类型分级超时:查询短、上传长、续期单独设置。
  • 只对幂等的读请求做指数退避 + 随机抖动的重试,并设总时长上限;下单、撤单、改单这类交易指令绝不自动重试。
  • 弱网时缩小行情订阅范围,只保留屏幕内的代码。
  • 失败分类上报:DNS、建连、TLS、超时、服务端错误分开统计。
交易指令非幂等写幂等读否是
请求失败请求类型不重试,交给用户决定已重试次数 / 总时长超限?指数退避 + 抖动后重试放弃,展示错误以上为建议(未做)

9. 踩过的坑

单飞标志泄漏。 持有者已释放时提前返回,没有复位在途标志,之后同一地址的请求都被当成「请求中」直接返回;修复是在这个分支里补上复位。任何「先置标志、异步后复位」的逻辑,都要检查所有提前返回的路径。它和续期的关系见凭证续期第 5 节。

重试要看触发点有多密。 续期失败后的自动重试已经删掉,改为等下一个触发点:续期的触发点本来就多,再叠加重试,弱网下会连续发出续期请求,见凭证续期第 5 节。第 8 节的退避重试建议因此只针对幂等读请求,并设总时长上限。

交易指令不能「贴心」地补发。 网络恢复后自动补发下单,价格和用户的意图可能都已经变了;项目里交易指令在未连接时直接丢弃,由上层按失败处理,见两条长连接第 4.4 节。


10. 对比与取舍

主题 已有 建议(未做)
网络切换 状态变化后延迟检查「是否已连接」 接口类型一变就主动重建;心跳判死
请求合并 单飞、1 秒窗口、主推后合并,各自手写 通用合并器;按签名共享结果;命中打点
请求头 每个请求带静态字段 静态字段握手时报一次;先量再改
压缩 长连接 Protobuf;交易侧压缩选项;HTTP 依赖系统 大接口确认 gzip / br;高频接口评估 Protobuf
超时 统一 15 秒 / 300 秒 按接口类型分级
重试 无通用重试;续期重试已删 只对幂等读做退避 + 抖动;交易指令绝不重试
度量 没有网络质量打点 失败分类上报;请求头字节数、合并命中数