# 网络调优
小程序和小游戏网络相关 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
- 证书过期或无效
可以通过 https://myssl.com/ssl.html 或其他在线工具验证, 因为 Android 手机的兼容性问题, 验证结果并不保证对所有 Android 机器都有效
- 证书链不完整
Android 的根证书不全, 如果服务器是使用中间证书, 而 Android 手机上又找不到相应的根证书, 就会出现相关的 SSL 错误, 此时需要服务器配置完整证书链
- wss 协议走 80 端口不成功
80 端口对应 http 默认不做证书校验, wss 应当选用 443 端口
# not in domain url
请求 url 不在域名列表中, 遇到这个问题有几种可能
- 请求 url 不在 mp 配置的域名列表里
- 重定向后的 url 不在域名列表里
- websocket 请求的端口没有配置
- 配置的域名未生效 (极低概率)
# network is down
iOS 14 系统新增了本地网络开关, 如果关闭则局域网不通, 系统接口报错 network is down, 目前系统未提供检测开关方法, 开发者需要根据错误信息提示用户打开权限