华佗网络质量诊断器的开发实现
前言
做海外业务时,经常出现客户反馈页面问题,有时候在穷尽客户日志之后依然难以定位,因为很多问题跟 N 种因素有关,而这个“N”种有一部分你很难看到。
例如对方“白屏“,但服务端查询不到他的接口调用、资源调用、html 调用。那么到底是卡在哪一步了呢。是否是某个资源的连通性有问题呢?
例如对方反馈网页加载慢,到底是哪个 url 的加载耗时拖累的页面速度呢?
有时候我们上报上来的日志没有那么全面,此时如果用户配合的情况下,可能就需要一个”诊断工具“来让用户帮助我们诊断用户网络情况。在国内,七牛云、腾讯云是我见过曾经有使用过类似产品的公司。那我今天就参考他们的产品来自己研发一款适用于公司场景的 ”网络诊断器“。
效果展示
以下是我已经实现完成的”华佗网络诊断器“界面截图,您可点击 https://product.cuiyongjian.com/huatuo/client 查看效果演示。
效果截图如下:
支持测速国内主流站点:
支持发出链接后,实时观测对方数据:
分析竞品
以 https://ping.huatuo.qq.com/ 作为目标,分析他是如何实现相关功能的。
不过 2024 年发现,他们已经变成腾讯的 itango 了,itango 的分析在本文后半部分,这里我们先分析早期的 ping.huatuo.qq.com。
总共有以下 4 部分内容,
下面分别来看分析。
基础信息部分
GEO 地理位置,这个需要靠浏览器开启位置权限。
地理微信则通过调用 H5 的 geolocation api 获取用户坐标(具体可参考:https://cloud.tencent.com/developer/article/2270645)。进而可以看到他将用户坐标传递到了 PHP 后台: https://ping.huatuo.qq.com/index.php?btype=logic&location=22.9346222,113.3817445
接下来,我们看到 PHP 后台返回了该坐标所对应的地理位置:
1 | { |
可以看到位置精确到了道路、围栏等,这个应该就是靠各类 MapApi 查询获得的:
IP 信息部分
可以看到他向自己的 php 服务器发出了一个后台请求:https://ping.huatuo.qq.com/index.php?btype=logic&userip=1
,
然后后台返回了这样的数据:
1 | { |
即,通过后台 IP 分析的方式得到了用户的出口 IP 信息并解析出出口 IP 的城市和运营商:
至于 DNS,看起来他是先向 3 个 地址发出了 3 个 Get 请求:
1 | https://1679898507860.3599.tx-livetools.cn/s |
接下来,他又向腾讯自己的 php 服务器发出 3 次 getlocaldns 的调用:
1 | https://ping.huatuo.qq.com/getldns.php?btype=logic&d=1679898507860.3599.tx-livetools.cn |
例如 livetols.cn
所返回的数据为:
1 | { "code": 0, "msg": "succ", "data": { "ldns": "172.253.6.3" } } |
可能因为 livetools.wang
返回了 null,所以重试了很多次:
1 | { "code": 0, "msg": "succ", "data": { "ldns": "NULL" } } |
当拿到 DNS IP 后,还需要展示 DNS IP 的地理城市信息,这个时候可以看到他请求了 PHP 后台,通过后台解析传过去的 IP 从而得到地理信息。请求地址为 https://ping.huatuo.qq.com/index.php?btype=logic&ldns=172.253.6.3
。
响应内容为:
1 | { |
可能是为了分别获取几种运营商的 DNS 结果。
关于如何获取 client-side 的 dns 结果,其实比较难。这里有讨论方案。但大部分人都是说制作一个 server 服务,但那其实是服务端的结果。只有火狐才有个 client api 可以获取。
我估计腾讯这个是利用第三方服务,从不同地理位置去进行 dns resolve,从而获得不同地区的 dns 结果。
IP 如何解析成城市信息
ip 解析服务商选择,我本来选择这个,并且采用他的 nodejs 库:https://www.ip2location.com/development-libraries/ip2location/nodejs
但最终发现需要付费,而免费版发现文档和实际的 npm 包对不上,最终没搞定,最后直接找了一个 npm 包走线上 api 进行转换的。
OS 操作系统(即 UA 信息部分)
操作系统 UA 基本信息这块,感觉基本上只能靠 UA 获取。 少部分 js 开关则可以通过执行 js 看看是否可执行成功来判断。
域名速度测试
可以看到他发出了很多这样的请求:
- https://www.zhihu.com/signin?next=%2F%3Fv%3D495056.48878455436
- https://www.iqiyi.com/?iqiyi=o&v=636461.7533801475
- https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1535019502079&di=3c4f50f47ad8fb38040086b1c6431602&imgtype=0&src=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fphotoblog%2F1309%2F19%2Fc7%2F25868621_25868621_1379558672307.jpg&time=95
- https://www.youku.com/?v=982993.6814117464
- https://www.baidu.com/?v=342184.2517654643
- https://captcha.gtimg.com/?v=18912.632505241112
- https://mmbiz.qpic.cn/?v=401990.88034320483
- https://v.qq.com/?v=552660.4777167394
- https://imgcache.qq.com/?v=713720.3183543943
这一步逻辑应该相对简单,估计就是发出 get 请求并看一下总共花费的时间。但我试了有的网站会检测到第三方跨域调用时直接给报错,具体腾讯怎么绕开报错的我还要再研究一下。
上报
最后,就是上报请求了。可以看到有个 POST 请求,将所有肉眼可见的那些数据,以 json 形式上报给了后台:https://ping.huatuo.qq.com/reportdata.php?btype=logic
。
itango 分析
我们再来看 itango,首先打开页面后,就发现他先请求
https://itango.tencent.com/out/itango/myip,拿了出口ip。
返回内容:
1 | { |
然后他又发了一个拿 ldns 的请求:
https://itango.tencent.com/out/api/itango
请求参数
1 | { |
这个感觉就是让服务器去查一下这个用户出口 ip 所在区域的运营商的本地 dns 服务器 ip。
响应内容:
1 | { |
接下来又发个请求,getISPLDns:
这个感觉就是让服务器去查一下这个用户出口 ip 所属的运营商的本地 dns 服务器 ip。
1 | { |
响应:
1 | { |
接下来点击“开始测试你输入的域名”,则他会发出:https://itango.tencent.com/out/api/itango,
请求:
1 | {"Action":"HuaTuo","Method":"DoPingTask","Data":{"Domain":"finded.net","Isp":"中国电信","Country":"中国"}} |
响应(这里截取一小段):
1 | { |
然后还看到发出了一个:
1 | { |
响应:
1 | { |
这就拿到了他的 dns 解析后的目标 ip。这里肯定是走的服务端解析。
然后还有这个域名的网络延时 370ms,他怎么得到的呢。可以看到当你点击查询按钮后,他还发了一个 GET 请求:
1 | https://www.baidu.com/?v=792684.9269718115 |
他应该就是 get 一下子然后靠时间戳相减得到的耗时结果。
至于下行带宽、有效 RTT、Flash 是否支持之类的。感觉他可能是通过前端发请求算的,因为我没有找到他相关请求有返回这个东西。
我认为:有效 RTT,可以通过精准记录用户发出一个 1 字节大小信息的时间戳—-服务器收到的时间戳—服务器响应开始—-浏览器收到首字节这样来得到(甚至可以用一个 iframe 然后依靠 chrome performance 来精准拿到这个首字节时间)。
然后就可以推算出用户—》服务器首字节的去向 RTT,并且再求出服务器—》浏览器首字节的回程 RTT。(前提:浏览器时间是准确的。不过我们如果只是纯算完整来回的合计 RTT 的话,其实就不需要手机准确)
带宽测试
至于带宽,首先浏览器提供了一个获取用户带宽的 api,但是这个仅供参考不一定是准的。为了获得真实带宽速度,则可以:向服务器请求一个 10M 大文件,求他下载完成的时间。然后设置 t=总时间减去上面算出来的 rtt。然后用 10M 除以 t,得到每秒传输的字节数,再乘以 8 就是家庭带宽值。注意:如果服务器端带宽是瓶颈,则这个结果可能就不是代表用户自己的带宽,而是服务器端所购买的带宽。
其他信息获取
接下来,为了打造我自己的网络诊断器,我把其他信息获取方式也整理并记录在此。
获取 cpu 信息
获取 cpu 类型,这个其实只有 windows 可以。如果你去看飞书,会发现他们 mac 上会给你弹 2 个芯片按钮让你选择下载,因为 mac 其实他们也判断不出来。
深色模式适配
可参考网上方案实现:https://juejin.cn/post/7298997940019085366
chrome 调试切换深色模式的调试方法:https://juejin.cn/post/7298997940019085366
服务器授时时间
有时候手机端需要依赖一个很准确的时间来做一些事情,所以我们不能依赖客户的设备的时间。而获取服务端精准授时的方法在业界有通用的 NTP 协议。我参考类似的策略并基于 performance api 的相对时间变化,实现了一个 Webapp 内的服务器精准授时从,从而可以在 webapp 运行期间任意时刻都拿到此刻的服务器准确时间。
前端如何精准授时(即:立刻拿到服务端时间),注意这里并不是说,实时从服务器拿到当前准确时间,而是在应用运行过程中随时去拿到当前服务器准确时间。
因为我们不可能每次用都去调用后台接口,因此我们需要应用初始化的时候就拿到。后续就靠时间推移来算。
应用初始时候,先记下服务器准确时间戳和当时的客户端时间戳。
那一刻服务器准确时间戳 server_init_time = server_init_time - delta / 2;
那一刻的客户端时间戳:local_init_time。
客户端这里,为了防止用户在后续改动时间,我们改成用相对时间,例如相对开机时常 或 函数相对运行时长;对前端来说就是用网页打开时长吧:performance.timing.now。
即:local_init_tickout = performance.timing.now在后续某一刻
server_now_time = server_init_time + (local_now_tickcount - local_init_tickout)
对于前端来说。就等于:
server_now_time = server_init_time + (performance.timing.now() - local_init_tickout)
其中 performance.now 应该就是 performance.timeOrigin 到此刻经历的时间 duration。
总体原理:
https://cloud.tencent.com/developer/article/1358922
通过服务器时间加上 RTT 除以 2,得到最新客户端时间。
NTP 协议和安卓:https://juejin.cn/post/7099256450676949006
performance.now 和 Date.now()对比:https://cloud.tencent.com/developer/article/2144005