You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Windows端UDP设备发现功能异常排查与修复求助

问题描述

我正在开发一款通过UDP数据包实现设备互发现的Flutter应用,当前遇到以下问题:

  • Android设备间可正常互发现
  • Windows设备无法发现其他设备,但Android设备能发现Windows设备
  • 已尝试关闭Windows防火墙并添加例外、关闭第三方杀毒软件,问题仍未解决

需求:实现Windows与Android设备的UDP双向互发现,以下是我的代码实现,求解决方法及调试方案。

import 'dart:async';
import 'dart:convert';
import 'dart:io'; // Only for non-Web platforms
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'file_select_page.dart'; // Ensure to import file_select_page

class TransferPage extends StatefulWidget {
  const TransferPage({Key? key}) : super(key: key);

  @override
  _TransferPageState createState() => _TransferPageState();
}

class _TransferPageState extends State<TransferPage> {
  List<Map<String, String>> devices = [];
  bool isScanning = false;
  late RawDatagramSocket receiver;
  Timer? scanTimer;

  @override
  void initState() {
    super.initState();
    startListening();
    scanNetwork(); // Start scanning on initialization
  }

  Future<void> startListening() async {
    receiver = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 22473);
    receiver.listen((RawSocketEvent event) async {
      if (event == RawSocketEvent.read) {
        Datagram? datagram = receiver.receive();
        if (datagram != null) {
          _handleReceivedData(datagram);
        }
      }
    });
  }

  void _handleReceivedData(Datagram datagram) async {
    try {
      final message = utf8.decode(datagram.data, allowMalformed: true);
      final ip = datagram.address.address;

      if (ip != await _getCurrentIP()) {
        final parts = message.split(';');
        String deviceBrand = parts.length > 1 ? parts[1].replaceFirst('Brand:', '') : 'Unknown Brand';
        String deviceModel = parts.length > 2 ? parts[2].replaceFirst('Model:', '') : 'Unknown Model';

        // Deduplication check
        if (!devices.any((device) => device['ip'] == ip)) {
          setState(() {
            devices.add({
              'ip': ip,
              'port': '22473',
              'status': 'Responded',
              'brand': deviceBrand,
              'model': deviceModel
            });
          });
        }
      }
    } catch (e) {
      print('Failed to decode data: $e'); // Handle decode failure
    }
  }

  Future<void> scanNetwork() async {
    setState(() {
      isScanning = true;
      devices.clear(); // Clear device list for new scan
    });

    if (kIsWeb) {
      // Logic for Web platform
      return; // Broadcasting not supported on Windows for now
    } else {
      // Get current IP on non-Web platforms (like Windows)
      String? wifiIP = await _getCurrentIP();
      if (wifiIP == null) {
        setState(() {
          isScanning = false;
        });
        return;
      }

      String broadcastAddress = await getBroadcastAddress(wifiIP);

      // Start periodic broadcasting
      Timer.periodic(Duration(seconds: 1), (timer) async {
        await sendBroadcast(broadcastAddress); // Use dynamically calculated broadcast address
      });

      // Set scan timeout
      scanTimer = Timer(Duration(seconds: 5), stopScanning);
    }
  }

  Future<void> sendBroadcast(String address) async {
    String message;

    // Use Platform property only on non-Web platforms
    if (Platform.isWindows) {
      message = 'Hello from Windows;Brand:Windows;Model:PC';
    } else {
      String deviceBrand = await _getDeviceBrand();
      String deviceModel = await _getDeviceModel();
      message = 'Hello from Flutter;Brand:$deviceBrand;Model:$deviceModel';
    }

    RawDatagramSocket sender = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
    sender.send(utf8.encode(message), InternetAddress(address), 22473); // Send to specified address
    sender.close();
  }

  Future<String?> _getCurrentIP() async {
    final info = NetworkInfo();
    return await info.getWifiIP();
  }

  Future<String> _getDeviceBrand() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    String brand = "Unknown Brand";

    if (Platform.isAndroid) {
      AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
      brand = androidInfo.brand!;
    } else if (Platform.isIOS) {
      IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
      brand = iosInfo.utsname.machine!;
    }

    return brand;
  }

  Future<String> _getDeviceModel() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    String model = "Unknown Model";

    if (Platform.isAndroid) {
      AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
      model = androidInfo.model!;
    } else if (Platform.isIOS) {
      IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
      model = iosInfo.utsname.machine!;
    }

    return model;
  }

  Future<String> getBroadcastAddress(String ip) async {
    List<String> parts = ip.split('.');
    return '${parts[0]}.${parts[1]}.${parts[2]}.255'; // Assuming subnet mask is 255.255.255.0
  }

  void stopScanning() {
    setState(() {
      isScanning = false;
    });
    scanTimer?.cancel();
  }

  @override
  void dispose() {
    receiver.close();
    scanTimer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('File Transfer'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              scanNetwork();
            },
          ),
        ],
      ),
      body: isScanning
          ? Center(child: CircularProgressIndicator())
          : ListView.builder(
        itemCount: devices.length,
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              // Navigate to file_select_page on tapping device
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => TransferFilesPage(
                    deviceName: devices[index]['brand'] ?? 'Unknown Brand',
                    deviceIp: devices[index]['ip'] ?? 'Unknown IP',
                  ),
                ),
              );
            },
            child: Card(
              margin: const EdgeInsets.all(10.0),
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(devices[index]['ip'] ?? 'Unknown IP'),
                        Text('Port: 22473'),
                        Text('Status: ${devices[index]['status'] ?? 'Unknown'}'),
                        Text('Brand: ${devices[index]['brand'] ?? 'Unknown Brand'}'),
                        Text('Model: ${devices[index]['model'] ?? 'Unknown Model'}'),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}
问题分析与修复方案

核心问题排查

  1. 广播地址计算不准确:当前代码硬编码假设子网掩码为255.255.255.0,但Windows设备可能处于非标准子网环境,导致广播包无法覆盖整个局域网。
  2. Windows UDP接收权限缺失:默认绑定InternetAddress.anyIPv4可能未启用广播接收权限,导致无法捕获其他设备的广播包。
  3. 自过滤逻辑错误:通过IP判断是否为自身数据包的逻辑,在Windows多网卡环境下(如有线、无线、虚拟机网卡)会误判,过滤掉其他设备的正常数据包。

具体修复步骤

1. 修复广播地址计算逻辑

替换硬编码的广播地址生成方式,通过真实子网掩码计算正确的广播地址:

Future<String> getBroadcastAddress(String ip) async {
  final interfaces = await NetworkInterface.list(type: InternetAddressType.IPv4, includeLinkLocal: true);
  for (var interface in interfaces) {
    for (var addr in interface.addresses) {
      if (addr.address == ip) {
        // 通过IP和子网掩码计算广播地址
        List<int> ipBytes = InternetAddress(ip).rawAddress;
        List<int> maskBytes = addr.netmask != null ? InternetAddress(addr.netmask!).rawAddress : [255,255,255,0];
        List<int> broadcastBytes = [];
        for (int i=0; i<4; i++) {
          broadcastBytes.add(ipBytes[i] | (~maskBytes[i] & 0xFF));
        }
        return InternetAddress.fromRawAddress(Uint8List.fromList(broadcastBytes)).address;
      }
    }
  }
  //  fallback到原逻辑
  List<String> parts = ip.split('.');
  return '${parts[0]}.${parts[1]}.${parts[2]}.255';
}

2. 启用Windows端UDP广播接收

在startListening中添加广播组加入配置,确保能接收局域网广播包:

Future<void> startListening() async {
  receiver = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 22473);
  // 启用广播接收权限
  receiver.setOption(
    RawSocketOption(IPPROTO_IP, IP_ADD_MEMBERSHIP, 
    InternetAddress('224.0.0.0').rawAddress + InternetAddress.anyIPv4.rawAddress)
  );
  receiver.listen((RawSocketEvent event) async {
    if (event == RawSocketEvent.read) {
      Datagram? datagram = receiver.receive();
      if (datagram != null) {
        _handleReceivedData(datagram);
      }
    }
  });
}

3. 修改自过滤逻辑

放弃IP对比,通过数据包内容判断是否为自身发送的消息:

void _handleReceivedData(Datagram datagram) async {
  try {
    final message = utf8.decode(datagram.data, allowMalformed: true);
    final ip = datagram.address.address;

    // 通过消息前缀判断是否为自身发送的包
    bool isSelfMessage = false;
    if (Platform.isWindows) {
      isSelfMessage = message.startsWith('Hello from Windows');
    } else {
      String deviceBrand = await _getDeviceBrand();
      isSelfMessage = message.startsWith('Hello from Flutter;Brand:$deviceBrand');
    }

    if (!isSelfMessage) {
      final parts = message.split(';');
      String deviceBrand = parts.length > 1 ? parts[1].replaceFirst('Brand:', '') : 'Unknown Brand';
      String deviceModel = parts.length > 2 ? parts[2].replaceFirst('Model:', '') : 'Unknown Model';

      if (!devices.any((device) => device['ip'] == ip)) {
        setState(() {
          devices.add({
            'ip': ip,
            'port': '22473',
            'status': 'Responded',
            'brand': deviceBrand,
            'model': deviceModel
          });
        });
      }
    }
  } catch (e) {
    print('Failed to decode data: $e');
  }
}

调试方案

  • 抓包验证:在Windows上用Wireshark监听UDP端口22473,确认是否收到Android设备的广播包。若未收到,检查广播地址是否正确;若收到但代码未处理,检查过滤逻辑。
  • 日志输出:在_handleReceivedData中添加详细日志,打印收到的IP、消息内容,确认数据包是否被错误过滤。
  • 多网卡检查:打印Windows所有网卡的IP地址,确认_getCurrentIP()返回的是当前连接WiFi的IP,避免多网卡导致的逻辑错误。

内容的提问来源于stack exchange,提问作者danmo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 19:29:49