├── .gitattributes ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib └── multicast.dart ├── pubspec.lock └── pubspec.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/ephemeral 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8bee0834e4be79879f15222cce8fa9642cfefb2b 8 | channel: master 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nightmare-MY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multicast 2 | 3 | 适用于 Dart 的局域网组播+广播实现的消息发送与接收。 4 | 5 | ## 使用的项目 6 | - ADB TOOL 7 | - 无界投屏 8 | - 速享 9 | ## 已知问题 10 | 11 | 在 Android 热点,PC 端连接的情况,PC 端检测 Android 端 UDP 消息会有延迟,具体视局域网而定。 12 | 简单说,PC 端监听 Android 比 Android 端监听 PC 端的延迟要高 。 13 | 14 | ### 相关资料 15 | 16 | 目前设备的发现是通过**组播**加**广播**实现的。 17 | 18 | 在尝试了[multicast_dns](https://github.com/flutter/packages/tree/master/packages/multicast_dns)后,并没有调通实例代码。 19 | 这是躺坑过程中的资料:[https://github.com/flutter/flutter/issues/16335](https://github.com/flutter/flutter/issues/16335) 20 | 21 | Android 默认关闭组播,意味着,局域网其他设备发送的组播消息,Android 设备无法收到,这个问题已经通过内部的 plugin 进行了解决,目前存在的问题是: 22 | 在 Android 设备打开热点,PC 端连接的情况,PC 设备收不到来自 Android 设备的组播消息,所以监听 UDP 的代码中,同时支持了组播与广播,发送 UDP 也同时将 23 | 消息发送到组播地址与广播地址中。 24 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /lib/multicast.dart: -------------------------------------------------------------------------------- 1 | library multicast; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | 7 | import 'dart:isolate'; 8 | 9 | InternetAddress _mDnsAddressIPv4 = InternetAddress('224.0.0.251'); 10 | const int _port = 4545; 11 | typedef MessageCall = void Function(String data, String address); 12 | 13 | Future> _localAddress() async { 14 | List address = []; 15 | final List interfaces = await NetworkInterface.list( 16 | includeLoopback: false, 17 | type: InternetAddressType.IPv4, 18 | ); 19 | for (final NetworkInterface netInterface in interfaces) { 20 | // 遍历网卡 21 | for (final InternetAddress netAddress in netInterface.addresses) { 22 | // 遍历网卡的IP地址 23 | if (netAddress.address.isIPv4) { 24 | address.add(netAddress.address); 25 | } 26 | } 27 | } 28 | return address; 29 | } 30 | 31 | bool _hasMatch(String? value, String pattern) { 32 | return (value == null) ? false : RegExp(pattern).hasMatch(value); 33 | } 34 | 35 | /// 抄的getx 36 | extension IpString on String { 37 | bool get isIPv4 => _hasMatch(this, r'^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$'); 38 | } 39 | 40 | extension Boardcast on RawDatagramSocket { 41 | Future boardcast(String msg, int port) async { 42 | List dataList = utf8.encode(msg); 43 | send(dataList, _mDnsAddressIPv4, port); 44 | await Future.delayed(const Duration(milliseconds: 10)); 45 | final List address = await _localAddress(); 46 | for (final String addr in address) { 47 | final tmp = addr.split('.'); 48 | tmp.removeLast(); 49 | final String addrPrfix = tmp.join('.'); 50 | final InternetAddress address = InternetAddress( 51 | '$addrPrfix.255', 52 | ); 53 | send( 54 | dataList, 55 | address, 56 | port, 57 | ); 58 | } 59 | } 60 | } 61 | 62 | /// 通过组播+广播的方式,让设备能够相互在局域网被发现 63 | class Multicast { 64 | Multicast({this.port = _port}); 65 | final int port; 66 | final List _callback = []; 67 | RawDatagramSocket? _socket; 68 | bool _isStartSend = false; 69 | bool _isStartReceive = false; 70 | final ReceivePort receivePort = ReceivePort(); 71 | Isolate? isolate; 72 | 73 | /// 停止对 udp 发送消息 74 | void stopSendBoardcast() { 75 | if (!_isStartSend) { 76 | return; 77 | } 78 | _isStartSend = false; 79 | isolate?.kill(); 80 | } 81 | 82 | /// 接收udp广播消息 83 | Future _receiveBoardcast() async { 84 | RawDatagramSocket.bind( 85 | InternetAddress.anyIPv4, 86 | port, 87 | reuseAddress: true, 88 | // reusePort: true, 89 | ttl: 255, 90 | ).then((RawDatagramSocket socket) { 91 | // 接收组播消息 92 | socket.joinMulticast(_mDnsAddressIPv4); 93 | // 开启广播支持 94 | socket.broadcastEnabled = true; 95 | socket.readEventsEnabled = true; 96 | socket.listen((RawSocketEvent rawSocketEvent) async { 97 | final Datagram? datagram = socket.receive(); 98 | if (datagram == null) { 99 | return; 100 | } 101 | 102 | String message = utf8.decode(datagram.data); 103 | _notifiAll(message, datagram.address.address); 104 | }); 105 | }); 106 | } 107 | 108 | void _notifiAll(String data, String address) { 109 | for (MessageCall call in _callback) { 110 | call(data, address); 111 | } 112 | } 113 | 114 | Future startSendBoardcast( 115 | List messages, { 116 | Duration duration = const Duration(seconds: 1), 117 | }) async { 118 | if (_isStartSend) { 119 | return; 120 | } 121 | _isStartSend = true; 122 | isolate = await Isolate.spawn( 123 | multicastIsoate, 124 | _IsolateArgs( 125 | receivePort.sendPort, 126 | port, 127 | messages, 128 | duration, 129 | ), 130 | ); 131 | } 132 | 133 | void addListener(MessageCall listener) { 134 | if (!_isStartReceive) { 135 | _receiveBoardcast(); 136 | _isStartReceive = true; 137 | } 138 | _callback.add(listener); 139 | } 140 | 141 | void removeListener(MessageCall listener) { 142 | if (_callback.contains(listener)) { 143 | _callback.remove(listener); 144 | } 145 | } 146 | } 147 | 148 | void multicastIsoate(_IsolateArgs args) { 149 | runZonedGuarded(() { 150 | startSendBoardcast( 151 | args.messages, 152 | args.port, 153 | args.duration, 154 | args.sendPort, 155 | ); 156 | }, (Object error, StackTrace stackTrace) { 157 | print('multicastIsoate error: $error'); 158 | }); 159 | } 160 | 161 | Future startSendBoardcast( 162 | List messages, 163 | int port, 164 | Duration duration, 165 | SendPort sendPort, 166 | ) async { 167 | RawDatagramSocket _socket = await RawDatagramSocket.bind( 168 | InternetAddress.anyIPv4, 169 | 0, 170 | ttl: 255, 171 | reuseAddress: true, 172 | ); 173 | _socket.broadcastEnabled = true; 174 | _socket.readEventsEnabled = true; 175 | final Timer timer = Timer.periodic(duration, (timer) async { 176 | for (String data in messages) { 177 | _socket.boardcast(data, port); 178 | await Future.delayed(Duration(milliseconds: 500)); 179 | } 180 | }); 181 | } 182 | 183 | class _IsolateArgs { 184 | _IsolateArgs( 185 | this.sendPort, 186 | this.port, 187 | this.messages, 188 | this.duration, 189 | ); 190 | 191 | final SendPort sendPort; 192 | final int port; 193 | final List messages; 194 | final Duration duration; 195 | } 196 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: {} 4 | sdks: 5 | dart: ">=2.12.0 <3.0.0" 6 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: multicast 2 | description: A new Flutter package project. 3 | version: 0.0.1 4 | homepage: 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | 9 | dependencies: 10 | --------------------------------------------------------------------------------