跳转到内容

地球仪与位置解析

地球仪功能包含两项相互独立的职责:先把每台服务器解析为坐标,再以可交互的方式绘制这些坐标。分离两者既能明确隐私边界,也便于独立测试几何计算。

面向用户的操作说明请参阅地球仪视图,数据处理政策请参阅地球仪与位置数据。

┌─────────────────────────────────────────────┐
│ ServerGlobe │
│ 解析服务器、构建卡片、处理 UI 操作 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ GlobeView │
│ 旋转、缩放、布局与命中测试 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ IpGeo │
│ 选择坐标及其来源 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ GeoData / GeoBundle │
│ 安装并读取本机数据集 │
└─────────────────────────────────────────────┘

只有 ServerGlobe 了解服务器记录和连接状态。它传给 GlobeView 的内容只有 id、坐标、标记颜色和卡片 widget,因此投影与卡片布局不依赖服务器模型、Riverpod 或位置解析逻辑。

位置解析只在地球仪显示期间运行。没有把它放进 provider,是为了避免其他 widget 仅仅读取共享状态时就触发所有服务器的位置解析。

设置项与解析器共同执行以下流程:

  1. globeEnabled 关闭后,服务器页会移除地球仪视图,不再启动自动位置解析。IpGeo 也会在执行 DNS 或读取数据集前检查该设置。
  2. IpGeo.locate 首先返回手动配置的坐标。该值直接来自本机服务器记录,不属于查询,因此方法会在内部检查功能开关之前处理它。
  3. IpGeo.geoHostOf 根据首选连接方式选择 host;如果该连接方式没有可用 host,则回退到另一种已配置的连接方式。
  4. locateHost 会立即拒绝明确的非公网 host。对于其他 host,它会按需解析域名、拒绝解析得到的非公网地址,然后查询城市级 bundle。
  5. 如果连接 host 是内网地址,locate 还可以使用该服务器此前上报的公网地址,并在城市级 bundle 中查询该地址。

最终的位置来源优先级如下:

GeoSource 含义
manual 保存在服务器记录中的坐标;不会被覆盖
selfReported 服务器上报的公网网卡地址,再通过城市级数据集解析
city 通过城市级数据集解析连接所用的 host 或地址

枚举顺序与解析器的处理顺序一致。优先级由控制流保证:IpGeo.locate 在第一个返回坐标的来源处停止,因此没有需要比较或替换的已保存结果。

GeoMiss.private 表示局域网、loopback、链路本地、文档保留或其他非公网地址。

GeoMiss.noData 表示域名无法解析、数据集中没有对应公网地址、城市级数据尚未安装,或无法从配置中选出 host。UI 区分这两类结果,因为它们需要不同的处理方式。

SelfAddr 用于处理“连接路径是内网,但机器自身具有公网网卡地址”的服务器。

  • 共享状态 manifest 会在扩展轮询周期通过 ip 字段上报网卡地址。
  • SSH 与 Monitor 两种连接方式都会填充同一个状态字段。仅配置 Monitor 的服务器不需要授予 full_access,也不需要通过 /exec 发起请求。
  • SelfAddr.publicIn 会解析地址、去重并过滤非公网地址。IPv4 和 IPv6 同时存在时,SelfAddr.pick 优先选择 IPv4,作为 dual-stack 服务器的稳定决胜规则。
  • 无论找到公网地址,还是确认没有公网地址,结果都会连同时间戳按服务器 id 保存;七天后可重新采集。

保存的是地址,而不是由它得到的坐标。坐标属于公网地址,但 selfReported 描述的是某一台服务器如何提供了该地址——若按公网 host 保存,该标签会被套用到同一地址后的其他所有服务器。保存地址则只留下那台机器独有的事实,查找仍由当前安装的数据集重新完成。

如果 NAT 后方的机器只有内网网卡地址,这条路径仍无法定位它,只能使用手动坐标。

App 不会内置或自动获取城市级数据集。用户在地球仪或设置中点击 下载 后,GeoDataInstall 会执行共用流程。它在最终确认对话框之前调用 GeoData.fetchManifest 获取 manifest.json,让对话框显示当前数据,而不是编译时写入的估算值。设置页会在数据行内显示下载进度;其他入口则使用模态进度对话框。

Manifest 包含:

  • 格式版本和构建月份;
  • 许可署名;
  • asset 名称与地址族;
  • 压缩前后的大小;
  • 每个压缩 asset 的 SHA-256。

解析过程会拒绝不支持的版本、格式错误的月份、缺失或重复的地址族、不安全的名称、无效或超限的长度,以及格式错误的 digest。Asset 名称必须符合字符和长度限制,且不得包含 ..,因为它同时用于 URL 和本地路径。

确认对话框会分别显示下载总大小和安装后总大小,并在允许确认前显示主 endpoint 与许可署名。

GeoData.install 会先移除现有数据集,再写入替换版本。对于每个 asset,它会:

  1. 在下载过程中限制接收大小;
  2. 校验压缩数据长度和 SHA-256;
  3. 使用 gzip 解压;
  4. 校验解压后长度;
  5. 写入 bundle,并在所有 asset 均成功后写入 installed manifest。

任何失败都会移除不完整的安装,避免地球仪在只安装了一个地址族时仍表现得像完整数据集。但这也意味着更新失败后不会继续保留旧版本。

主 endpoint 为 ipgeo.lollipopkit.com。主 endpoint 没有返回可用数据时,会回退到 GitHub Releases URL。Digest 用于发现传输损坏或截断,不是数字签名,因为 manifest 和 asset 来自同一个 endpoint。

每个地址族使用一个大端序 SBGX bundle:

magic "SBGX" 4 B
format 1 B == 1
family 1 B == 4 或 6
year u16, month u8 3 B
count u32 4 B
reserved 3 B 置零,将 header 补齐到 16 B
bucket table (2^bucketBits + 1) × u32
records count × (offset + lat i16 + lon i16)

IPv4 使用 32 位 key,并按前 8 位分桶;记录中的 offset 为 3 字节,每条记录共 7 字节。IPv6 保存前 48 位,并按前 16 位分桶;offset 为 4 字节,每条记录共 8 字节。因此 /48 是该格式能够表示的最细 IPv6 粒度。

每个 bucket 内的记录按起始 offset 排序。查询时通过二分查找找到起始 offset 不大于目标地址的最后一条记录。坐标量化为有符号 16 位整数;纬度值 -32768 保留用于表示该地址范围没有位置数据。

GeoBundle.open 会在接受 bundle 前检查 magic、格式、构建月份、地址族、单调的表边界和精确文件长度。地址族和构建月份都从 header 读取,再与已安装的 manifest 比较,而不是根据文件名推断。

常驻内存的只有 header 和 bucket table——IPv4 约 1 KB,IPv6 约 256 KB。记录通过已打开的文件执行小块同步读取,让操作系统 page cache 提供数据,而不必把约 52 MB 的数据集保留在 Dart heap 中。同步访问也避免了 Flutter widget test 的 fake-async zone 无法完成真实 I/O future 的问题。

Store Key 内容
SelfAddrStore 服务器 id 上报的公网地址或明确的无结果状态,以及采集时间

只有这一项。它属于可重新生成的本机派生状态,不会更新 App 的用户数据修改时间,也不用于同步或备份。

坐标不保存。 曾经有一个按连接 host 建索引的 GeoStore 缓存坐标,它随着按次查找的网络请求一起被移除。完整数据集下载到本地后,一次查找只需对已打开的文件执行约十余次很小的同步读取。在此之前再读取一行数据库记录并解码 JSON,只会增加开销并保留过期结果:更新已安装的数据集时,缓存的 host 不会移动;主机名改为解析到其他地址时,旧记录也无法察觉。m019_drop_geo_cache 会在旧版本升级时删除这些已废弃的记录。

SelfAddrStore 保留,因为它不是对已计算坐标的缓存。只有服务器自身知道哪个公网地址分配给了它的网卡。该 Store 保存地址而非坐标,因此坐标始终跟随当前安装的数据集。超过 staleAfter(7 天)后,该记录会进入可刷新状态,由后续的状态轮询重新采集。

移除缓存的代价只有一项:设备离线时,以名称配置的服务器不再被定位——resolver 无法作答,也没有预留的结果可用。这些服务器在该状态下本就无法连接。

  • projection.dart 根据地球仪相机投影坐标,并排除背面的点。
  • layout.dart 分离重叠的固定尺寸卡片,并生成连接标记的引导线。
  • painter.dart 绘制球体、陆地、标记和引导线,并让标记与线条在接近地平线时淡出。
  • 已定位条目超过 labelLimit(默认 14)后,只为当前选中的条目显示卡片。第一次点击标记会选中它,第二次点击才打开服务器。
  • 只要至少有一台已定位服务器位于背面,地球仪就会沿经度自动旋转。用户首次操作后,本次 GlobeView 实例不再自动旋转。
  • 拖动惯性按实际经过时间计算衰减,因此不同刷新率下的行为一致。缩放使用乘法比例;鼠标滚轮和触控板的 pointer signal 与触摸手势分开处理。

初始相机会朝向当前列表中第一台已定位的服务器。用户已经移动视角后,后续返回的位置解析结果不会再次自动居中。

地球仪埋点使用经过处理的结构化 breadcrumb,只记录粗粒度操作和计数,例如打开或关闭视图、数据集处理结果、各来源的定位数量,以及通过卡片还是标记打开服务器。事件不包含坐标、地址、服务器名称或国家。

在完整信息诊断级别下,这些 breadcrumb 还可以作为功能使用事件发送。在基本信息级别下,相关 breadcrumb 可能随错误报告一同发送,但不会作为 analytics event 持续上报。完整规则请参阅隐私政策。