地球仪与位置解析
地球仪功能包含两项相互独立的职责:先把每台服务器解析为坐标,再以可交互的方式绘制这些坐标。分离两者既能明确隐私边界,也便于独立测试几何计算。
面向用户的操作说明请参阅地球仪视图,数据处理政策请参阅地球仪与位置数据。
┌─────────────────────────────────────────────┐│ ServerGlobe ││ 解析服务器、构建卡片、处理 UI 操作 │└─────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────┐│ GlobeView ││ 旋转、缩放、布局与命中测试 │└─────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────┐│ IpGeo ││ 选择坐标及其来源 │└─────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────┐│ GeoData / GeoBundle ││ 安装并读取本机数据集 │└─────────────────────────────────────────────┘只有 ServerGlobe 了解服务器记录和连接状态。它传给 GlobeView 的内容只有 id、坐标、标记颜色和卡片 widget,因此投影与卡片布局不依赖服务器模型、Riverpod 或位置解析逻辑。
位置解析只在地球仪显示期间运行。没有把它放进 provider,是为了避免其他 widget 仅仅读取共享状态时就触发所有服务器的位置解析。
设置项与解析器共同执行以下流程:
globeEnabled关闭后,服务器页会移除地球仪视图,不再启动自动位置解析。IpGeo也会在执行 DNS 或读取数据集前检查该设置。IpGeo.locate首先返回手动配置的坐标。该值直接来自本机服务器记录,不属于查询,因此方法会在内部检查功能开关之前处理它。IpGeo.geoHostOf根据首选连接方式选择 host;如果该连接方式没有可用 host,则回退到另一种已配置的连接方式。locateHost会立即拒绝明确的非公网 host。对于其他 host,它会按需解析域名、拒绝解析得到的非公网地址,然后查询城市级 bundle。- 如果连接 host 是内网地址,
locate还可以使用该服务器此前上报的公网地址,并在城市级 bundle 中查询该地址。
最终的位置来源优先级如下:
GeoSource |
含义 |
|---|---|
manual |
保存在服务器记录中的坐标;不会被覆盖 |
selfReported |
服务器上报的公网网卡地址,再通过城市级数据集解析 |
city |
通过城市级数据集解析连接所用的 host 或地址 |
枚举顺序与解析器的处理顺序一致。优先级由控制流保证:IpGeo.locate 在第一个返回坐标的来源处停止,因此没有需要比较或替换的已保存结果。
GeoMiss.private 表示局域网、loopback、链路本地、文档保留或其他非公网地址。
GeoMiss.noData 表示域名无法解析、数据集中没有对应公网地址、城市级数据尚未安装,或无法从配置中选出 host。UI 区分这两类结果,因为它们需要不同的处理方式。
服务器上报的公网地址
Section titled “服务器上报的公网地址”SelfAddr 用于处理“连接路径是内网,但机器自身具有公网网卡地址”的服务器。
- 共享状态 manifest 会在扩展轮询周期通过
ip字段上报网卡地址。 - SSH 与 Monitor 两种连接方式都会填充同一个状态字段。仅配置 Monitor 的服务器不需要授予
full_access,也不需要通过/exec发起请求。 SelfAddr.publicIn会解析地址、去重并过滤非公网地址。IPv4 和 IPv6 同时存在时,SelfAddr.pick优先选择 IPv4,作为 dual-stack 服务器的稳定决胜规则。- 无论找到公网地址,还是确认没有公网地址,结果都会连同时间戳按服务器 id 保存;七天后可重新采集。
保存的是地址,而不是由它得到的坐标。坐标属于公网地址,但 selfReported 描述的是某一台服务器如何提供了该地址——若按公网 host 保存,该标签会被套用到同一地址后的其他所有服务器。保存地址则只留下那台机器独有的事实,查找仍由当前安装的数据集重新完成。
如果 NAT 后方的机器只有内网网卡地址,这条路径仍无法定位它,只能使用手动坐标。
城市级数据集
Section titled “城市级数据集”Manifest 与确认流程
Section titled “Manifest 与确认流程”App 不会内置或自动获取城市级数据集。用户在地球仪或设置中点击 下载 后,GeoDataInstall 会执行共用流程。它在最终确认对话框之前调用 GeoData.fetchManifest 获取 manifest.json,让对话框显示当前数据,而不是编译时写入的估算值。设置页会在数据行内显示下载进度;其他入口则使用模态进度对话框。
Manifest 包含:
- 格式版本和构建月份;
- 许可署名;
- asset 名称与地址族;
- 压缩前后的大小;
- 每个压缩 asset 的 SHA-256。
解析过程会拒绝不支持的版本、格式错误的月份、缺失或重复的地址族、不安全的名称、无效或超限的长度,以及格式错误的 digest。Asset 名称必须符合字符和长度限制,且不得包含 ..,因为它同时用于 URL 和本地路径。
确认对话框会分别显示下载总大小和安装后总大小,并在允许确认前显示主 endpoint 与许可署名。
安装与失败处理
Section titled “安装与失败处理”GeoData.install 会先移除现有数据集,再写入替换版本。对于每个 asset,它会:
- 在下载过程中限制接收大小;
- 校验压缩数据长度和 SHA-256;
- 使用 gzip 解压;
- 校验解压后长度;
- 写入 bundle,并在所有 asset 均成功后写入 installed manifest。
任何失败都会移除不完整的安装,避免地球仪在只安装了一个地址族时仍表现得像完整数据集。但这也意味着更新失败后不会继续保留旧版本。
主 endpoint 为 ipgeo.lollipopkit.com。主 endpoint 没有返回可用数据时,会回退到 GitHub Releases URL。Digest 用于发现传输损坏或截断,不是数字签名,因为 manifest 和 asset 来自同一个 endpoint。
Bundle 格式
Section titled “Bundle 格式”每个地址族使用一个大端序 SBGX bundle:
magic "SBGX" 4 Bformat 1 B == 1family 1 B == 4 或 6year u16, month u8 3 Bcount u32 4 Breserved 3 B 置零,将 header 补齐到 16 Bbucket table (2^bucketBits + 1) × u32records 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 持续上报。完整规则请参阅隐私政策。