flutter attach时候经常出现下面这种错误:
Rerun this command with one of the following passed in as the appId:
flutter attach --app-id com.test.1
flutter attach --app-id com.test.1 (2)
flutter attach --app-id com.test.1 (3)
基于此探索一下与flutter attach相关的内容。
Flutter是一个跨平台的移动应用程序开发框架,Flutter attach是Flutter命令行工具提供的一个命令,用于将开发者的编辑器(如VSCode、Android Studio)连接到正在运行的Flutter应用程序,以便于进行调试。Flutter attach的原理是利用了Dart VM的一个调试协议——VM服务协议,它允许开发者以REST风格的API与Dart VM进行通信。
Flutter attach的连接流程可以大致分为以下几步:
启动Flutter应用程序:开发者使用Flutter run命令启动Flutter应用程序,该命令将启动Dart VM并加载应用程序代码。
启用VM服务:Dart VM支持一个VM服务,用于向外部应用程序提供调试和诊断功能。Flutter run命令会自动启用VM服务,并监听一个默认的端口号(默认为“8181”)。
连接编辑器:开发者使用Flutter attach命令连接编辑器。Flutter attach命令会尝试连接到运行中的Flutter应用程序的VM服务,连接成功后,将在编辑器中打开一个调试会话。
交互调试:在编辑器中,开发者可以设置断点、单步执行代码、查看变量等,通过与Dart VM服务的交互进行调试。
需要注意的是,Flutter attach命令要求开发者在启动Flutter应用程序时启用了VM服务。如果在启动应用程序时未启用VM服务,则无法使用Flutter attach命令进行连接。此外,Flutter attach命令还要求运行中的Flutter应用程序与编辑器在同一台计算机上,或者在通过网络进行通信时,必须通过安全的通道进行连接。
另外flutter attach 命令需要 flutter 应用程序对应的源代码,否则报错:
Target file "lib/main.dart" not found.
因为需要热重载和热重启时,需要比对源代码的修改,做出文件同步,这是可以理解的。
1、attach
连接到 Flutter 应用程序并启动开发工具和调试服务
attach-》 _attachToDevice-》getObservatoryUri-》 _client.start(); -》
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
bool allowExistingDdsInstance = false,
bool enableDevTools = false,
}) async {
_didAttach = true;
try {
await connectToServiceProtocol(
reloadSources: _reloadSourcesService,
restart: _restartService,
compileExpression: _compileExpressionService,
getSkSLMethod: writeSkSL,
allowExistingDdsInstance: allowExistingDdsInstance,
);
// Catches all exceptions, non-Exception objects are rethrown.
} catch (error) { // ignore: avoid_catches_without_on_clauses
if (error is! Exception && error is! String) {
rethrow;
}
globals.printError('Error connecting to the service protocol: $error');
return 2;
}
if (enableDevTools) {
// The method below is guaranteed never to return a failing future.
unawaited(residentDevtoolsHandler.serveAndAnnounceDevTools(
devToolsServerAddress: debuggingOptions.devToolsServerAddress,
flutterDevices: flutterDevices,
));
}
for (final FlutterDevice device in flutterDevices) {
await device.initLogReader();
}
try {
final List<Uri> baseUris = await _initDevFS();
if (connectionInfoCompleter != null) {
// Only handle one debugger connection.
connectionInfoCompleter.complete(
DebugConnectionInfo(
httpUri: flutterDevices.first.vmService.httpAddress,
wsUri: flutterDevices.first.vmService.wsAddress,
baseUri: baseUris.first.toString(),
),
);
}
} on DevFSException catch (error) {
globals.printError('Error initializing DevFS: $error');
return 3;
}
final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
_addBenchmarkData(
'hotReloadInitialDevFSSyncMilliseconds',
initialUpdateDevFSsTimer.elapsed.inMilliseconds,
);
if (!devfsResult.success) {
return 3;
}
for (final FlutterDevice device in flutterDevices) {
// VM must have accepted the kernel binary, there will be no reload
// report, so we let incremental compiler know that source code was accepted.
if (device.generator != null) {
device.generator.accept();
}
final List<FlutterView> views = await device.vmService.getFlutterViews();
for (final FlutterView view in views) {
globals.printTrace('Connected to $view.');
}
}
// In fast-start mode, apps are initialized from a placeholder splashscreen
// app. We must do a restart here to load the program and assets for the
// real app.
if (debuggingOptions.fastStart) {
await restart(
fullRestart: true,
reason: 'restart',
silent: true,
);
}
appStartedCompleter?.complete();
if (benchmarkMode) {
// Wait multiple seconds for the isolate to have fully started.
await Future<void>.delayed(const Duration(seconds: 10));
// We are running in benchmark mode.
globals.printStatus('Running in benchmark mode.');
// Measure time to perform a hot restart.
globals.printStatus('Benchmarking hot restart');
await restart(fullRestart: true);
// Wait multiple seconds to stabilize benchmark on slower device lab hardware.
// Hot restart finishes when the new isolate is started, not when the new isolate
// is ready. This process can actually take multiple seconds.
await Future<void>.delayed(const Duration(seconds: 10));
globals.printStatus('Benchmarking hot reload');
// Measure time to perform a hot reload.
await restart();
if (stayResident) {
await waitForAppToFinish();
} else {
globals.printStatus('Benchmark completed. Exiting application.');
await _cleanupDevFS();
await stopEchoingDeviceLog();
await exitApp();
}
final File benchmarkOutput = globals.fs.file('hot_benchmark.json');
benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
return 0;
}
writeVmServiceFile();
int result = 0;
if (stayResident) {
result = await waitForAppToFinish();
}
await cleanupAtFinish();
return result;
}
上面的代码是 Flutter 开发框架中的一个函数,它在调试模式下连接到 Flutter 应用程序并启动开发工具和调试服务。它有几个参数,用于控制连接和初始化的行为。
函数首先将 _didAttach 标记设置为 true,以表示已经连接到调试服务。然后,它通过调用 connectToServiceProtocol 函数来连接到服务协议,并通过传递几个服务对象来注册服务。
如果连接过程中出现错误,则函数会打印错误消息并返回 2。
如果 enableDevTools 参数设置为 true,则函数会启动开发工具,并在开发工具服务器地址上向客户端广播 DevTools 的可用性。
接下来,函数将对每个 Flutter 设备调用 initLogReader 方法以初始化日志读取器。然后,它将调用 _initDevFS 方法来初始化开发文件系统(DevFS)并获取基本 URI。如果 connectionInfoCompleter 参数不为空,则函数将使用第一个 Flutter 设备的 VM 服务地址和基本 URI 完成 DebugConnectionInfo 对象。
如果在初始化 DevFS 过程中出现错误,则函数会打印错误消息并返回 3。
接下来,函数将调用 _updateDevFS 方法来更新开发文件系统,并将 fullRestart 参数设置为 true。如果更新失败,则函数将返回 3。
然后,函数将对每个 Flutter 设备调用 getFlutterViews 方法以获取 Flutter 视图,并打印连接成功的消息。
如果 debuggingOptions.fastStart 参数设置为 true,则函数将调用 restart 方法以进行全面重启,并在静默模式下重新启动应用程序。
如果 benchmarkMode 参数设置为 true,则函数将测量性能并记录测试结果。首先,函数将等待 10 秒钟以确保隔离环境完全启动。然后,函数将打印开始基准测试的消息,并调用 restart 方法以进行全面重启。然后,函数将再次等待 10 秒钟,以稳定基准测试结果。接下来,函数将打印开始基准测试热重载的消息,并调用 restart 方法以进行热重载。如果 stayResident 参数设置为 true,则函数将等待应用程序运行完成,否则函数将清理 DevFS、停止日志记录并退出应用程序。最后,函数将使用 toPrettyJson 函数将基准测试结果写入文件,并返回 0。
最后,函数将调用 writeVmServiceFile 方法以将 VM 服务地址写入文件。如果 stayResident 参数设置为 true,则函数将调用 waitForAppToFinish 方法并返回其结果。否则,函数将调用 cleanupAtFinish 方法以清理资源,并返回 0。
2、_attachToDevice
Future<void> _attachToDevice(Device device) async {
final FlutterProject flutterProject = FlutterProject.current();
final Daemon daemon = boolArg('machine')
? Daemon(
DaemonConnection(
daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger),
logger: globals.logger,
),
notifyingLogger: (globals.logger is NotifyingLogger)
? globals.logger as NotifyingLogger
: NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger),
logToStdout: true,
)
: null;
Stream<Uri> observatoryUri;
bool usesIpv6 = ipv6;
final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;
if (debugPort == null && debugUri == null) {
if (device is FuchsiaDevice) {
final String module = stringArg('module');
if (module == null) {
throwToolExit("'--module' is required for attaching to a Fuchsia device");
}
usesIpv6 = device.ipv6;
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
try {
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
observatoryUri = Stream<Uri>.value(await isolateDiscoveryProtocol.uri).asBroadcastStream();
} on Exception {
isolateDiscoveryProtocol?.dispose();
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (final ForwardedPort port in ports) {
await device.portForwarder.unforward(port);
}
rethrow;
}
} else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) {
final Uri uriFromMdns =
await MDnsObservatoryDiscovery.instance.getObservatoryUri(
appId,
device,
usesIpv6: usesIpv6,
deviceVmservicePort: deviceVmservicePort,
);
observatoryUri = uriFromMdns == null
? null
: Stream<Uri>.value(uriFromMdns).asBroadcastStream();
}
// If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
if (observatoryUri == null) {
final ProtocolDiscovery observatoryDiscovery =
ProtocolDiscovery.observatory(
// If it's an Android device, attaching relies on past log searching
// to find the service protocol.
await device.getLogReader(includePastLogs: device is AndroidDevice),
portForwarder: device.portForwarder,
ipv6: ipv6,
devicePort: deviceVmservicePort,
hostPort: hostVmservicePort,
logger: globals.logger,
);
globals.printStatus('Waiting for a connection from Flutter on ${device.name}...');
observatoryUri = observatoryDiscovery.uris;
// Determine ipv6 status from the scanned logs.
usesIpv6 = observatoryDiscovery.ipv6;
}
} else {
observatoryUri = Stream<Uri>
.fromFuture(
buildObservatoryUri(
device,
debugUri?.host ?? hostname,
debugPort ?? debugUri.port,
hostVmservicePort,
debugUri?.path,
)
).asBroadcastStream();
}
globals.terminal.usesTerminalUi = daemon == null;
try {
int result;
if (daemon != null) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
AppInstance app;
try {
app = await daemon.appDomain.launch(
runner,
({Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter}) {
return runner.attach(
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter,
allowExistingDdsInstance: true,
enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
);
},
device,
null,
true,
globals.fs.currentDirectory,
LaunchMode.attach,
globals.logger as AppRunLogger,
);
} on Exception catch (error) {
throwToolExit(error.toString());
}
result = await app.runner.waitForAppToFinish();
assert(result != null);
return;
}
while (true) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
final Completer<void> onAppStart = Completer<void>.sync();
TerminalHandler terminalHandler;
unawaited(onAppStart.future.whenComplete(() {
terminalHandler = TerminalHandler(
runner,
logger: globals.logger,
terminal: globals.terminal,
signals: globals.signals,
processInfo: globals.processInfo,
reportReady: boolArg('report-ready'),
pidFile: stringArg('pid-file'),
)
..registerSignalHandlers()
..setupTerminal();
}));
result = await runner.attach(
appStartedCompleter: onAppStart,
allowExistingDdsInstance: true,
enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
);
if (result != 0) {
throwToolExit(null, exitCode: result);
}
terminalHandler?.stop();
assert(result != null);
if (runner.exited || !runner.isWaitingForObservatory) {
break;
}
globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
}
} on RPCError catch (err) {
if (err.code == RPCErrorCodes.kServiceDisappeared) {
throwToolExit('Lost connection to device.');
}
rethrow;
} finally {
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (final ForwardedPort port in ports) {
await device.portForwarder.unforward(port);
}
}
}
- 这是一段 Flutter 命令行工具的 Dart 代码,具体功能是将一个 Flutter 应用程序附加到特定设备的调试器上,以便进行调试。
- 在这段代码中,根据设备类型选择不同的附加方式。例如,如果是 Fuchsia 设备,则使用 FuchsiaIsolateDiscoveryProtocol 协议来查找应用程序,如果是 iOS 设备,则使用 MDnsObservatoryDiscovery 协议查找。如果以上两种方法都失败,则使用 ProtocolDiscovery 协议查找。
- 在找到应用程序的 Uri 后,该应用程序会使用运行中的 daemon 或创建新的 daemon 与设备进行通信。
找到uri http://127.0.0.1:55177/RXKA2jepV60=/
运行while循环接收指令:
while (true) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
final Completer<void> onAppStart = Completer<void>.sync();
TerminalHandler terminalHandler;
unawaited(onAppStart.future.whenComplete(() {
terminalHandler = TerminalHandler(
runner,
logger: globals.logger,
terminal: globals.terminal,
signals: globals.signals,
processInfo: globals.processInfo,
reportReady: boolArg('report-ready'),
pidFile: stringArg('pid-file'),
)
..registerSignalHandlers()
..setupTerminal();
}));
result = await runner.attach(
appStartedCompleter: onAppStart,
allowExistingDdsInstance: true,
enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
);
if (result != 0) {
throwToolExit(null, exitCode: result);
}
terminalHandler?.stop();
assert(result != null);
if (runner.exited || !runner.isWaitingForObservatory) {
break;
}
globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
}
3、getObservatoryUri
@visibleForTesting
Future<MDnsObservatoryDiscoveryResult?> query({String? applicationId, int? deviceVmservicePort}) async {
_logger.printTrace('Checking for advertised Dart observatories...');
try {
await _client.start();
final List<PtrResourceRecord> pointerRecords = await _client
.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(dartObservatoryName),
)
.toList();
if (pointerRecords.isEmpty) {
_logger.printTrace('No pointer records found.');
return null;
}
// We have no guarantee that we won't get multiple hits from the same
// service on this.
final Set<String> uniqueDomainNames = pointerRecords
.map<String>((PtrResourceRecord record) => record.domainName)
.toSet();
String? domainName;
if (applicationId != null) {
for (final String name in uniqueDomainNames) {
if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
domainName = name;
break;
}
}
if (domainName == null) {
throwToolExit('Did not find a observatory port advertised for $applicationId.');
}
} else if (uniqueDomainNames.length > 1) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('There are multiple observatory ports available.');
buffer.writeln('Rerun this command with one of the following passed in as the appId:');
buffer.writeln();
for (final String uniqueDomainName in uniqueDomainNames) {
buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
}
throwToolExit(buffer.toString());
} else {
domainName = pointerRecords[0].domainName;
}
_logger.printTrace('Checking for available port on $domainName');
// Here, if we get more than one, it should just be a duplicate.
final List<SrvResourceRecord> srv = await _client
.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(domainName),
)
.toList();
if (srv.isEmpty) {
return null;
}
if (srv.length > 1) {
_logger.printWarning('Unexpectedly found more than one observatory report for $domainName '
'- using first one (${srv.first.port}).');
}
_logger.printTrace('Checking for authentication code for $domainName');
final List<TxtResourceRecord> txt = await _client
.lookup<TxtResourceRecord>(
ResourceRecordQuery.text(domainName),
)
.toList();
if (txt == null || txt.isEmpty) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
const String authCodePrefix = 'authCode=';
String? raw;
for (final String record in txt.first.text.split('\n')) {
if (record.startsWith(authCodePrefix)) {
raw = record;
break;
}
}
if (raw == null) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
String authCode = raw.substring(authCodePrefix.length);
// The Observatory currently expects a trailing '/' as part of the
// URI, otherwise an invalid authentication code response is given.
if (!authCode.endsWith('/')) {
authCode += '/';
}
return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
} finally {
_client.stop();
}
}
代码流程如下:
- 打印日志,开始查找已经广告的Dart Observatory。
- 启动MDNS客户端。
- 通过客户端查询指向Dart Observatory的指针记录(PtrResourceRecord)。
- 如果找不到指针记录,打印日志并返回null。
- 如果找到指针记录,将其唯一的域名添加到集合中。
- 如果提供了应用程序ID,则在集合中查找以该ID开头的唯一域名。如果找不到,则抛出异常。
- 如果未提供应用程序ID,并且集合中有多个唯一的域名,则打印建议的应用程序ID并抛出异常。
- 如果未提供应用程序ID,并且集合中只有一个唯一的域名,则使用该唯一的域名。
- 检查所选域名上是否有可用端口。
- 如果有多个服务记录(SrvResourceRecord),则使用第一个记录的端口。
- 检查所选域名上是否有身份验证代码(authCode)。
- 如果没有身份验证代码,则返回使用第一个服务记录的端口和空的身份验证代码的MDnsObservatoryDiscoveryResult。
- 如果有身份验证代码,则从TXT资源记录中提取该代码。
- 如果找不到身份验证代码,则返回使用第一个服务记录的端口和空的身份验证代码的MDnsObservatoryDiscoveryResult。
- 如果找到了身份验证代码,则将其分配给MDnsObservatoryDiscoveryResult,同时确保代码以"/"结尾。
- 停止MDNS客户端。
- 返回使用所选域名的第一个服务记录的端口和身份验证代码的MDnsObservatoryDiscoveryResult。
总的来说,每一次的attach都会启动一个启动MDNS客户端,如果启动失败,则停止MDNS客户端。
4、 await _client.start();
Future<void> start({
InternetAddress? listenAddress,
NetworkInterfacesFactory? interfacesFactory,
int mDnsPort = mDnsPort,
InternetAddress? mDnsAddress,
}) async {
listenAddress ??= InternetAddress.anyIPv4;
interfacesFactory ??= allInterfacesFactory;
assert(listenAddress.address == InternetAddress.anyIPv4.address ||
listenAddress.address == InternetAddress.anyIPv6.address);
if (_started || _starting) {
return;
}
_starting = true;
final int selectedMDnsPort = _mDnsPort = mDnsPort;
_mDnsAddress = mDnsAddress;
// Listen on all addresses.
final RawDatagramSocket incoming = await _rawDatagramSocketFactory(
listenAddress.address,
selectedMDnsPort,
reuseAddress: true,
reusePort: true,
ttl: 255,
);
// Can't send to IPv6 any address.
if (incoming.address != InternetAddress.anyIPv6) {
_sockets.add(incoming);
} else {
_toBeClosed.add(incoming);
}
_mDnsAddress ??= incoming.address.type == InternetAddressType.IPv4
? mDnsAddressIPv4
: mDnsAddressIPv6;
final List<NetworkInterface> interfaces =
(await interfacesFactory(listenAddress.type)).toList();
for (final NetworkInterface interface in interfaces) {
// Create a socket for sending on each adapter.
final InternetAddress targetAddress = interface.addresses[0];
final RawDatagramSocket socket = await _rawDatagramSocketFactory(
targetAddress,
selectedMDnsPort,
reuseAddress: true,
reusePort: true,
ttl: 255,
);
_sockets.add(socket);
// Ensure that we're using this address/interface for multicast.
if (targetAddress.type == InternetAddressType.IPv4) {
socket.setRawOption(RawSocketOption(
RawSocketOption.levelIPv4,
RawSocketOption.IPv4MulticastInterface,
targetAddress.rawAddress,
));
} else {
socket.setRawOption(RawSocketOption.fromInt(
RawSocketOption.levelIPv6,
RawSocketOption.IPv6MulticastInterface,
interface.index,
));
}
// Join multicast on this interface.
incoming.joinMulticast(_mDnsAddress!, interface);
}
incoming.listen((RawSocketEvent event) => _handleIncoming(event, incoming));
_started = true;
_starting = false;
}
检查是否已经启动或正在启动,如果是则直接返回。
初始化网络地址、接口工厂等参数。
创建一个 RawDatagramSocket 对象,用于接收网络数据。通过 _rawDatagramSocketFactory 方法创建并设置监听地址、端口、地址重用、端口重用等选项。
将创建的 RawDatagramSocket 对象添加到 _sockets 列表中,如果地址为 InternetAddress.anyIPv6,则添加到 _toBeClosed 列表中。
确定 mDNS 地址,如果没有传入 mDNS 地址,则根据监听地址类型选择 IPv4 或 IPv6 的默认 mDNS 地址。
获取本地网络接口列表,并对每个接口创建一个 RawDatagramSocket 对象,用于发送网络数据。对每个接口设置监听地址、端口、地址重用、端口重用等选项,并添加到 _sockets 列表中。对于 IPv4 接口,使用 setRawOption 方法设置 IPv4 组播接口,对于 IPv6 接口,使用 setRawOption 方法设置 IPv6 组播接口。
_sockets.add(socket);会发现有3个sockets
0.0.0.0,127.0.0.1,253.53.111.111 这三个ip地址应该对应是同一个主机。
- 对接收 RawDatagramSocket 对象调用 joinMulticast 方法,加入 mDNS 组播地址和本地网络接口。
- 对接收 RawDatagramSocket 对象调用 listen 方法,监听网络事件并调用 _handleIncoming 方法处理网络数据。
// Process incoming datagrams.
void _handleIncoming(RawSocketEvent event, RawDatagramSocket incoming) {
if (event == RawSocketEvent.read) {
final Datagram? datagram = incoming.receive();
if (datagram == null) {
return;
}
// Check for published responses.
final List<ResourceRecord>? response = decodeMDnsResponse(datagram.data);
if (response != null) {
_cache.updateRecords(response);
_resolver.handleResponse(response);
return;
}
// TODO(dnfield): Support queries coming in for published entries.
}
}
在_handleIncoming的数据回调中,可看到数据长这样:
- 设置 _started 标志表示已启动,设置 _starting 标志表示正在启动。
总的来说,在Flutter中,mdnsclient.start是启动一个mDNS客户端的方法,用于在本地网络上发现可用的服务。
mDNS是一种广泛使用的服务发现协议,可以通过在本地网络中进行广播和响应来发现可用的服务。mDNS客户端使用查询报文向本地网络中的所有设备发送请求,以查找可用的服务。一旦某个设备响应了请求,mDNS客户端就会接收到包含服务信息的响应报文。
mdnsclient.start方法会启动一个mDNS客户端,并开始向本地网络中发送查询报文。当发现可用的服务时,客户端将回调一个提供服务信息的回调函数,以便应用程序可以处理这些信息。通过这种方式,应用程序可以在本地网络中发现可用的服务,并使用这些服务进行网络通信。