Flutter技术问题:等待变量变为指定值(UDP设备发现)
问题解决:UDP广播获取ESP8266 IP时的StreamController报错与自身IP过滤问题
问题根源
- 原代码找到ESP IP后关闭了
StreamController,但RawDatagramSocket的监听并未停止,后续收到的数据包(包括自身广播的延迟包)会尝试往已关闭的StreamSink中添加事件,触发Bad state: Cannot add event after closing异常。 - 未正确释放RawDatagramSocket资源,多次调用时存在遗留的socket实例持续发送事件。
- 使用StreamController的实现方式冗余,完全可以用更轻量的
Completer完成异步等待逻辑。
修复方案
用Completer替代StreamController,找到目标IP后立即关闭socket停止接收事件,同时添加超时处理避免无限等待:
import 'dart:async'; import 'dart:io'; class EspDeviceUdp { /// Port to send UDP messages to. int port = 2390; /// IP address of the smartphone. final InternetAddress ownIP; /// IP address of the target device. InternetAddress tarIP = InternetAddress('0.0.0.0'); EspDeviceUdp({required this.ownIP}); /// Tries to find the esp in network while using udp broadcast Future<InternetAddress?> getEspIp({Duration timeout = const Duration(seconds: 5)}) async { final Uint8List tXData = Uint8List(12); const String ipAdr = "255.255.255.255"; const int id = udpIdHandshakeRequest; final completer = Completer<InternetAddress?>(); RawDatagramSocket? udpSocket; // Build tx data stream buildUdpStream(tXData, id, 0, 0, 0); try { udpSocket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port); udpSocket.broadcastEnabled = true; // Listen for incoming datagrams udpSocket.listen((RawSocketEvent event) { if (event == RawSocketEvent.read) { final datagram = udpSocket!.receive(); if (datagram != null) { final address = datagram.address; // Filter out own IP and avoid duplicate completion if (address != ownIP && !completer.isCompleted) { completer.complete(address); udpSocket.close(); } } } }, onError: (error) { if (!completer.isCompleted) { completer.completeError(error); udpSocket?.close(); } }, onDone: () { if (!completer.isCompleted) { completer.complete(null); } }); // Send broadcast request udpSocket.send(tXData, InternetAddress(ipAdr), port); // Add timeout to avoid infinite waiting return await completer.future.timeout(timeout, onTimeout: () { udpSocket?.close(); return null; }); } catch (e) { udpSocket?.close(); rethrow; } } /// Building the tx stream to config the esp device via udp. void buildUdpStream( Uint8List txDataList, int id, int data1, int data2, int data3) { // id txDataList[0] = id & 0xFF; txDataList[1] = (id >> 8) & 0xFF; // data1 txDataList[2] = data1 & 0xFF; txDataList[3] = (data1 >> 8) & 0xFF; // data2 txDataList[4] = data2 & 0xFF; txDataList[5] = (data2 >> 8) & 0xFF; // data3 txDataList[6] = data3 & 0xFF; txDataList[7] = (data3 >> 8) & 0xFF; // reserved txDataList[8] = 0; txDataList[9] = 0; txDataList[10] = 0; txDataList[11] = 0; } // 补充定义缺失的常量(已有则删除) static const int udpIdHandshakeRequest = 0x0100; }
关键修改点说明
Completer替代StreamController:
Completer是专门处理一次性异步结果的工具,比StreamController更适配"等待第一个符合条件结果"的场景,找到目标IP后立即完成异步任务并关闭socket。及时释放Socket资源:
无论找到IP、超时还是发生错误,都会关闭RawDatagramSocket,避免资源泄漏和后续无效事件触发。超时处理:
添加5秒超时逻辑,防止因网络问题或ESP设备未响应导致的无限等待,超时后自动关闭socket并返回null。过滤逻辑优化:
在读取事件中直接过滤自身IP,同时检查completer.isCompleted,避免重复触发完成操作。
测试验证
第二次调用getEspIp()时会创建全新的socket实例,用完即关闭,不会出现旧socket往已关闭的StreamSink发送事件的情况,彻底解决Bad state异常。
内容的提问来源于stack exchange,提问作者Kerendel
相关产品推荐
相关产品推荐

