2023-03-03 flutter attach流程2

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)
image.png
image.png

通过在运行时添加参数就能解决此问题。

基于此探索一下与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。

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的数据回调中,可看到数据长这样:

image.png
  • 设置 _started 标志表示已启动,设置 _starting 标志表示正在启动。

总的来说,在Flutter中,mdnsclient.start是启动一个mDNS客户端的方法,用于在本地网络上发现可用的服务。

mDNS是一种广泛使用的服务发现协议,可以通过在本地网络中进行广播和响应来发现可用的服务。mDNS客户端使用查询报文向本地网络中的所有设备发送请求,以查找可用的服务。一旦某个设备响应了请求,mDNS客户端就会接收到包含服务信息的响应报文。

mdnsclient.start方法会启动一个mDNS客户端,并开始向本地网络中发送查询报文。当发现可用的服务时,客户端将回调一个提供服务信息的回调函数,以便应用程序可以处理这些信息。通过这种方式,应用程序可以在本地网络中发现可用的服务,并使用这些服务进行网络通信。

5、 MDnsObservatoryDiscoveryResult(srv.first.port, authCode)

image.png

6、buildObservatoryUri

  • MDnsObservatoryDiscovery.instance.getObservatoryUri
        final Uri uriFromMdns =
          await MDnsObservatoryDiscovery.instance.getObservatoryUri(
            appId,
            device,
            usesIpv6: usesIpv6,
            deviceVmservicePort: deviceVmservicePort,
          );
        observatoryUri = uriFromMdns == null
          ? null
          : Stream<Uri>.value(uriFromMdns).asBroadcastStream();
  • query(
    applicationId: applicationId,
    deviceVmservicePort: deviceVmservicePort,
    );
final MDnsObservatoryDiscoveryResult? result = await query(
      applicationId: applicationId,
      deviceVmservicePort: deviceVmservicePort,
    );
  • buildObservatoryUri
Future<Uri> buildObservatoryUri(
  Device device,
  String host,
  int devicePort, [
  int? hostVmservicePort,
  String? authCode,
]) async {
  String path = '/';
  if (authCode != null) {
    path = authCode;
  }
  // Not having a trailing slash can cause problems in some situations.
  // Ensure that there's one present.
  if (!path.endsWith('/')) {
    path += '/';
  }
  hostVmservicePort ??= 0;
  final int? actualHostPort = hostVmservicePort == 0 ?
    await device.portForwarder?.forward(devicePort) :
    hostVmservicePort;
  return Uri(scheme: 'http', host: host, port: actualHostPort, path: path);
}

最终是得到了一个这样的 Uri:
http://127.0.0.1:55612/tJgiq9vKwN0=/

7、问题回归:

mDNS代表“多播DNS”,是一种网络协议,用于在局域网上发现可用的设备和服务。它使用了组播IP地址(224.0.0.251)和标准的DNS编解码格式,通过局域网内广播查询和响应的方式来实现设备的发现和服务的注册。mDNS协议的一个重要用途是使Apple的Bonjour技术能够在Mac OS X和其他操作系统上自动发现可用的网络服务和设备。mDNS还可以用于智能家居和物联网设备之间的通信,因为它可以通过在局域网上广播和接收信息来简化设备之间的连接和交互。由于mDNS使用组播地址,因此它可以在不需要集中式服务器的情况下工作,这使得它非常适合于在家庭和小型办公室网络中使用。

flutter tools attach上面getObservatoryUri 这个方法做的就是跟使用dns-sd 命令差不多的事情:

  • 发现可用服务:使用以下命令可以列出网络中可用的所有服务:
dns-sd -B _services._dns-sd._udp     
mdns-
  • 发现特定服务:使用以下命令可以列出特定服务的所有实例:
 dns-sd -B _dartobservatory
image.png
  • 查找网络上运行的 Dart Observatory 实例的命令
    Dart Observatory 是一种用于调试和分析 Dart 代码的工具,它运行在特定的端口上并提供了一系列的调试功能。在调试 Dart 应用程序时,您可以使用 Dart Observatory 监视运行时性能指标,跟踪内存使用情况,以及诊断和修复其他问题。

命令中的 "-Z" 选项告诉命令行实用程序 "dns-sd"(也称为 Bonjour)执行零配置服务发现,并查找名称为 "_dartobservatory" 的服务。如果在本地网络上运行了 Dart Observatory 实例,它应该会在终端中输出有关该实例的详细信息,例如IP地址、端口等。

dns-sd -Z _dartobservatory
image.png

最后我们可以使用在flutter attach 时添加参数:

image.png

Flutter通过将更新的源代码文件注入到正在运行的Dart 虚拟机(VM)来实现热重载。在虚拟机使用新的字段和函数更新类之后, Flutter 框架会自动重新构建 widget 树,以便快速查看更改的效果。

而attach的过程就是一个连接VM的过程,应用以Debug模式运行后,会启动一个VM服务,并且使用mDNS协议( mDNS/DNS-SD 是用于本地局域网服务发现的协议)广播。执行attach操作时,会通过mDNS协议去查找当前应用所匹配的VM服务,再通过WS协议进行连接,这个时候就可能出现如下几种情况,导致连接失败:

  1. 找到的VM服务太多,需要选择连接哪一个。
    在flutter attach参数上添加-app-id 指定appid

  2. 调度问题,没有去连正确的 VM 服务。
    在attach时拔掉网线,断开网络连接,让mDNS找不到局域网内其它设备的VM服务,每次运行前执行 dns-sd -Z _dartobservatory._tcp 查看当前的VM服务,根据上面的设备名和端口选择正确的app-id进行配置。

  3. mDNS缓存没有刷新

  4. mDNS查找问题

  • 关闭个人热点
  • Mac -> 设置 -> 网络 -> iPhone USB -> 不勾选“除非需要,否则请停用”
  • 重试
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容