华佗网络质量诊断器的开发实现

前言

做海外业务时,经常出现客户反馈页面问题,有时候在穷尽客户日志之后依然难以定位,因为很多问题跟 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"code": 0,
"msg": "succ",
"data": {
"status": 0,
"message": "query ok",
"request_id": "d133d7da-1da4-4386-a95d-6e8c4911eaa3",
"result": {
"location": { "lat": 22.934622, "lng": 113.381744 },
"address": "\u5e7f\u4e1c\u7701\u5e7f\u5dde\u5e02\u756a\u79ba\u533a\u76db\u6cf0\u8def170\u53f7",
"formatted_addresses": {
"recommend": "\u756a\u79ba\u533a\u76db\u6cf0\u82b1\u56ed(\u5317\u533a)",
"rough": "\u756a\u79ba\u533a\u76db\u6cf0\u82b1\u56ed(\u5317\u533a)"
}
// 此处仅摘抄一部分
}
}
}

可以看到位置精确到了道路、围栏等,这个应该就是靠各类 MapApi 查询获得的:

IP 信息部分

可以看到他向自己的 php 服务器发出了一个后台请求:https://ping.huatuo.qq.com/index.php?btype=logic&userip=1
然后后台返回了这样的数据:

1
2
3
4
5
6
7
8
9
10
11
12
{
"code": 0,
"msg": "succ",
"data": {
"ip": "58.248.226.14",
"isp": [
"\u4e2d\u56fd\u8054\u901a",
"\u5e7f\u4e1c\u7701",
"\u5e7f\u5dde\u5e02"
]
}
}

即,通过后台 IP 分析的方式得到了用户的出口 IP 信息并解析出出口 IP 的城市和运营商:

至于 DNS,看起来他是先向 3 个 地址发出了 3 个 Get 请求:

1
2
3
https://1679898507860.3599.tx-livetools.cn/s
https://1679899117252.804.tx-livetools.com/s
https://1679898328379.1794.tx-livetools.wang/s

接下来,他又向腾讯自己的 php 服务器发出 3 次 getlocaldns 的调用:

1
2
3
https://ping.huatuo.qq.com/getldns.php?btype=logic&d=1679898507860.3599.tx-livetools.cn
https://ping.huatuo.qq.com/getldns.php?btype=logic&d=1679898328379.1794.tx-livetools.wang
https://ping.huatuo.qq.com/getldns.php?btype=logic&d=1679899117252.804.tx-livetools.com

例如 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
2
3
4
5
6
7
{
"code": 0,
"msg": "succ",
"data": {
"isp": ["google", "province:", "\u9999\u6e2f\u7279\u522b\u884c\u653f\u533a"]
}
}

可能是为了分别获取几种运营商的 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 看看是否可执行成功来判断。

域名速度测试

可以看到他发出了很多这样的请求:

这一步逻辑应该相对简单,估计就是发出 get 请求并看一下总共花费的时间。但我试了有的网站会检测到第三方跨域调用时直接给报错,具体腾讯怎么绕开报错的我还要再研究一下。

上报

最后,就是上报请求了。可以看到有个 POST 请求,将所有肉眼可见的那些数据,以 json 形式上报给了后台:https://ping.huatuo.qq.com/reportdata.php?btype=logic

itango 分析

我们再来看 itango,首先打开页面后,就发现他先请求
https://itango.tencent.com/out/itango/myip,拿了出口ip。

返回内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"data": {
"AsnInfo": {
"Address": "中国广东省广州市番禺区",
"AsId": 4134,
"BackboneISP": "中国电信",
"City": "广州市",
"Country": "中国",
"CreateTime": "2024-10-12 14:34:39",
"FrontISP": "中国电信",
"IP": "119.xx.xx.227",
"Id": 18321,
"Latitude": 22.93756,
"Longitude": 113.383917,
"Province": "广东省",
"Region": "番禺区"
},
"Code": 0,
"ErrMessage": "",
"IP": "119.xx.xx.227"
},
"msg": "",
"status": 0
}

然后他又发了一个拿 ldns 的请求:

https://itango.tencent.com/out/api/itango
请求参数

1
2
3
4
5
{
"Action": "HuaTuo",
"Method": "GetLdns",
"Data": { "Random": "1728718870287.3538" }
}

这个感觉就是让服务器去查一下这个用户出口 ip 所在区域的运营商的本地 dns 服务器 ip。

响应内容:

1
2
3
4
5
{
"data": ["172.217.44.216"],
"msg": "",
"status": 0
}

接下来又发个请求,getISPLDns:
这个感觉就是让服务器去查一下这个用户出口 ip 所属的运营商的本地 dns 服务器 ip。

1
2
3
4
5
{
"Action": "HuaTuo",
"Method": "GetIspLdns",
"Data": { "Isp": "中国电信", "Province": "广东省" }
}

响应:

1
2
3
4
5
{
"data": "202.96.128.86;202.96.134.133;202.96.128.166;202.96.134.33",
"msg": "",
"status": 0
}

接下来点击“开始测试你输入的域名”,则他会发出:https://itango.tencent.com/out/api/itango,

请求:

1
{"Action":"HuaTuo","Method":"DoPingTask","Data":{"Domain":"finded.net","Isp":"中国电信","Country":"中国"}}

响应(这里截取一小段):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"data": {
"Detail": [
{
"Addr": "finded.net",
"Af": 4,
"AgentAsId": 134768,
"AgentCity": "上海市",
"AgentCountry": "中国",
"AgentISP": "中国电信",
"AgentProvince": "上海市",
"AgentRemoteIP": "113.142.145.32",
"AvgRttMicro": 0,
"AvgRttMilli": 0,
"BuildinAf": 4,
"BuildinAgentId": "943449553",
"BuildinAgentPublicIP": "",
"BuildinAgentRemoteIP": "113.142.145.32",
"BuildinAgentVersion": "itango0.5.21",
"BuildinCSRecvTimestampNs": 1728718300090462500,
"BuildinCSStoreTimestampNs": 1728718300290878500,
"BuildinDurationNano": 5127689473,
"BuildinErrMessage": "",
"BuildinExcMode": "once",

然后还看到发出了一个:

1
2
3
4
5
6
7
8
9
10
11
{
"Action": "HuaTuo",
"Method": "GetDomainParseIp",
"Data": {
"Domain": "finded.net",
"Isp": "中国电信",
"Province": "广东省",
"Country": "中国",
"ClientIp": "119.145.66.227"
}
}

响应:

1
2
3
4
5
{
"data": "183.2.172.185;183.2.172.42",
"msg": "",
"status": 0
}

这就拿到了他的 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 运行期间任意时刻都拿到此刻的服务器准确时间。

前端如何精准授时(即:立刻拿到服务端时间),注意这里并不是说,实时从服务器拿到当前准确时间,而是在应用运行过程中随时去拿到当前服务器准确时间。
因为我们不可能每次用都去调用后台接口,因此我们需要应用初始化的时候就拿到。后续就靠时间推移来算。

  1. 应用初始时候,先记下服务器准确时间戳和当时的客户端时间戳。
    那一刻服务器准确时间戳 server_init_time = server_init_time - delta / 2;
    那一刻的客户端时间戳:local_init_time。
    客户端这里,为了防止用户在后续改动时间,我们改成用相对时间,例如相对开机时常 或 函数相对运行时长;对前端来说就是用网页打开时长吧:performance.timing.now。
    即:local_init_tickout = performance.timing.now

  2. 在后续某一刻
    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