Globalping 全球延迟与 MTR 路由测试
Globalping 全球延迟测试与 MTR 路由测试通过分布式探针从不同国家测量目标,而不是仅从访问者本地网络发起请求。两项工具复用 useGlobalpingMeasurement 完成测量创建、结果轮询和 Vue 作用域销毁时的清理。
目的与范围
本页覆盖两项工具共享的核心机制:目标 IP 分类、国家选择基础数据、Globalping HTTP 请求、轮询终止条件、错误回调及生命周期管理。全球延迟测试用于比较不同来源到目标的延迟;MTR 用于观察探针到目标的数据包路由,这一区别见项目说明。
本页不展开网速测试、DNS 解析或网络审查检测。CensorshipCheck 也是该编排器的使用方,但其专用国家分组与结果处理不属于本页范围。当前提供的目录上下文未包含其他页面的确切路径,因此不虚构兄弟页面链接。
**证据边界:**本次核验涵盖共享编排器、前端探针清单缓存和单元测试片段;尝试定位的界面组件路径不存在。因此,下文不声称已核验 GlobalLatencyTest / MtrTest 的实际组件实现、完整请求体、MTR 跳点结构、延迟统计或地图渲染算法。测量请求体由调用方提供,不能从通用编排器反推所有业务字段。
概述
实现将两类职责分开:
- 业务工具负责目标与结果语义。 调用方传入测量请求体,并通过
onResults(data)判断某次响应是否包含可用结果。 - 共享编排器负责异步生命周期。 它直接访问
https://api.globalping.io/v1/measurements,先 POST 创建,再 GET 查询;状态以 Vueref暴露。 - 国家清单走独立路径。
getProbeInventory()请求本站/api/globalping-probes,把国家代码转换为Set,并在前端模块内缓存 Promise。
这个分工意味着:编排器的 finished 表示“本地轮询流程结束且曾获得调用方认可的结果”,不必然等同于 Globalping 返回了 status: 'finished'。同样,收到 HTTP 成功响应也不等于业务成功。
依据:use-globalping-measurement.js、globalping-probes.js。
架构
Sources: use-globalping-measurement.js、use-globalping-measurement.js、globalping-probes.js
组件复用关系由共享模块注释明确说明。图中的国家清单链路单独呈现:它不参与测量 POST/GET 编排,也不是测量结果缓存。编排器导入的是公共目录中的 fetchWithTimeout;清单模块导入的是前端工具目录中的同名封装,本页没有核验这两个封装的内部实现。
目标与国家选择
目标分类:语法合法不等于公网可达
classifyTarget(input) 先把字符串去除首尾空白,非字符串按空串处理,然后依次调用 isValidIP 与 isUsablePublicIP:
| 返回值 | 条件 | 调用方可表达的含义 |
|---|---|---|
empty | 清理后为空 | 尚未输入目标 |
invalid | 不是合法 IP | 输入格式不符合目标要求 |
unreachable | IP 合法但不属于可用公网地址 | 公网探针不适合测量该地址 |
ok | 两项检查均通过 | 通过本地目标分类 |
ok 不是在线或可达性保证;函数没有发起网络探测。共享模块说明将非公网地址单独分类,是因为外部探针通常无法访问访问者的 LAN 地址。具体保留地址范围由被调用的校验工具决定,本次未核验其实现。
1export function classifyTarget(input) {
2 const ip = typeof input === 'string' ? input.trim() : '';
3 if (ip === '') return 'empty';
4 if (!isValidIP(ip)) return 'invalid';
5 if (!isUsablePublicIP(ip)) return 'unreachable';
6 return 'ok';
7}Source: use-globalping-measurement.js
注意:start() 本身不调用 classifyTarget(),因此目标校验需要由调用方落实,不能把分类工具视为编排器内置的访问控制。
收集到的 IP 与手工输入不是同一道校验
selectableIPs(storeIPs) 返回通过筛选的原始条目,而非仅返回 IP 字符串,因此可以保留条目中的 country 等展示信息。
export function selectableIPs(storeIPs) {
return storeIPs.filter((e) => e && e.ip && !e.ip.includes(' '));
}Source: use-globalping-measurement.js
这只是占位数据过滤:实现检查的是字面量空格 ' ',并非所有空白字符;也没有验证 IP 语法或公网属性。参数应为数组,条目的非空 ip 应支持 .includes()。不要把此函数的输出当作已经完成公网目标校验。
建议国家与实时探针清单
共享常量提供 28 个建议国家,注释说明用于 MTR 和全球延迟工具的建议区与默认选择;GLOBALPING_MAX_COUNTRIES 为 30,约定用户组合的测量每国一个探针。
1export const GLOBALPING_SUGGESTED_COUNTRIES = [
2 'HK', 'TW', 'CN', 'JP', 'KR', 'MY', 'ID', 'SG', 'IN', 'SA',
3 'GB', 'DE', 'PL', 'FI', 'FR', 'TR', 'UA', 'RU',
4 'ZA', 'NG', 'EG',
5 'CA', 'US', 'MX', 'AR', 'BR',
6 'NZ', 'AU',
7];
8// Shared cap for user-composed Globalping runs (one probe per country).
9export const GLOBALPING_MAX_COUNTRIES = 30;Source: use-globalping-measurement.js
这是导出的业务约定,而不是 start() 内部强制执行的限制。国家上限如何在界面落实,以及建议国家如何与当前探针覆盖相交,本次未核验。
getProbeInventory() 的返回值是 Promise,成功解析为:
| 字段 | 运行时形态 | 处理方式 |
|---|---|---|
countries | Set,预期元素为国家代码 | 上游字段为数组才转换,否则使用空数组 |
continents | 数组,注释约定元素为 { key, countries } | 上游字段为数组则原样保留,否则为空数组 |
清单模块不逐项校验代码或大洲对象,也不自行排序、分组。成功后一直复用同一个 Promise;失败时先清空缓存再抛出错误,让下次调用可以重新请求。不存在此模块内部的定时刷新或 TTL。
核心流程:创建、轮询与结果归并
1. 每次启动建立独立的结果记账
start(body, callbacks) 首先调用 cancel() 清理已有定时器和当前控制器,然后把状态设为 running。此次调用创建自己的 tryCount = 0 与 anyResults = false 闭包变量。
随后立即启动异步 POST:请求体是 JSON.stringify(body),请求头仅显式设置 Content-Type: application/json,并传递 10 秒超时与 AbortSignal。编排器没有构造 ping/MTR 专属字段,也没有添加认证头。非成功 HTTP 响应、JSON 解析错误或缺少真值 id 都进入 create 错误分支。
2. 创建成功后延迟发起首次查询
首次 GET 并非立即发出,而是通过 setTimeout 等待 pollInterval。每次 GET 开始时生成新的 AbortController,读取响应 JSON 后,先调用 onResults(data),再决定是否继续。
Source: use-globalping-measurement.js
这是正常 HTTP 成功路径。POST/GET 出错会提前结束,不会继续执行图中的轮询。
3. 可用性判断是累计的,而非只看最终响应
关键终止逻辑如下:
1 if (onResults?.(data)) anyResults = true;
2
3 if (data.status === 'in-progress' && tryCount < maxRetries) {
4 tryCount++;
5 schedule(() => poll(id), pollInterval);
6 } else if (anyResults) {
7 status.value = 'finished';
8 onFinish?.();
9 } else {
10 finishError('empty');
11 }Source: use-globalping-measurement.js
这段实现有四个重要含义:
- 中间结果也交给业务工具。 即使远端仍为
in-progress,调用方也能接收与呈现结果。 - 成功标记不可逆。 某次回调返回真值后,后面的空载荷不会把
anyResults重置为假。 - 次数耗尽仍可能成功。 只要曾获得可用结果,即使最后一次响应仍是
in-progress,本地也进入finished。 - 省略
onResults不会产生成功。 没有回调把anyResults置真时,正常终止会成为empty;仅提供onFinish不足以完成成功流程。
maxRetries 指首次 GET 后最多再查询多少次,不是总 GET 次数。默认值 4 对应最多 5 次 GET;测试用 maxRetries: 2 明确断言总计 3 次。只有成功响应且远端仍为 in-progress 时才会追加查询,网络失败没有重试。
依据:重试计数测试。
4. 状态机与作用域清理
Source: use-globalping-measurement.js
cancel() 不设置新状态;代码也没有 cancelled 状态。因此图中不绘制取消状态转移。作用域销毁时会先设置 disposed = true 再取消,后续异步路径通过检查 disposed 避免更新状态或调用回调。
API 与配置参考
以下签名采用源代码的 JavaScript 写法;代码没有 TypeScript 参数或返回类型声明。
| API | 参数与返回 | 行为及注意事项 |
|---|---|---|
useGlobalpingMeasurement({ pollInterval = 1000, maxRetries = 4 } = {}) | 可选配置对象;返回 { status, start, cancel } | 在 Vue 作用域内使用,注册 onScopeDispose |
start(body, { onResults, onFinish, onError } = {}) | 请求体与可选回调;无显式返回值 | 启动内部异步过程,不返回供等待最终完成的 Promise |
cancel() | 无参数、无显式返回值 | 清除当前 timeout,调用当前控制器的 abort(),不重置状态 |
classifyTarget(input) | 输入值;返回四种分类字符串之一 | 非字符串视为空输入;具体 IP 范围由校验依赖决定 |
selectableIPs(storeIPs) | 条目数组;返回过滤后的数组 | 保留条目对象,不做完整目标校验 |
getProbeInventory() | 无参数;返回清单 Promise | 合并并发请求,失败清空缓存并拒绝 Promise |
回调契约
| 回调 | 触发点 | 返回值契约 |
|---|---|---|
onResults(data) | 每次 HTTP 与 JSON 读取成功的查询后 | 同步返回真值表示存在可用结果 |
onFinish() | 本地转为 finished 后 | 返回值不使用 |
onError(reason) | 本地转为 error 后 | 返回值不使用;reason 为 create、poll 或 empty |
回调不被 await:若把 onResults 写成异步函数,返回的 Promise 本身就是真值,可能错误地标记已有结果。查询路径中回调调用也不在请求读取的 try/catch 内,回调异常不保证被转换为 poll 错误。因此调用方应同步完成可用性判断,并自行处理业务解析异常。
配置值与常量
| 名称 | 类型 | 默认值或固定值 | 含义 |
|---|---|---|---|
pollInterval | number | 1000 毫秒 | 首次 GET 前等待时间及后续 GET 间隔 |
maxRetries | number | 4 | 首次 GET 后的额外查询次数上限 |
REQUEST_TIMEOUT_MS | number | 10000 毫秒 | 每次创建与查询传入请求封装的超时值 |
API_BASE | string | https://api.globalping.io/v1/measurements | 固定测量地址 |
GLOBALPING_MAX_COUNTRIES | number | 30 | 导出的国家数约定;start() 不强制检查 |
GLOBALPING_SUGGESTED_COUNTRIES | string[] | 28 个国家代码 | 建议国家与默认选择基础 |
inventoryPromise | Promise 或 null | null | 模块内缓存状态,不是用户可配置选项 |
这些值中只有 pollInterval、maxRetries 是工厂函数配置参数;本次所读代码未提供环境变量覆盖、指数退避、总任务超时或持久化配置。
依据:默认值与常量。
使用示例与测试证据
在作用域中启动 ping 测量
下面片段直接取自成功路径测试。withScope 是同一测试文件中基于 Vue effectScope 的辅助函数;resultPayloads 与计数器由测试预先定义,网络请求由 stubFetch 模拟。这是已存在测试中的调用片段,不是完整独立脚本,也不是 MTR 请求体示例。
1 const { scope, value } = withScope(() =>
2 useGlobalpingMeasurement({ pollInterval: 5, maxRetries: 3 })
3 );
4 try {
5 value.start({ target: '1.2.3.4', type: 'ping' }, {
6 onResults: (d) => { resultPayloads.push(d); return d.results.length > 0; },
7 onFinish: () => { finishCalls++; },
8 onError: () => { errorCalls++; },
9 });
10
11 await waitFor(() => value.status.value !== 'running', { label: "status to leave 'running'" });
12
13 assert.equal(value.status.value, 'finished');
14 assert.equal(finishCalls, 1);
15 assert.equal(errorCalls, 0);
16 assert.deepEqual(resultPayloads, [payload]);
17 } finally {
18 scope.stop();
19 }此示例展示真正的完成等待方式:观察 status.value,而不是 await start()。onResults 同时收集响应和返回可用性判断,scope.stop() 则保证测试结束时触发清理。
生命周期清理的生产实现
1 onScopeDispose(() => {
2 disposed = true;
3 cancel();
4 });
5
6 return { status, start, cancel };Source: use-globalping-measurement.js
这使测量编排器跟随 Vue 作用域结束,而不是让抽屉或路由卸载后仍有轮询写入旧组件状态。实例销毁后应重新创建,不应继续调用旧实例的 start();该函数没有把 disposed 重置为假。
已核验测试覆盖
测试通过替换 globalThis.fetch 模拟创建和查询,并在各用例前后恢复。waitFor 反复检查状态,避免固定 sleep 在慢机器上不稳定;它的默认 2000 毫秒是测试挂起上限,不是生产任务超时。
| 场景 | 断言或验证行为 |
|---|---|
| POST 成功,首次 GET 完成 | 状态 finished,onFinish 一次,无错误回调,载荷透传 |
连续 in-progress 后完成 | 持续查询,示例总计三次 GET |
| 耗尽次数且始终无结果 | error、原因 empty,总 GET 数等于首次加重试次数 |
| POST 抛出错误 | create,不进入查询 |
POST 返回无 id 的 JSON | create |
| GET 抛出错误 | poll |
依据:测试辅助函数、已核验用例。本次未执行这些测试;后续未读测试内容不作为覆盖结论,界面集成与 MTR 数据展示测试也未核验。
失败模式、边界条件与并发
请求失败不会自动恢复
| 条件 | 本地结果 | 排查重点 |
|---|---|---|
| POST 请求失败、HTTP 非成功或 JSON 读取失败 | error / create | 浏览器到 Globalping 的连通性及响应内容 |
创建响应没有真值 id | error / create | 上游响应形状 |
| GET 请求失败、HTTP 非成功或 JSON 读取失败 | error / poll | 测量 ID 对应请求与网络错误 |
| 正常终止但没有任何回调返回真值 | error / empty | 实际结果为空,或调用方未正确实现 onResults |
| 国家清单请求失败 | Promise 拒绝,缓存置空 | 下次调用可重试;界面降级行为未核验 |
HTTP 错误在内部构造 Error,测量创建/查询失败会写入 console.error;调用方只收到分类原因,拿不到结构化 HTTP 状态对象。请求超时的底层异常类型与 AbortSignal 合并方式未核验,不能承诺具体异常名称。
取消与重启并不是严格的“最新请求获胜”
cancel() 只清理当前定时器和控制器,不更新 disposed,也不分配请求版本号。区别如下:
- **作用域销毁:**先置
disposed = true,旧任务恢复执行后会尽早返回,不再更新状态。 - **手工取消:**不会改变
status;若仅清除等待中的 timer,状态可能仍为running。若中断活动请求导致请求拒绝,且作用域仍有效,现有 catch 分支仍可能发出create或poll错误。 - **快速重新启动:**新
start()先取消旧请求,但旧请求的异步失败分支没有 run ID 隔离,可能随后覆盖新任务状态或触发旧回调。
因此,“会取消旧请求”不应被文档夸大为“已经完全消除重启竞争”。需要严格隔离时,可在扩展中加入每次运行的标识,并在每次异步返回后检查;这属于改进建议,不是当前已实现行为。
清单缓存的并发与数据质量
inventoryPromise 在模块范围共享,同一模块实例的并发调用复用同一个请求。成功缓存没有主动失效机制,所以运行期间新增或下线的探针不一定立即反映到选择数据。
对形状异常的成功响应,代码把非数组字段变为空数组,但不会因此清空成功缓存。这与网络失败不同:网络失败允许下一次重试,而合法 JSON 中错误类型的字段可能留下持续复用的空清单。后端的具体数据清洗与界面兜底不在本次已核验实现内。
性能、运维与扩展
请求量与等待时间
默认单次启动最多产生 1 次 POST 和 5 次 GET;查询链使用 setTimeout,下一次是在当前查询完成并处理结果后才安排,因此同一正常轮询链不会按固定节拍重叠 GET。真实间隔包括请求耗时、结果处理耗时和 pollInterval,并非严格每秒一次。
每个请求传入 10 秒超时,但不存在统一任务截止时间。不能把 maxRetries × pollInterval 当作端到端耗时上限,也不能把查询次数上限理解为远端任务取消:已核验代码没有发送远端取消请求。
存储与可观测性边界
本模块只保存内存中的状态、定时器、控制器及结果可用性标记;清单模块保存一个内存 Promise。已读实现未向数据库、localStorage 或历史记录写入测量结果,也未实现后台任务或遥测指标。具体结果列表如何在组件中保存尚未核验。
排障时应区分两条 HTTP 链路:国家清单使用本站接口,实际测量直接访问 Globalping。国家列表正常并不能证明浏览器能够成功创建测量。
安全扩展方向
- **增加测量展示:**沿用
start(body, callbacks),在调用方解析响应,不把 MTR 或 ping 专属表格逻辑塞入编排器。 - **改变等待策略:**通过工厂参数调整查询间隔和次数;当前代码不校验参数范围,调用方应传入合理的非负数值。
- **增加异步后处理:**在回调内部明确处理异常,不把异步 Promise 当作
onResults的可用性布尔结果。 - **改善清单新鲜度:**可增加显式刷新或过期策略,但要同时保留并发请求合并和失败后可重试的行为。
- **增强取消语义:**为运行增加版本隔离,并明确取消是否改变状态、是否通知调用方。