Repository Wiki
jason5ng32/MyIP

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 查询;状态以 Vue ref 暴露。
  • 国家清单走独立路径。 getProbeInventory() 请求本站 /api/globalping-probes,把国家代码转换为 Set,并在前端模块内缓存 Promise。

这个分工意味着:编排器的 finished 表示“本地轮询流程结束且曾获得调用方认可的结果”,不必然等同于 Globalping 返回了 status: 'finished'。同样,收到 HTTP 成功响应也不等于业务成功。

依据:use-globalping-measurement.js、globalping-probes.js。

架构

Loading diagram...

Sources: use-globalping-measurement.js、use-globalping-measurement.js、globalping-probes.js

组件复用关系由共享模块注释明确说明。图中的国家清单链路单独呈现:它不参与测量 POST/GET 编排,也不是测量结果缓存。编排器导入的是公共目录中的 fetchWithTimeout;清单模块导入的是前端工具目录中的同名封装,本页没有核验这两个封装的内部实现。

目标与国家选择

目标分类:语法合法不等于公网可达

classifyTarget(input) 先把字符串去除首尾空白,非字符串按空串处理,然后依次调用 isValidIP 与 isUsablePublicIP:

返回值条件调用方可表达的含义
empty清理后为空尚未输入目标
invalid不是合法 IP输入格式不符合目标要求
unreachableIP 合法但不属于可用公网地址公网探针不适合测量该地址
ok两项检查均通过通过本地目标分类

ok 不是在线或可达性保证;函数没有发起网络探测。共享模块说明将非公网地址单独分类,是因为外部探针通常无法访问访问者的 LAN 地址。具体保留地址范围由被调用的校验工具决定,本次未核验其实现。

javascript
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 等展示信息。

javascript
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,约定用户组合的测量每国一个探针。

javascript
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,成功解析为:

字段运行时形态处理方式
countriesSet,预期元素为国家代码上游字段为数组才转换,否则使用空数组
continents数组,注释约定元素为 { key, countries }上游字段为数组则原样保留,否则为空数组

清单模块不逐项校验代码或大洲对象,也不自行排序、分组。成功后一直复用同一个 Promise;失败时先清空缓存再抛出错误,让下次调用可以重新请求。不存在此模块内部的定时刷新或 TTL。

依据:globalping-probes.js。

核心流程:创建、轮询与结果归并

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),再决定是否继续。

Loading diagram...

Source: use-globalping-measurement.js

这是正常 HTTP 成功路径。POST/GET 出错会提前结束,不会继续执行图中的轮询。

3. 可用性判断是累计的,而非只看最终响应

关键终止逻辑如下:

javascript
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

这段实现有四个重要含义:

  1. 中间结果也交给业务工具。 即使远端仍为 in-progress,调用方也能接收与呈现结果。
  2. 成功标记不可逆。 某次回调返回真值后,后面的空载荷不会把 anyResults 重置为假。
  3. 次数耗尽仍可能成功。 只要曾获得可用结果,即使最后一次响应仍是 in-progress,本地也进入 finished。
  4. 省略 onResults 不会产生成功。 没有回调把 anyResults 置真时,正常终止会成为 empty;仅提供 onFinish 不足以完成成功流程。

maxRetries 指首次 GET 后最多再查询多少次,不是总 GET 次数。默认值 4 对应最多 5 次 GET;测试用 maxRetries: 2 明确断言总计 3 次。只有成功响应且远端仍为 in-progress 时才会追加查询,网络失败没有重试。

依据:重试计数测试。

4. 状态机与作用域清理

Loading diagram...

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

依据:测量与目标 API、清单 API。

回调契约

回调触发点返回值契约
onResults(data)每次 HTTP 与 JSON 读取成功的查询后同步返回真值表示存在可用结果
onFinish()本地转为 finished 后返回值不使用
onError(reason)本地转为 error 后返回值不使用;reason 为 create、poll 或 empty

回调不被 await:若把 onResults 写成异步函数,返回的 Promise 本身就是真值,可能错误地标记已有结果。查询路径中回调调用也不在请求读取的 try/catch 内,回调异常不保证被转换为 poll 错误。因此调用方应同步完成可用性判断,并自行处理业务解析异常。

配置值与常量

名称类型默认值或固定值含义
pollIntervalnumber1000 毫秒首次 GET 前等待时间及后续 GET 间隔
maxRetriesnumber4首次 GET 后的额外查询次数上限
REQUEST_TIMEOUT_MSnumber10000 毫秒每次创建与查询传入请求封装的超时值
API_BASEstringhttps://api.globalping.io/v1/measurements固定测量地址
GLOBALPING_MAX_COUNTRIESnumber30导出的国家数约定;start() 不强制检查
GLOBALPING_SUGGESTED_COUNTRIESstring[]28 个国家代码建议国家与默认选择基础
inventoryPromisePromise 或 nullnull模块内缓存状态,不是用户可配置选项

这些值中只有 pollInterval、maxRetries 是工厂函数配置参数;本次所读代码未提供环境变量覆盖、指数退避、总任务超时或持久化配置。

依据:默认值与常量。

使用示例与测试证据

在作用域中启动 ping 测量

下面片段直接取自成功路径测试。withScope 是同一测试文件中基于 Vue effectScope 的辅助函数;resultPayloads 与计数器由测试预先定义,网络请求由 stubFetch 模拟。这是已存在测试中的调用片段,不是完整独立脚本,也不是 MTR 请求体示例。

javascript
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 }

Source: composable-globalping-measurement.test.js

此示例展示真正的完成等待方式:观察 status.value,而不是 await start()。onResults 同时收集响应和返回可用性判断,scope.stop() 则保证测试结束时触发清理。

生命周期清理的生产实现

javascript
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 的 JSONcreate
GET 抛出错误poll

依据:测试辅助函数、已核验用例。本次未执行这些测试;后续未读测试内容不作为覆盖结论,界面集成与 MTR 数据展示测试也未核验。

失败模式、边界条件与并发

请求失败不会自动恢复

条件本地结果排查重点
POST 请求失败、HTTP 非成功或 JSON 读取失败error / create浏览器到 Globalping 的连通性及响应内容
创建响应没有真值 iderror / 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 的可用性布尔结果。
  • **改善清单新鲜度:**可增加显式刷新或过期策略,但要同时保留并发请求合并和失败后可重试的行为。
  • **增强取消语义:**为运行增加版本隔离,并明确取消是否改变状态、是否通知调用方。

相关链接