Files
pangolin/client/lib/bridge/vpn_bridge.dart
T
wangjia e00163e2d9 fix(client): statsStream/statusStream 缓存为共享广播流,修延迟不回写
根因:VpnNativeBridge.statsStream getter 每次访问都 receiveBroadcastStream(),多订阅者
(连接页速度 + ConnectionController._onStats 回写 urltest 延迟)各起一条原生订阅;但
EventChannel 原生侧只留一个 sink(StatsStreamHandler.onListen 覆盖),后订阅者赢、先订阅者
变哑。速度订阅赢了 sink → _onStats 收不到 → urltest 实测(已确认到达 native:reality-out=595)
永不回写 node.ping → 连接页延迟一直 —。

修复:receiveBroadcastStream() 只调一次、缓存成 asBroadcastStream() 共享流,所有 Dart
订阅者复用同一条原生订阅。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:57:37 +08:00

254 lines
9.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// vpn_bridge.dart — Dart↔原生桥接 API 契约(冻结字段)
//
// ═══════════════════════════════════════════════════════════════
// 通道名称(Channel names
// ═══════════════════════════════════════════════════════════════
// MethodChannel : pangolin/vpn
// EventChannel : pangolin/vpn/status
// EventChannel : pangolin/vpn/stats
//
// ═══════════════════════════════════════════════════════════════
// MethodChannel 方法签名(Method signatures)—— 不得私改
// ═══════════════════════════════════════════════════════════════
// start(String configJson) → void
// stop() → void
// getStatus() → String ("off"|"connecting"|"on"|"error")
// selectOutbound(String tag) → void
// getActiveOutbound() → String tag
// setKillSwitch(bool on) → void
//
// ═══════════════════════════════════════════════════════════════
// EventChannel 事件 schema —— 不得私改
// ═══════════════════════════════════════════════════════════════
// pangolin/vpn/status → String
// 值: "off" | "connecting" | "on" | "error"
// 来源:内核回调(严禁 UI 乐观显示;start 返回 ≠ on)
//
// pangolin/vpn/stats → Map<String, dynamic>
// {
// "uploadBytes" : int, // 本次隧道累计上传字节
// "downloadBytes" : int, // 本次隧道累计下载字节
// "uploadSpeed" : double, // bytes/s 瞬时上行速率
// "downloadSpeed" : double, // bytes/s 瞬时下行速率
// "urltestResults" : [ // urltest 延迟结果列表
// { "tag": String, "delayMs": int },
// ...
// ]
// }
// 周期:约 1 秒一帧(内核驱动,非固定间隔)
//
// 最后修订:任务 tsk_iP5-_Ztq5THx(桥接 API 面定稿)
// ═══════════════════════════════════════════════════════════════
import 'dart:async';
import 'package:flutter/services.dart';
// ── 状态枚举 ────────────────────────────────────────────────────
// 与 design/flutter/widgets/connect_button.dart VpnStatus 对齐。
// error 为桥接层扩展态,仅由内核回调产生,UI 层按需处理。
enum VpnStatus {
off,
connecting,
on,
error;
/// 从原生侧字符串解析
static VpnStatus fromString(String s) => switch (s) {
'off' => VpnStatus.off,
'connecting' => VpnStatus.connecting,
'on' => VpnStatus.on,
'error' => VpnStatus.error,
_ => VpnStatus.error,
};
/// 序列化为原生侧字符串
String toNativeString() => switch (this) {
VpnStatus.off => 'off',
VpnStatus.connecting => 'connecting',
VpnStatus.on => 'on',
VpnStatus.error => 'error',
};
}
// ── stats 数据模型 ───────────────────────────────────────────────
class UrltestResult {
const UrltestResult({required this.tag, required this.delayMs});
final String tag;
final int delayMs;
factory UrltestResult.fromMap(Map<String, dynamic> m) => UrltestResult(
tag: m['tag'] as String? ?? '',
delayMs: (m['delayMs'] as num?)?.toInt() ?? -1,
);
Map<String, dynamic> toMap() => {'tag': tag, 'delayMs': delayMs};
@override
String toString() => 'UrltestResult(tag=$tag, delayMs=$delayMs)';
}
class VpnStatsEvent {
const VpnStatsEvent({
required this.uploadBytes,
required this.downloadBytes,
required this.uploadSpeed,
required this.downloadSpeed,
required this.urltestResults,
});
final int uploadBytes;
final int downloadBytes;
final double uploadSpeed;
final double downloadSpeed;
final List<UrltestResult> urltestResults;
factory VpnStatsEvent.fromMap(Map<String, dynamic> m) => VpnStatsEvent(
uploadBytes: (m['uploadBytes'] as num?)?.toInt() ?? 0,
downloadBytes: (m['downloadBytes'] as num?)?.toInt() ?? 0,
uploadSpeed: (m['uploadSpeed'] as num?)?.toDouble() ?? 0.0,
downloadSpeed: (m['downloadSpeed'] as num?)?.toDouble() ?? 0.0,
urltestResults: ((m['urltestResults'] as List?)
?.whereType<Map>()
.map((e) => UrltestResult.fromMap(
Map<String, dynamic>.from(e),
))
.toList()) ??
const [],
);
/// 从平台通道原始 MapMap<Object?, Object?>)解析,自动转换键类型。
static VpnStatsEvent fromNativeMap(Map<Object?, Object?> raw) =>
VpnStatsEvent.fromMap(raw.cast<String, dynamic>());
Map<String, dynamic> toMap() => {
'uploadBytes': uploadBytes,
'downloadBytes': downloadBytes,
'uploadSpeed': uploadSpeed,
'downloadSpeed': downloadSpeed,
'urltestResults': urltestResults.map((r) => r.toMap()).toList(),
};
@override
String toString() =>
'VpnStatsEvent(up=${uploadBytes}B, down=${downloadBytes}B, '
'upSpeed=${uploadSpeed.toStringAsFixed(0)}B/s, '
'downSpeed=${downloadSpeed.toStringAsFixed(0)}B/s, '
'urltest=${urltestResults.length}nodes)';
}
// ── 抽象接口 ─────────────────────────────────────────────────────
/// Dart 侧 VPN 桥接接口。
/// 实现方:[VpnNativeBridge](真实原生通道)与 [VpnBridgeMock]UI 联调)。
abstract class VpnBridge {
// ── MethodChannel 方法 ──
/// 启动 VPN。[configJson] 为 sing-box 兼容 JSON 配置字符串。
/// 返回不代表连接已建立,实际状态由 [statusStream] 驱动。
Future<void> start(String configJson);
/// 停止 VPN。
Future<void> stop();
/// 查询当前状态(一次性拉取,实时监听用 [statusStream])。
Future<VpnStatus> getStatus();
/// 切换出口节点。[tag] 为 sing-box outbound tag。
Future<void> selectOutbound(String tag);
/// 获取当前激活出口节点 tag。
Future<String> getActiveOutbound();
/// 设置 Kill Switch 开关。
Future<void> setKillSwitch({required bool on});
// ── EventChannel 流 ──
/// 实时 VPN 状态流(来自内核回调)。
Stream<VpnStatus> get statusStream;
/// 约每秒推一帧统计数据流(来自内核计时器)。
Stream<VpnStatsEvent> get statsStream;
/// 释放资源。
void dispose();
}
// ── 原生实现 ─────────────────────────────────────────────────────
/// 生产实现:使用 Flutter MethodChannel / EventChannel 与原生侧通信。
class VpnNativeBridge implements VpnBridge {
VpnNativeBridge()
: _method = const MethodChannel('pangolin/vpn'),
_statusRaw = const EventChannel('pangolin/vpn/status'),
_statsRaw = const EventChannel('pangolin/vpn/stats');
// 允许测试注入
VpnNativeBridge.withChannels({
required MethodChannel method,
required EventChannel status,
required EventChannel stats,
}) : _method = method,
_statusRaw = status,
_statsRaw = stats;
final MethodChannel _method;
final EventChannel _statusRaw;
final EventChannel _statsRaw;
@override
Future<void> start(String configJson) =>
_method.invokeMethod<void>('start', configJson);
@override
Future<void> stop() => _method.invokeMethod<void>('stop');
@override
Future<VpnStatus> getStatus() async {
final s = await _method.invokeMethod<String>('getStatus') ?? 'off';
return VpnStatus.fromString(s);
}
@override
Future<void> selectOutbound(String tag) =>
_method.invokeMethod<void>('selectOutbound', tag);
@override
Future<String> getActiveOutbound() async =>
await _method.invokeMethod<String>('getActiveOutbound') ?? 'auto';
@override
Future<void> setKillSwitch({required bool on}) =>
_method.invokeMethod<void>('setKillSwitch', on);
// ⚠️ EventChannel 原生侧只保留「一个」sink:每次 receiveBroadcastStream() + listen 都会
// 触发 onListen 覆盖原生 sink → 多个订阅者(连接页速度 + ConnectionController 回写延迟)
// 互相抢,后订阅的赢、先订阅的流变哑。必须只调一次 receiveBroadcastStream()、缓存成共享
// 广播流,所有 Dart 订阅者复用同一条原生订阅。
late final Stream<VpnStatus> _statusStream = _statusRaw
.receiveBroadcastStream()
.map((e) => VpnStatus.fromString(e?.toString() ?? 'error'))
.asBroadcastStream();
late final Stream<VpnStatsEvent> _statsStream = _statsRaw
.receiveBroadcastStream()
.where((e) => e is Map)
.map((e) => VpnStatsEvent.fromNativeMap(
Map<Object?, Object?>.from(e as Map),
))
.asBroadcastStream();
@override
Stream<VpnStatus> get statusStream => _statusStream;
@override
Stream<VpnStatsEvent> get statsStream => _statsStream;
@override
void dispose() {
// EventChannel 由 Flutter 引擎管理,无需手动关闭
}
}