2023-03-03 flutter attach流程2

flutter attach時候經(jīng)常出現(xiàn)下面這種錯誤:

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

通過在運行時添加參數(shù)就能解決此問題。

基于此探索一下與flutter attach相關(guān)的內(nèi)容。

Flutter是一個跨平臺的移動應用程序開發(fā)框架,F(xiàn)lutter attach是Flutter命令行工具提供的一個命令,用于將開發(fā)者的編輯器(如VSCode、Android Studio)連接到正在運行的Flutter應用程序,以便于進行調(diào)試。Flutter attach的原理是利用了Dart VM的一個調(diào)試協(xié)議——VM服務協(xié)議,它允許開發(fā)者以REST風格的API與Dart VM進行通信。

Flutter attach的連接流程可以大致分為以下幾步:

  • 啟動Flutter應用程序:開發(fā)者使用Flutter run命令啟動Flutter應用程序,該命令將啟動Dart VM并加載應用程序代碼。

  • 啟用VM服務:Dart VM支持一個VM服務,用于向外部應用程序提供調(diào)試和診斷功能。Flutter run命令會自動啟用VM服務,并監(jiān)聽一個默認的端口號(默認為“8181”)。

  • 連接編輯器:開發(fā)者使用Flutter attach命令連接編輯器。Flutter attach命令會嘗試連接到運行中的Flutter應用程序的VM服務,連接成功后,將在編輯器中打開一個調(diào)試會話。

  • 交互調(diào)試:在編輯器中,開發(fā)者可以設置斷點、單步執(zhí)行代碼、查看變量等,通過與Dart VM服務的交互進行調(diào)試。

需要注意的是,F(xiàn)lutter attach命令要求開發(fā)者在啟動Flutter應用程序時啟用了VM服務。如果在啟動應用程序時未啟用VM服務,則無法使用Flutter attach命令進行連接。此外,F(xiàn)lutter attach命令還要求運行中的Flutter應用程序與編輯器在同一臺計算機上,或者在通過網(wǎng)絡進行通信時,必須通過安全的通道進行連接。

另外flutter attach 命令需要 flutter 應用程序?qū)脑创a,否則報錯:
Target file "lib/main.dart" not found.
因為需要熱重載和熱重啟時,需要比對源代碼的修改,做出文件同步,這是可以理解的。

1、attach

連接到 Flutter 應用程序并啟動開發(fā)工具和調(diào)試服務

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 開發(fā)框架中的一個函數(shù),它在調(diào)試模式下連接到 Flutter 應用程序并啟動開發(fā)工具和調(diào)試服務。它有幾個參數(shù),用于控制連接和初始化的行為。

  • 函數(shù)首先將 _didAttach 標記設置為 true,以表示已經(jīng)連接到調(diào)試服務。然后,它通過調(diào)用 connectToServiceProtocol 函數(shù)來連接到服務協(xié)議,并通過傳遞幾個服務對象來注冊服務。

  • 如果連接過程中出現(xiàn)錯誤,則函數(shù)會打印錯誤消息并返回 2。

  • 如果 enableDevTools 參數(shù)設置為 true,則函數(shù)會啟動開發(fā)工具,并在開發(fā)工具服務器地址上向客戶端廣播 DevTools 的可用性。

  • 接下來,函數(shù)將對每個 Flutter 設備調(diào)用 initLogReader 方法以初始化日志讀取器。然后,它將調(diào)用 _initDevFS 方法來初始化開發(fā)文件系統(tǒng)(DevFS)并獲取基本 URI。如果 connectionInfoCompleter 參數(shù)不為空,則函數(shù)將使用第一個 Flutter 設備的 VM 服務地址和基本 URI 完成 DebugConnectionInfo 對象。

  • 如果在初始化 DevFS 過程中出現(xiàn)錯誤,則函數(shù)會打印錯誤消息并返回 3。

  • 接下來,函數(shù)將調(diào)用 _updateDevFS 方法來更新開發(fā)文件系統(tǒng),并將 fullRestart 參數(shù)設置為 true。如果更新失敗,則函數(shù)將返回 3。

  • 然后,函數(shù)將對每個 Flutter 設備調(diào)用 getFlutterViews 方法以獲取 Flutter 視圖,并打印連接成功的消息。

  • 如果 debuggingOptions.fastStart 參數(shù)設置為 true,則函數(shù)將調(diào)用 restart 方法以進行全面重啟,并在靜默模式下重新啟動應用程序。

  • 如果 benchmarkMode 參數(shù)設置為 true,則函數(shù)將測量性能并記錄測試結(jié)果。首先,函數(shù)將等待 10 秒鐘以確保隔離環(huán)境完全啟動。然后,函數(shù)將打印開始基準測試的消息,并調(diào)用 restart 方法以進行全面重啟。然后,函數(shù)將再次等待 10 秒鐘,以穩(wěn)定基準測試結(jié)果。接下來,函數(shù)將打印開始基準測試熱重載的消息,并調(diào)用 restart 方法以進行熱重載。如果 stayResident 參數(shù)設置為 true,則函數(shù)將等待應用程序運行完成,否則函數(shù)將清理 DevFS、停止日志記錄并退出應用程序。最后,函數(shù)將使用 toPrettyJson 函數(shù)將基準測試結(jié)果寫入文件,并返回 0。

  • 最后,函數(shù)將調(diào)用 writeVmServiceFile 方法以將 VM 服務地址寫入文件。如果 stayResident 參數(shù)設置為 true,則函數(shù)將調(diào)用 waitForAppToFinish 方法并返回其結(jié)果。否則,函數(shù)將調(diào)用 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 應用程序附加到特定設備的調(diào)試器上,以便進行調(diào)試。
  • 在這段代碼中,根據(jù)設備類型選擇不同的附加方式。例如,如果是 Fuchsia 設備,則使用 FuchsiaIsolateDiscoveryProtocol 協(xié)議來查找應用程序,如果是 iOS 設備,則使用 MDnsObservatoryDiscovery 協(xié)議查找。如果以上兩種方法都失敗,則使用 ProtocolDiscovery 協(xié)議查找。
  • 在找到應用程序的 Uri 后,該應用程序會使用運行中的 daemon 或創(chuàng)建新的 daemon 與設備進行通信。

找到uri http://127.0.0.1:55177/RXKA2jepV60=/
運行while循環(huán)接收指令:

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();
    }
  }

代碼流程如下:

  • 打印日志,開始查找已經(jīng)廣告的Dart Observatory。
  • 啟動MDNS客戶端。
  • 通過客戶端查詢指向Dart Observatory的指針記錄(PtrResourceRecord)。
  • 如果找不到指針記錄,打印日志并返回null。
  • 如果找到指針記錄,將其唯一的域名添加到集合中。
  • 如果提供了應用程序ID,則在集合中查找以該ID開頭的唯一域名。如果找不到,則拋出異常。
  • 如果未提供應用程序ID,并且集合中有多個唯一的域名,則打印建議的應用程序ID并拋出異常。
  • 如果未提供應用程序ID,并且集合中只有一個唯一的域名,則使用該唯一的域名。
  • 檢查所選域名上是否有可用端口。
  • 如果有多個服務記錄(SrvResourceRecord),則使用第一個記錄的端口。
  • 檢查所選域名上是否有身份驗證代碼(authCode)。
  • 如果沒有身份驗證代碼,則返回使用第一個服務記錄的端口和空的身份驗證代碼的MDnsObservatoryDiscoveryResult。
  • 如果有身份驗證代碼,則從TXT資源記錄中提取該代碼。
  • 如果找不到身份驗證代碼,則返回使用第一個服務記錄的端口和空的身份驗證代碼的MDnsObservatoryDiscoveryResult。
  • 如果找到了身份驗證代碼,則將其分配給MDnsObservatoryDiscoveryResult,同時確保代碼以"/"結(jié)尾。
  • 停止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;
  }
  • 檢查是否已經(jīng)啟動或正在啟動,如果是則直接返回。

  • 初始化網(wǎng)絡地址、接口工廠等參數(shù)。

  • 創(chuàng)建一個 RawDatagramSocket 對象,用于接收網(wǎng)絡數(shù)據(jù)。通過 _rawDatagramSocketFactory 方法創(chuàng)建并設置監(jiān)聽地址、端口、地址重用、端口重用等選項。

  • 將創(chuàng)建的 RawDatagramSocket 對象添加到 _sockets 列表中,如果地址為 InternetAddress.anyIPv6,則添加到 _toBeClosed 列表中。

  • 確定 mDNS 地址,如果沒有傳入 mDNS 地址,則根據(jù)監(jiān)聽地址類型選擇 IPv4 或 IPv6 的默認 mDNS 地址。

  • 獲取本地網(wǎng)絡接口列表,并對每個接口創(chuàng)建一個 RawDatagramSocket 對象,用于發(fā)送網(wǎng)絡數(shù)據(jù)。對每個接口設置監(jiān)聽地址、端口、地址重用、端口重用等選項,并添加到 _sockets 列表中。對于 IPv4 接口,使用 setRawOption 方法設置 IPv4 組播接口,對于 IPv6 接口,使用 setRawOption 方法設置 IPv6 組播接口。

_sockets.add(socket);會發(fā)現(xiàn)有3個sockets
0.0.0.0,127.0.0.1,253.53.111.111 這三個ip地址應該對應是同一個主機。

  • 對接收 RawDatagramSocket 對象調(diào)用 joinMulticast 方法,加入 mDNS 組播地址和本地網(wǎng)絡接口。
  • 對接收 RawDatagramSocket 對象調(diào)用 listen 方法,監(jiān)聽網(wǎng)絡事件并調(diào)用 _handleIncoming 方法處理網(wǎng)絡數(shù)據(jù)。
  // 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的數(shù)據(jù)回調(diào)中,可看到數(shù)據(jù)長這樣:

image.png
  • 設置 _started 標志表示已啟動,設置 _starting 標志表示正在啟動。

總的來說,在Flutter中,mdnsclient.start是啟動一個mDNS客戶端的方法,用于在本地網(wǎng)絡上發(fā)現(xiàn)可用的服務。

mDNS是一種廣泛使用的服務發(fā)現(xiàn)協(xié)議,可以通過在本地網(wǎng)絡中進行廣播和響應來發(fā)現(xiàn)可用的服務。mDNS客戶端使用查詢報文向本地網(wǎng)絡中的所有設備發(fā)送請求,以查找可用的服務。一旦某個設備響應了請求,mDNS客戶端就會接收到包含服務信息的響應報文。

mdnsclient.start方法會啟動一個mDNS客戶端,并開始向本地網(wǎng)絡中發(fā)送查詢報文。當發(fā)現(xiàn)可用的服務時,客戶端將回調(diào)一個提供服務信息的回調(diào)函數(shù),以便應用程序可以處理這些信息。通過這種方式,應用程序可以在本地網(wǎng)絡中發(fā)現(xiàn)可用的服務,并使用這些服務進行網(wǎng)絡通信。

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”,是一種網(wǎng)絡協(xié)議,用于在局域網(wǎng)上發(fā)現(xiàn)可用的設備和服務。它使用了組播IP地址(224.0.0.251)和標準的DNS編解碼格式,通過局域網(wǎng)內(nèi)廣播查詢和響應的方式來實現(xiàn)設備的發(fā)現(xiàn)和服務的注冊。mDNS協(xié)議的一個重要用途是使Apple的Bonjour技術(shù)能夠在Mac OS X和其他操作系統(tǒng)上自動發(fā)現(xiàn)可用的網(wǎng)絡服務和設備。mDNS還可以用于智能家居和物聯(lián)網(wǎng)設備之間的通信,因為它可以通過在局域網(wǎng)上廣播和接收信息來簡化設備之間的連接和交互。由于mDNS使用組播地址,因此它可以在不需要集中式服務器的情況下工作,這使得它非常適合于在家庭和小型辦公室網(wǎng)絡中使用。

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

  • 發(fā)現(xiàn)可用服務:使用以下命令可以列出網(wǎng)絡中可用的所有服務:
dns-sd -B _services._dns-sd._udp     
mdns-
  • 發(fā)現(xiàn)特定服務:使用以下命令可以列出特定服務的所有實例:
 dns-sd -B _dartobservatory
image.png
  • 查找網(wǎng)絡上運行的 Dart Observatory 實例的命令
    Dart Observatory 是一種用于調(diào)試和分析 Dart 代碼的工具,它運行在特定的端口上并提供了一系列的調(diào)試功能。在調(diào)試 Dart 應用程序時,您可以使用 Dart Observatory 監(jiān)視運行時性能指標,跟蹤內(nèi)存使用情況,以及診斷和修復其他問題。

命令中的 "-Z" 選項告訴命令行實用程序 "dns-sd"(也稱為 Bonjour)執(zhí)行零配置服務發(fā)現(xiàn),并查找名稱為 "_dartobservatory" 的服務。如果在本地網(wǎng)絡上運行了 Dart Observatory 實例,它應該會在終端中輸出有關(guān)該實例的詳細信息,例如IP地址、端口等。

dns-sd -Z _dartobservatory
image.png

最后我們可以使用在flutter attach 時添加參數(shù):

image.png

Flutter通過將更新的源代碼文件注入到正在運行的Dart 虛擬機(VM)來實現(xiàn)熱重載。在虛擬機使用新的字段和函數(shù)更新類之后, Flutter 框架會自動重新構(gòu)建 widget 樹,以便快速查看更改的效果。

而attach的過程就是一個連接VM的過程,應用以Debug模式運行后,會啟動一個VM服務,并且使用mDNS協(xié)議( mDNS/DNS-SD 是用于本地局域網(wǎng)服務發(fā)現(xiàn)的協(xié)議)廣播。執(zhí)行attach操作時,會通過mDNS協(xié)議去查找當前應用所匹配的VM服務,再通過WS協(xié)議進行連接,這個時候就可能出現(xiàn)如下幾種情況,導致連接失敗:

  1. 找到的VM服務太多,需要選擇連接哪一個。
    在flutter attach參數(shù)上添加-app-id 指定appid

  2. 調(diào)度問題,沒有去連正確的 VM 服務。
    在attach時拔掉網(wǎng)線,斷開網(wǎng)絡連接,讓mDNS找不到局域網(wǎng)內(nèi)其它設備的VM服務,每次運行前執(zhí)行 dns-sd -Z _dartobservatory._tcp 查看當前的VM服務,根據(jù)上面的設備名和端口選擇正確的app-id進行配置。

  3. mDNS緩存沒有刷新

  4. mDNS查找問題

  • 關(guān)閉個人熱點
  • Mac -> 設置 -> 網(wǎng)絡 -> iPhone USB -> 不勾選“除非需要,否則請停用”
  • 重試
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容