# 网络调优

小程序和小游戏网络相关 API 使用方式相同, 所以我们用网络接口来统称

# 网络接口的构成

网络接口主要包括四个类型

  • request
  • download
  • upload
  • websocket

# 不同平台的实现

# Android

  • request 接口从客户端 7.0.10 版本开始使用 Chromium 内网络相关部分封装的底层组件 (cronet), 之前版本使用 HttpURLConnection 系统组件 (系统组件依赖系统实现会有平台兼容性问题, 我们建议用新版本微信来进行调试)
  • download 接口从客户端 7.0.12 版本开始使用 cronet 组件, 之前版本使用 HttpURLConnection 组件
  • upload 接口目前仍在使用 HttpURLConnection 组件
  • websocket 接口从客户端 7.0.4 版本开始使用微信底层组件 wcwss, 并在 7.0.10 版本优化了调用性能

# iOS

  • request/download 接口从客户端 8.0.3 版本开始使用 cronet 组件, 之前版本使用 NSURLSession 系统组件
  • upload 接口目前仍在使用 NSURLSession 组件
  • websocket 接口从客户端 7.0.20 版本开始使用微信底层组件 wcwss, 之前版本使用 SRWebSocket 组件

# 易误解的概念

# success/fail/complete 回调

  • 对于 request/download/upload 接口, 回调代表网络请求的最终结果
  • 对于 websocket 接口, 回调仅代表接口调用结果, 应当监听其具体事件来获取真实的网络连接/请求状态

# wx.sendSocketMessage/SocketTask.send

早期单个小程序只允许同时存在一条 WebSocket 连接, 所以老版本基础库 WebSocket 相关接口都直接设计在了 wx 上:

  • wx.connectSocket
  • wx.onSocketOpen
  • wx.sendSocketMessage
  • wx.onSocketMessage
  • wx.closeSocket
  • wx.onSocketClose
  • wx.onSocketError

现在单个小程序允许同时存在多个 WebSocket 连接, 原有接口设计并不能满足需求, 于是基础库在 1.7.0 版本之后增加了 SocketTask 的概念, 通过不同的实例来管理多条连接:

  • wx.connectSocket
  • SocketTask.onOpen
  • SocketTask.send
  • SocketTask.onMessage
  • SocketTask.close
  • SocketTask.onClose
  • SocketTask.onError

原有的 wx.connectSocket 接口在新版本设计中承载了创建实例 new SocketTask 的用途, 所以除了 wx.connectSocket 以外, 不应该使用其它任何挂在 wx 上的 WebSocket 接口; 在 wx.connectSocket 调用后, 请立即同步监听 SocketTask.onOpen, 否则可能会漏掉 onOpen 通知

# 性能分析

# Android

  • request/download 接口从客户端 7.0.12 版本开始, 回调中提供了 profile 信息, 给出了网络连接过程中关键时间点的耗时信息, 具体含义如下
名称 含义
redirectStart 第一个 HTTP 重定向发生时的时间. 有跳转且是同域名内的重定向才算, 否则值为 0
redirectEnd 最后一个 HTTP 重定向完成时的时间. 有跳转且是同域名内部的重定向才算, 否则值为 0
fetchStart 组件准备好使用 HTTP 请求抓取资源的时间, 这发生在检查本地缓存之前
domainLookUpStart DNS 域名查询开始的时间, 如果使用了本地缓存 (即无 DNS 查询) 或持久连接, 则与 fetchStart 值相等
domainLookUpEnd DNS 域名查询完成的时间, 如果使用了本地缓存 (即无 DNS 查询) 或持久连接, 则与 fetchStart 值相等
connectStart TCP 开始建立连接的时间, 如果是持久连接, 则与 fetchStart 值相等. 注意如果在传输层发生了错误且重新建立连接, 则这里显示的是新建立的连接开始的时间
connectEnd TCP 完成建立连接的时间 (完成握手), 如果是持久连接, 则与 fetchStart 值相等. 注意如果在传输层发生了错误且重新建立连接, 则这里显示的是新建立的连接完成的时间. 注意这里握手结束, 包括安全连接建立完成、SOCKS 授权通过
SSLconnectionStart SSL 建立连接的时间, 如果不是安全连接, 则值为 0
SSLconnectionEnd SSL 建立完成的时间, 如果不是安全连接, 则值为 0
requestStart HTTP 请求读取真实文档开始的时间 (完成建立连接), 包括从本地读取缓存. 连接错误重连时, 这里显示的也是新建立连接的时间
requestEnd HTTP 请求读取真实文档结束的时间
responseStart HTTP 开始接收响应的时间 (获取到第一个字节), 包括从本地读取缓存
responseEnd HTTP 响应全部接收完成的时间 (获取到最后一个字节), 包括从本地读取缓存
rtt 当次请求连接过程中实时 rtt
estimate_nettype 评估的网络状态 unknown, offline, slow 2g, 2g, 3g, 4g, last/0, 1, 2, 3, 4, 5, 6
httpRttEstimate 协议层根据多个请求评估当前网络的 rtt (仅供参考)
transportRttEstimate 传输层根据多个请求评估的当前网络的 rtt (仅供参考)
downstreamThroughputKbpsEstimate 评估当前网络下载的kbps, 根据最近的几次请求的rtt, 回包情况, 结合当前的网络情况, 进行的一个网络评估结果
throughputKbps 当前网络的实际下载kbps, 根据本次请求实际计算的一个下载值, 从开始请求到 请求结束收到的 字节数 * 8/请求耗时
peerIP 当前请求的目标IP
port 当前请求的目标端口
protocol 当前请求使用的协议
socketReused 是否复用连接
sendBytesCount 发送的字节数
receivedBytedCount 收到字节数

整个请求链路为 DNS -> Connect -> SSL -> request -> response; 表中 rtt 是连接过程中实时的 rtt, 每个阶段都会更新, 而 httpRttEstimate 和 transportRttEstimate 是结合前序请求计算的综合值

  • websocket 接口从客户端 7.0.12 版本开始, 在 onOpen 回调中提供了 profile 信息, 给出了网络连接过程中关键时间点的耗时信息, 具体含义如下
名称 含义
fetchStart 组件准备好使用 SOCKET 建立请求的时间, 这发生在检查本地缓存之前
domainLookUpStart DNS 域名查询开始的时间, 如果使用了本地缓存 (即无 DNS 查询) 或持久连接, 则与 fetchStart 值相等
domainLookUpEnd DNS 域名查询完成的时间, 如果使用了本地缓存 (即无 DNS 查询) 或持久连接, 则与 fetchStart 值相等
connectStart 开始建立连接的时间, 如果是持久连接, 则与 fetchStart 值相等. 注意如果在传输层发生了错误且重新建立连接, 则这里显示的是新建立的连接开始的时间
connectEnd 完成建立连接的时间 (完成握手), 如果是持久连接, 则与 fetchStart 值相等. 注意如果在传输层发生了错误且重新建立连接, 则这里显示的是新建立的连接完成的时间. 注意这里握手结束, 包括安全连接建立完成、SOCKS 授权通过
rtt 单次连接的耗时, 包括 connect, tls
handshakeCost 握手耗时
cost 上层请求到返回的耗时

整个请求链路为 DNS -> Connect; 表中 connectEnd - connectStart 代表纯 tcp 连接耗时, domainEnd - domainStart 代表域名解析耗时; 上述两步耗时加上 handshakeCost 代表单次连接请求的耗时

# iOS

  • request/download 接口从客户端 8.0.3 版本开始提供 profile 能力
  • websocket 接口从客户端 7.0.20 版本开始提供 profile 能力

# 提示

  • 当遇到网络问题时, 除了判断网络状态是否连通外, 还可以通过 rtt 来分析用户当前网络状况, 用以动态调整超时参数
  • 网络请求提供 enableProfile 参数, 默认值为 true, 可以通过传入 false 关闭

# 优化建议

# 前后台切换

小程序切后台 5s 后, 会中断网络请求, 开发者会收到 interrupted 的回调, 此时需要做好兼容逻辑

# 网络状态变化

当用户网络状态变化时会通过事件 wx.onNetworkStatusChange 进行通知, 不少网络问题是断网引起的, 可以通过此事件给用户更好的提示

# 弱网状态变化

基础库从 2.19.0 版本开始, 提供 wx.onNetworkWeakChange 弱网变化通知, 很多超时类的问题都是用户处于弱网引起的, 可以通过此事件给用户更好的提示

在最近的八次网络请求中, 出现下列三个现象之一则判定弱网

  • 出现三次以上连接超时
  • 出现三次 rtt 超过 400
  • 出现三次以上的丢包

弱网事件通知规则是: 弱网状态变化时立即通知, 状态不变时 30s 内最多通知一次

# request/download 新协议

从 Android 7.0.12 / iOS 8.0.3 开始, 提供下面三个新参数

名称 含义
enableHttp2 如果后台支持, 尝试使用 Http2 协议
enableQuic 如果后台支持, 尝试使用 Quic 协议
enableCache 缓存内容, 相同请求优先读取本地内容

h2 连接速度更快, 建议支持, 这里需要注意 h2 的 header 是需要为全小写, 打开 enableHttp2 开关前需要注意代码逻辑

# perMessageDeflate

压缩参数目前已在 Android 和 iOS 上全量支持

# 问题排查

# 不同平台的错误返回规则

# Android

cronet 的错误返回可以参考: https://chromium.googlesource.com/chromium/src/+/master/net/base/net_error_list.h

WebSocket 接口常见错误

名称 含义
Underlying Transport Error 异常, 大概率无网络引起
Timer Expired 超时, 弱网或无网
The total timed out 超时, 弱网或无网
TLS handshake failed tls 协商失败
TLS handshake timed tls 协商超时, 可以考虑重试
Invalid HttpCode 服务器配置有误

# iOS

cronet 的错误返回参考同 Android

upload 一般返回汉语信息加上 kcferrordomaincfnetwork 可以直接在苹果开发者官网上搜索到具体的对应错误信息, 协助分析解决

# ipv6 慢的问题

Android HttpURLConnection 是按照 RFC 3484 顺序尝试每个 ip 地址, 这里应该是 v6 优先, 但是系统尝试 v6 连接时超时就会按顺序再去尝试 v4, 虽然最后也有可能在设置的 60s 超时时间内完成, 但是整体耗时还是变长了, 现象就是 request 接口的请求时间很长. 在客户端 7.0.10 版本切换 cronet 后已经解决此问题

# 证书问题

证书的注意事项已有文档说明: https://developers.weixin.qq.com/minigame/dev/guide/base-ability/network.html

  1. 证书过期或无效

可以通过 https://myssl.com/ssl.html 或其他在线工具验证, 因为 Android 手机的兼容性问题, 验证结果并不保证对所有 Android 机器都有效

  1. 证书链不完整

Android 的根证书不全, 如果服务器是使用中间证书, 而 Android 手机上又找不到相应的根证书, 就会出现相关的 SSL 错误, 此时需要服务器配置完整证书链

  1. wss 协议走 80 端口不成功

80 端口对应 http 默认不做证书校验, wss 应当选用 443 端口

# not in domain url

请求 url 不在域名列表中, 遇到这个问题有几种可能

  1. 请求 url 不在 mp 配置的域名列表里
  2. 重定向后的 url 不在域名列表里
  3. websocket 请求的端口没有配置
  4. 配置的域名未生效 (极低概率)

# network is down

iOS 14 系统新增了本地网络开关, 如果关闭则局域网不通, 系统接口报错 network is down, 目前系统未提供检测开关方法, 开发者需要根据错误信息提示用户打开权限