Flutter网络通讯插件zerotier_sockets的使用

Flutter网络通讯插件zerotier_sockets的使用

zerotier_sockets 是一个用于 Flutter 的插件,它提供了对 libzt 库的绑定。该库使用了 dart:ffi

支持的平台

目前仅支持 Android 平台。

要支持其他平台:

  1. 需要在对应的平台文件夹中创建默认内容。
  2. 将从 libzt 构建的库文件包含到相应的平台构建过程中。
  3. 修改 loader.dart 以包含新的平台。

使用方法

更多详细用法请参见 example 文件夹。也可以参考 ZeroTier Sockets 教程

目前只实现了客户端TCP套接字。

import 'package:zerotier_sockets/zerotier_sockets.dart';
import 'package:path_provider/path_provider.dart';

Future<void> startNodeAndConnectToNetwork(String networkId) async {
  // 获取节点实例
  var node = ZeroTierNode.instance;

  // 设置持久化存储路径,以便缓存身份和网络配置
  // 你也可以使用 initSetIdentity 从内存设置身份,但网络配置不会被缓存
  var appDocPath = (await getApplicationDocumentsDirectory()).path + '/zerotier_node';
  node.initSetPath(appDocPath);
  
  // 尝试启动节点
  var result = node.start();
  if (!result.success) {
    throw Exception('Failed to start node: $result');
  } 

  // 等待节点上线
  await node.waitForOnline();

  // 解析网络ID
  var nwId = BigInt.parse(networkId, radix: 16);
  
  // 加入网络
  result = node.join(nwId);
  if (!result.success) {
    throw Exception('Failed to join network: $result');
  }
  
  // 等待网络准备就绪
  await node.waitForNetworkReady(nwId);
  await node.waitForAddressAssignment(nwId);

  // 获取网络信息
  var networkInfo = node.getNetworkInfo(nwId);
  print(networkInfo.name);
  print(networkInfo.address);
  print(networkInfo.id);

  ZeroTierSocket socket;
  try {
    // 连接套接字
    socket = await ZeroTierSocket.connect('10.144.242.244', 22);
  } catch (e) {
    print('Failed to connect socket: $e');
    socket.close();
    return;
  }
  
  // 发送数据
  socket.sink.add([1, 2, 3, 4, 5]);

  // 监听数据
  socket.stream.listen((data) => print('received ${data.length} byte(s)'));

  // 检测套接字关闭
  socket.done.then((_) => print('socket closed'));
  
  // 不要忘记关闭套接字
  socket.close();
}

事件

libzt 提供了一种方式,可以通过回调方法在后台线程中调用事件发生时的方法。

问题是 Dart 和 Flutter 不支持在隔离线程中调用。

解决方法包括:

  1. 分叉 libzt 并使用 Dart_Port,如这里所述。
  2. 等待 Dart 团队实现从后台线程调用。
  3. 可以使用平台通道,但这必须为每个平台单独实现,并且不是理想的选择。此外,Java API 传递的事件和数据有限。

在前两种情况下,libzt 必须使用 ZTS_C_API_ONLY 宏构建,因为不会使用 Java 回调。

示例代码

以下是完整的示例代码,展示了如何使用 zerotier_sockets 插件来启动节点并连接到网络。

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:zerotier_sockets/zerotier_sockets.dart';

import 'dialogs.dart';
import 'network_model.dart';
import 'socket_model.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: "ZeroTier Sockets",
      home: HomePage(),
    );
  }
}

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

  [@override](/user/override)
  HomePageState createState() => HomePageState();
}

class HomePageState extends State<HomePage> {
  HomePageState() {
    _node = ZeroTierNode.instance;
  }

  late final ZeroTierNode _node;
  late final Timer _timer;

  bool _startNode = false;
  bool _allowStartNode = true;
  bool _running = false;
  bool _online = false;
  String _identity = 'none';

  final List<BigInt> _networkIds = [];
  final List<NetworkModel> _networks = [];
  final List<SocketModel> _sockets = [];

  Future<void> _toggleStartNode(bool value) async {
    setState(() => _allowStartNode = false);

    _startNode = value;
    if (value) {
      _node.start();
    } else {
      _networkIds.clear();
      _networks.clear();
      _node.stop();
    }

    // 防止重复操作导致崩溃的小冷却时间
    await Future.delayed(const Duration(milliseconds: 500));
    setState(() => _allowStartNode = true);
  }

  Future<void> _onJoinNetwork() async {
    var result = await InputDialog.show(context, title: 'Join network', hint: 'Network ID', sendText: 'ADD');
    if (result != null) {
      var networkId = BigInt.tryParse(result, radix: 16);
      if (networkId != null) {
        _networkIds.add(networkId);
        _node.join(networkId);
      }
    }
  }

  Future<void> _onLeaveNetwork(BigInt networkId) async {
    var result = await BoolDialog.show(context, title: 'Leave network?');
    if (result == true) {
      _networkIds.remove(networkId);
      _node.leave(networkId);
    }
  }

  Future<void> _onCreateSocket() async {
    var address = await InputDialog.show(context, title: 'Connect socket', hint: 'IP address', sendText: 'NEXT');
    if (address == null) {
      return;
    }

    var port = await InputDialog.show(context, title: 'Connect socket', hint: 'Port', sendText: 'CONNECT');
    if (port == null || int.tryParse(port) == null) {
      return;
    }

    _sockets.add(SocketModel(address, int.parse(port)));
    setState(() {});
  }

  Future<void> _onSocketPress(SocketModel socket) async {
    await ActionDialog.show(
      context,
      title: 'Socket',
      actions: [
        ActionDialogAction(text: 'Reconnect', onPressed: socket.reconnect),
        ActionDialogAction(text: 'Disconnect', onPressed: socket.disconnect),
        ActionDialogAction(
          text: 'Delete',
          onPressed: () {
            _sockets.remove(socket);
            setState(() {});
          },
        ),
      ],
    );
  }

  [@override](/user/override)
  void initState() {
    super.initState();
    _timer = Timer.periodic(const Duration(milliseconds: 500), (t) => _updateStatus());
  }

  [@override](/user/override)
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  void _updateStatus() {
    bool needUpdate = false;

    if (_running != _node.running) {
      _running = _node.running;
      needUpdate = true;
    }

    if (_online != _node.online) {
      _online = _node.online;
      needUpdate = true;
    }

    final id = _node.getId();
    if (id.success) {
      var identity = id.data.toRadixString(16);
      if (identity != _identity) {
        _identity = identity ?? 'none';
        needUpdate = true;
      }
    } else {
      if (_identity != 'none') {
        _identity = 'none';
        needUpdate = true;
      }
    }

    NetworkModel createNetworkModel(BigInt id) {
      var info = _node.getNetworkInfo(id);
      if (info != null) {
        return NetworkModel.fromInfo(info);
      } else {
        return NetworkModel.fromId(id);
      }
    }

    if (_networks.length != _networkIds.length) {
      needUpdate = true;
      _networks.clear();
      for (var id in _networkIds) {
        _networks.add(createNetworkModel(id));
      }
    } else {
      for (var i = 0; i < _networkIds.length; i++) {
        var model = createNetworkModel(_networkIds[i]);
        if (!model.equals(_networks[i])) {
          _networks[i] = model;
          needUpdate = true;
        }
      }
    }

    if (needUpdate) {
      setState(() {});
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ZeroTier Sockets'),
        ),
        body: Column(children: [
          Expanded(
            child: SingleChildScrollView(
              child: Material(
                color: Colors.transparent,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    const SizedBox(height: 16),
                    SwitchListTile(
                      title: const Text('Start node'),
                      value: _startNode,
                      enableFeedback: true,
                      onChanged: _allowStartNode ? _toggleStartNode : null,
                    ),
                    ListTile(
                      title: const Text('Status'),
                      trailing: Text(
                        _running ? (_online ? 'online' : 'connecting...') : 'not running',
                      ),
                    ),
                    ListTile(
                      title: const Text('Identity'),
                      trailing: Text(_identity),
                    ),
                    ...(!_running)
                        ? []
                        : [
                            Padding(
                              padding: const EdgeInsets.all(16.0),
                              child: Text('Networks', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.start),
                            ),
                            ..._networks.map((model) {
                              return ListTile(
                                leading: const Icon(Icons.public),
                                minLeadingWidth: 20,
                                title: Text(model.name),
                                subtitle: Text('${model.id.toRadixString(16)}\r\n${model.address} • ${model.type}'),
                                trailing: Text(model.status),
                                onTap: () => _onLeaveNetwork(model.id),
                              );
                            }).toList(),
                            ListTile(
                              leading: const Icon(Icons.add),
                              minLeadingWidth: 20,
                              title: const Text('Join network'),
                              onTap: _onJoinNetwork,
                            ),
                            Padding(
                              padding: const EdgeInsets.all(16.0),
                              child: Text('Socket instances', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.start),
                            ),
                            ..._sockets.map(
                              (e) => AnimatedBuilder(
                                animation: e,
                                builder: (context, child) => ListTile(
                                  leading: const Icon(Icons.public),
                                  minLeadingWidth: 20,
                                  title: Text('${e.address}:${e.port}'),
                                  subtitle: Text(e.status),
                                  onTap: () => _onSocketPress(e),
                                ),
                              ),
                            ),
                            ListTile(
                              leading: const Icon(Icons.add),
                              minLeadingWidth: 20,
                              title: const Text('Create socket'),
                              onTap: _onCreateSocket,
                            ),
                          ],
                  ],
                ),
              ),
            ),
          )
        ]),
      ),
    );
  }
}

更多关于Flutter网络通讯插件zerotier_sockets的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter网络通讯插件zerotier_sockets的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter项目中使用zerotier_sockets插件进行网络通讯的示例代码。这个插件允许你通过ZeroTier网络进行套接字通信。

首先,确保你已经在pubspec.yaml文件中添加了zerotier_sockets依赖:

dependencies:
  flutter:
    sdk: flutter
  zerotier_sockets: ^latest_version  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

接下来,我们将展示一个简单的示例,该示例将展示如何初始化ZeroTier套接字并进行基本的网络通信。

1. 初始化ZeroTier并获取网络状态

import 'package:flutter/material.dart';
import 'package:zerotier_sockets/zerotier_sockets.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ZeroTierSockets? _zeroTierSockets;
  String _status = 'Initializing...';

  @override
  void initState() {
    super.initState();
    _initZeroTier();
  }

  void _initZeroTier() async {
    _zeroTierSockets = ZeroTierSockets();

    _zeroTierSockets!.init().then((success) {
      if (success) {
        _zeroTierSockets!.getNetworkStatus().then((status) {
          setState(() {
            _status = 'ZeroTier Status: ${status.online ? 'Online' : 'Offline'}';
          });
        });
      } else {
        setState(() {
          _status = 'Failed to initialize ZeroTier';
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('ZeroTier Flutter Example'),
        ),
        body: Center(
          child: Text(_status),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _zeroTierSockets?.dispose();
    super.dispose();
  }
}

2. 创建一个TCP客户端并连接到服务器

接下来,我们将展示如何使用zerotier_sockets插件创建一个TCP客户端并连接到指定的服务器。

import 'dart:typed_data';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:zerotier_sockets/zerotier_sockets.dart';

class TCPClientScreen extends StatefulWidget {
  @override
  _TCPClientScreenState createState() => _TCPClientScreenState();
}

class _TCPClientScreenState extends State<TCPClientScreen> {
  ZeroTierSockets? _zeroTierSockets;
  TCPSocket? _socket;
  String _response = '';
  TextEditingController _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    _zeroTierSockets = ZeroTierSockets();
    _connectToServer();
  }

  void _connectToServer() async {
    String serverAddress = '192.168.xxx.xxx'; // 替换为你的ZeroTier网络中的服务器地址
    int port = 12345; // 替换为你的服务器端口号

    _socket = await _zeroTierSockets!.createTCPSocket();
    _socket!.connect(serverAddress, port).then((success) {
      if (success) {
        _socket!.listen((Uint8List data) {
          setState(() {
            _response += String.fromCharCodes(data);
          });
        });
      } else {
        setState(() {
          _response = 'Failed to connect to server';
        });
      }
    });
  }

  void _sendMessage() async {
    if (_socket!.isOpen) {
      String message = _controller.text;
      Uint8List data = Uint8List.fromList(utf8.encode(message));
      _socket!.write(data);
      _controller.clear();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TCP Client'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Server Response:', style: TextStyle(fontSize: 18)),
            SizedBox(height: 16),
            Expanded(
              child: SingleChildScrollView(
                child: Text(_response, style: TextStyle(fontSize: 16)),
              ),
            ),
            SizedBox(height: 16),
            TextField(
              controller: _controller,
              decoration: InputDecoration(
                border: OutlineInputBorder(),
                labelText: 'Message',
              ),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: _sendMessage,
              child: Text('Send'),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _socket?.close();
    _zeroTierSockets?.dispose();
    _controller.dispose();
    super.dispose();
  }
}

3. 使用TCP客户端屏幕

现在,你可以将TCP客户端屏幕添加到你的应用程序中:

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('ZeroTier Flutter Example'),
        ),
        body: TCPClientScreen(), // 使用TCP客户端屏幕
      ),
    );
  }
}

这个示例展示了如何使用zerotier_sockets插件在Flutter应用程序中进行基本的网络通讯。请根据你的实际需求调整服务器地址和端口号。

回到顶部