Flutter for OpenHarmony:dart_ping 网络诊断的瑞士军刀(支持 ICMP Ping) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

前言

在应用开发中,网络连通性检测是一个强需求。

  • 用户的网络是 WiFi 还是 4G?
  • 虽然连着 WiFi,但是否真的能通公网(Ping www.baidu.com)?
  • 连接内网服务器的延迟是多少?

虽然 connectivity_plus 可以告诉我们网络类型(WiFi/Mobile),但它无法检测实际的连通性(比如 WiFi 连上了但没网)。这时,最直接的手段就是 Ping

dart_ping 是一个跨平台的 Dart Ping 库。它并不重新实现 ICMP 协议(那需要 root 权限),而是巧妙地封装了各操作系统的原生 ping 命令,并解析其输出流。

对于 OpenHarmony,虽然其底层基于 Linux 内核,但由于该库内部对 Platform 类型的严格检查以及应用沙箱对进程 fork 的限制,dart_ping 的标准用法目前在鸿蒙系统上会直接失效。

一、核心原理

dart_ping 的工作原理非常 Unix:

  1. Spawn Process : 启动一个子进程运行 ping google.com
  2. Stream Output: 监听 stdout 和 stderr。
  3. Regex Parse : 使用正则表达式解析每一行输出(如 64 bytes from ... time=23ms)。
  4. Dart Object : 转换为结构化的 PingData 对象供业务使用。

Ping('baidu.com')
Process.start
ping cmd
ICMP Echo Reply
stdout
正则解析
更新 UI
Flutter App
DartPing 封装
系统 Shell
鸿蒙/Linux Kernel
Stream
延迟显示

二、集成与用法详解

2.1 添加依赖

yaml 复制代码
dependencies:
  dart_ping: ^9.0.1

2.2 基础用法

dart 复制代码
import 'package:dart_ping/dart_ping.dart';

void main() async {
  // 1. 创建 Ping 实例
  // count: 5 表示 ping 5 次后自动停止
  final ping = Ping('www.qq.com', count: 5);

  // 2. 监听流
  ping.stream.listen((PingData event) {
    if (event.response != null) {
      print('收到回复: ${event.response!.time}ms, seq=${event.response!.seq}');
    } else if (event.error != null) {
      print('Ping 错误: ${event.error}');
    } else if (event.summary != null) {
      print('统计: 发送 ${event.summary!.transmitted}, 接收 ${event.summary!.received}');
    }
  });

  // 也可以手动通过 ping.stop() 停止无线 ping
}

2.3 鸿蒙的权限与环境封锁

在 OpenHarmony 应用沙箱中,应用层开发者通常会面临以下两个致命障碍:

  1. 库平台识别失败dart_ping 内部会检查 Platform.isAndroidPlatform.isIOS。由于 OpenHarmony 返回的是自己的平台 ID,库会直接抛出:
    UnimplementedError: Ping not supported on this platform
  2. 进程 fork 被禁 :即便绕过库直接调用 Process.start('ping', ...),也会因为沙箱隔离无法访问系统的 /bin/ping 二进制文件,从而抛出:
    ProcessException: No such file or directory

结论:在标准鸿蒙 HAP 开发中,依赖系统 ping 命令的方案是不可行的。

3.2 替代方案:TCP 与 HTTP 探测(推荐)

既然 ICMP 受限,我们应该使用 Dart 原语提供的网络能力来实现类似的连通性检测。

A. TCP 握手检测 (TCP Ping)

这是最通用的方式,不依赖系统底层命令,在模拟器和真机上均百分之百可用。

dart 复制代码
import 'dart:io';

Future<bool> tcpPing(String host, int port, {Duration timeout = const Duration(seconds: 2)}) async {
  try {
    // 尝试建立连接,成功响应即代表服务器可达
    final socket = await Socket.connect(host, port, timeout: timeout);
    socket.destroy();
    return true;
  } catch (e) {
    print('TCP Connect Failed: $e');
    return false;
  }
}
B. HTTP HEAD 请求

如果你检测的是 Web 服务器,发送一个 HEAD 请求(不下载包体)是非常节省流量且专业的方式。

dart 复制代码
import 'package:http/http.dart' as http;

Future<bool> httpCheck(String url) async {
  try {
    // 只请求响应头,不下载内容,速度极快
    final response = await http.head(Uri.parse(url)).timeout(Duration(seconds: 2));
    return response.statusCode < 400; // 2xx 或 3xx 均代表服务存活
  } catch (e) {
    return false;
  }
}

3.3 如果你有权限:适配 Ping 解析器

dart_ping 允许自定义解析器。如果鸿蒙的 ping 命令输出格式与标准 Linux 不同(例如这是 BusyBox 修改版),你需要注册自定义解析器。

dart 复制代码
class OhosPingParser extends PingParser {
  // 实现正则解析逻辑
  @override
  StreamTransformer<String, PingData> get transform => ...
}

final ping = Ping('1.1.1.1', parser: OhosPingParser());

3.4 网络权限配置

无论使用哪种方式,必须在 module.json5 中申请网络权限:

json5 复制代码
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  }
]

四、功能延伸:网络诊断工具

结合 dart_ping,我们可以开发一个鸿蒙版的"网络诊断助手"。

  1. DNS 解析 :使用 InternetAddress.lookup
  2. HTTP 连通性 :使用 diohttp
  3. ICMP 延迟 :使用 dart_ping (如果环境支持)。
dart 复制代码
class NetworkDiagnostics {
  // 综合诊断
  static Stream<String> diagnose(String target) async* {
    yield '开始诊断 $target...';
    
    // 1. DNS
    try {
      var addresses = await InternetAddress.lookup(target);
      yield 'DNS 解析成功: ${addresses.first.address}';
    } catch (e) {
      yield 'DNS 解析失败';
      return;
    }

    // 2. Ping
    yield '正在 Ping...';
    await for (var data in Ping(target, count: 3).stream) {
      if (data.response != null) {
        yield 'Ping: ${data.response!.time}ms';
      }
    }
  }
}

五、总结

dart_ping 是一个非常方便的 wrapper 库,它隔离了底层的 Process 操作细节。

对于 OpenHarmony 开发者:

  • 优先使用 TCP/HTTP :在鸿蒙 HAP 应用中,直接调用系统 ping 命令由于沙箱隔离和库兼容性问题,通常会抛出 UnimplementedError
  • 网络连通性 ≠ ICMP:对于绝大多数业务场景,TCP 探测和 HTTP HEAD 请求已经足够,且在权限管理上更加稳健。

它不仅能用于简单的连通性检查,还能用于计算网络抖动(Jitter)和丢包率,是网络敏感型应用(如实时音视频、游戏加速器)的重要辅助工具。

最佳实践

  1. 不要在主线程运行:Ping 是异步的 Stream,不要阻塞 UI。
  2. 设置超时:务必处理超时逻辑,避免 Ping 进程僵死。
  3. 用户隐私:Ping 用户的内网地址可能涉及隐私,请确保合规。

六、完整实战示例

dart 复制代码
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:dart_ping/dart_ping.dart';

class PingManagerExample extends StatefulWidget {
  const PingManagerExample({super.key});

  @override
  State<PingManagerExample> createState() => _PingManagerExampleState();
}

class _PingManagerExampleState extends State<PingManagerExample> {
  final TextEditingController _hostController =
      TextEditingController(text: 'www.baidu.com');
  final List<String> _logs = [];
  bool _isPinging = false;
  Ping? _ping;
  bool _isAborted = false;

  void _startPing() {
    if (_isPinging) return;
    _isAborted = false;

    setState(() {
      _isPinging = true;
      _logs.clear();
      _logs.add('🔍 正在启动诊断: ${_hostController.text}...');
    });

    try {
      // 尝试使用标准的 ICMP Ping (dart_ping 库)
      _ping = Ping(_hostController.text, count: 5, timeout: 2, interval: 1);

      _ping!.stream.listen((event) {
        if (!mounted) return;
        setState(() {
          if (event.response != null) {
            final time = event.response!.time?.inMilliseconds ?? 0;
            _logs
                .add('✅ ICMP Reply: time=${time}ms seq=${event.response!.seq}');
          } else if (event.error != null) {
            _logs.add('❌ ICMP Error: ${event.error}');
          } else if (event.summary != null) {
            final sum = event.summary!;
            _logs.add(
                '📊 统计: 发送 ${sum.transmitted}, 接收 ${sum.received}, 丢包率 ${((sum.transmitted - sum.received) / sum.transmitted * 100).toStringAsFixed(1)}%');
          }
        });
      }, onDone: () {
        if (mounted) {
          setState(() => _isPinging = false);
          _logs.add('🏁 ICMP 诊断结束');
        }
      }, onError: (e) {
        // 如果流内部抛出"平台不支持"的错误
        if (e.toString().contains('UnimplementedError')) {
          _runTcpFallback();
        } else {
          _handleError('流错误: $e');
        }
      });
    } on UnimplementedError {
      // 捕获同步抛出的平台不支持错误
      _runTcpFallback();
    } catch (e) {
      _handleError('启动失败: $e');
    }
  }

  // TCP 降级方案:当 ICMP 不可用时调用
  void _runTcpFallback() async {
    setState(() {
      _logs.add('⚠️ 系统禁止 ICMP Ping (如 OpenHarmony 沙箱限制)');
      _logs.add('🔄 正在尝试 [TCP 仿真] 降级方案 (Port:80)...');
    });

    int transmitted = 5;
    int received = 0;
    final host = _hostController.text;

    for (int i = 0; i < transmitted; i++) {
      if (_isAborted || !mounted) break;

      final stopwatch = Stopwatch()..start();
      try {
        final socket =
            await Socket.connect(host, 80, timeout: const Duration(seconds: 2));
        stopwatch.stop();
        socket.destroy();
        received++;
        if (mounted) {
          setState(() {
            _logs.add(
                '✅ TCP Connect: $host:80, time=${stopwatch.elapsedMilliseconds}ms');
          });
        }
      } catch (e) {
        if (mounted) {
          setState(() => _logs.add('❌ TCP Failed: $e'));
        }
      }
      await Future.delayed(const Duration(seconds: 1));
    }

    if (mounted) {
      setState(() {
        _isPinging = false;
        _logs.add(
            '📊 TCP 统计: 发送 $transmitted, 成功 $received, 成功率 ${(received / transmitted * 100).toStringAsFixed(0)}%');
        _logs.add('🏁 降级诊断结束');
      });
    }
  }

  void _handleError(String msg) {
    if (mounted) {
      setState(() {
        _isPinging = false;
        _logs.add('🚫 $msg');
      });
    }
  }

  void _stopPing() {
    _isAborted = true;
    _ping?.stop();
    setState(() => _isPinging = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('6.1 全功能网络诊断助手')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _hostController,
                    decoration: const InputDecoration(
                      labelText: '目标主机',
                      hintText: '域名或IP',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 10),
                if (_isPinging)
                  IconButton(
                    icon: const Icon(Icons.stop_circle,
                        color: Colors.red, size: 40),
                    onPressed: _stopPing,
                  )
                else
                  IconButton(
                    icon: const Icon(Icons.play_circle,
                        color: Colors.blue, size: 40),
                    onPressed: _startPing,
                  ),
              ],
            ),
          ),
          const Divider(height: 1),
          Expanded(
            child: Container(
              color: const Color(0xFF1E1E1E),
              width: double.infinity,
              padding: const EdgeInsets.all(8),
              child: ListView.builder(
                itemCount: _logs.length,
                itemBuilder: (context, index) {
                  return Padding(
                    padding: const EdgeInsets.symmetric(vertical: 2),
                    child: Text(
                      _logs[index],
                      style: const TextStyle(
                        color: Colors.lightGreenAccent,
                        fontFamily: 'monospace',
                        fontSize: 13,
                        height: 1.4,
                      ),
                    ),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}
相关推荐
CodeByV1 小时前
【Qt】窗口
开发语言·qt
白露与泡影1 小时前
Java 春招高级面试指南( Java 面试者必备)
java·开发语言·面试
_OP_CHEN1 小时前
【前端开发之JavaScript】(四)JS基础语法下篇:函数与对象核心要点深度解析
开发语言·前端·javascript·界面开发·前端开发·网页开发·语法基础
大尚来也2 小时前
Python 调用 Ollama 本地大模型 API 完全指南
开发语言·python
独行soc2 小时前
2026年渗透测试面试题总结-26(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
班公湖里洗过脚2 小时前
《通过例子学Rust》第18章 错误处理
rust
qq_24218863322 小时前
Python 春节贺卡代码
开发语言·python
wuqingshun3141592 小时前
说一下java的四种引用
java·开发语言
拍客圈2 小时前
Discuz搜索报错
服务器·网络·安全