2023-02-24 flutter attach流程解析1

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

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相關(guān)的內(nèi)容侣灶。

Flutter是一個(gè)跨平臺(tái)的移動(dòng)應(yīng)用程序開發(fā)框架嵌言,F(xiàn)lutter attach是Flutter命令行工具提供的一個(gè)命令,用于將開發(fā)者的編輯器(如VSCode键兜、Android Studio)連接到正在運(yùn)行的Flutter應(yīng)用程序阻课,以便于進(jìn)行調(diào)試缕允。Flutter attach的原理是利用了Dart VM的一個(gè)調(diào)試協(xié)議——VM服務(wù)協(xié)議,它允許開發(fā)者以REST風(fēng)格的API與Dart VM進(jìn)行通信掂恕。

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

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

  • 啟用VM服務(wù):Dart VM支持一個(gè)VM服務(wù)懊亡,用于向外部應(yīng)用程序提供調(diào)試和診斷功能依啰。Flutter run命令會(huì)自動(dòng)啟用VM服務(wù),并監(jiān)聽一個(gè)默認(rèn)的端口號(hào)(默認(rèn)為“8181”)店枣。

  • 連接編輯器:開發(fā)者使用Flutter attach命令連接編輯器速警。Flutter attach命令會(huì)嘗試連接到運(yùn)行中的Flutter應(yīng)用程序的VM服務(wù)叹誉,連接成功后,將在編輯器中打開一個(gè)調(diào)試會(huì)話闷旧。

  • 交互調(diào)試:在編輯器中长豁,開發(fā)者可以設(shè)置斷點(diǎn)、單步執(zhí)行代碼忙灼、查看變量等匠襟,通過(guò)與Dart VM服務(wù)的交互進(jìn)行調(diào)試。

需要注意的是该园,F(xiàn)lutter attach命令要求開發(fā)者在啟動(dòng)Flutter應(yīng)用程序時(shí)啟用了VM服務(wù)酸舍。如果在啟動(dòng)應(yīng)用程序時(shí)未啟用VM服務(wù),則無(wú)法使用Flutter attach命令進(jìn)行連接里初。此外啃勉,F(xiàn)lutter attach命令還要求運(yùn)行中的Flutter應(yīng)用程序與編輯器在同一臺(tái)計(jì)算機(jī)上,或者在通過(guò)網(wǎng)絡(luò)進(jìn)行通信時(shí)双妨,必須通過(guò)安全的通道進(jìn)行連接淮阐。

另外flutter attach 命令需要 flutter 應(yīng)用程序?qū)?yīng)的源代碼,否則報(bào)錯(cuò):
Target file "lib/main.dart" not found.
因?yàn)樾枰獰嶂剌d和熱重啟時(shí)刁品,需要比對(duì)源代碼的修改枝嘶,做出文件同步,這是可以理解的哑诊。

1、attach

連接到 Flutter 應(yīng)用程序并啟動(dòng)開發(fā)工具和調(diào)試服務(wù)

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

  • 函數(shù)首先將 _didAttach 標(biāo)記設(shè)置為 true暑劝,以表示已經(jīng)連接到調(diào)試服務(wù)。然后颗搂,它通過(guò)調(diào)用 connectToServiceProtocol 函數(shù)來(lái)連接到服務(wù)協(xié)議担猛,并通過(guò)傳遞幾個(gè)服務(wù)對(duì)象來(lái)注冊(cè)服務(wù)。

  • 如果連接過(guò)程中出現(xiàn)錯(cuò)誤丢氢,則函數(shù)會(huì)打印錯(cuò)誤消息并返回 2傅联。

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

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

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

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

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

  • 如果 debuggingOptions.fastStart 參數(shù)設(shè)置為 true,則函數(shù)將調(diào)用 restart 方法以進(jìn)行全面重啟禽翼,并在靜默模式下重新啟動(dòng)應(yīng)用程序屠橄。

  • 如果 benchmarkMode 參數(shù)設(shè)置為 true,則函數(shù)將測(cè)量性能并記錄測(cè)試結(jié)果闰挡。首先锐墙,函數(shù)將等待 10 秒鐘以確保隔離環(huán)境完全啟動(dòng)。然后长酗,函數(shù)將打印開始基準(zhǔn)測(cè)試的消息溪北,并調(diào)用 restart 方法以進(jìn)行全面重啟。然后夺脾,函數(shù)將再次等待 10 秒鐘之拨,以穩(wěn)定基準(zhǔn)測(cè)試結(jié)果。接下來(lái)咧叭,函數(shù)將打印開始基準(zhǔn)測(cè)試熱重載的消息蚀乔,并調(diào)用 restart 方法以進(jìn)行熱重載。如果 stayResident 參數(shù)設(shè)置為 true菲茬,則函數(shù)將等待應(yīng)用程序運(yùn)行完成吉挣,否則函數(shù)將清理 DevFS、停止日志記錄并退出應(yīng)用程序婉弹。最后睬魂,函數(shù)將使用 toPrettyJson 函數(shù)將基準(zhǔn)測(cè)試結(jié)果寫入文件,并返回 0镀赌。

  • 最后汉买,函數(shù)將調(diào)用 writeVmServiceFile 方法以將 VM 服務(wù)地址寫入文件。如果 stayResident 參數(shù)設(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 代碼,具體功能是將一個(gè) Flutter 應(yīng)用程序附加到特定設(shè)備的調(diào)試器上舔痕,以便進(jìn)行調(diào)試评抚。
  • 在這段代碼中,根據(jù)設(shè)備類型選擇不同的附加方式伯复。例如慨代,如果是 Fuchsia 設(shè)備,則使用 FuchsiaIsolateDiscoveryProtocol 協(xié)議來(lái)查找應(yīng)用程序啸如,如果是 iOS 設(shè)備侍匙,則使用 MDnsObservatoryDiscovery 協(xié)議查找。如果以上兩種方法都失敗叮雳,則使用 ProtocolDiscovery 協(xié)議查找想暗。
  • 在找到應(yīng)用程序的 Uri 后,該應(yīng)用程序會(huì)使用運(yùn)行中的 daemon 或創(chuàng)建新的 daemon 與設(shè)備進(jìn)行通信帘不。

找到uri http://127.0.0.1:55177/RXKA2jepV60=/
運(yùn)行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寞焙。
  • 啟動(dòng)MDNS客戶端储狭。
  • 通過(guò)客戶端查詢指向Dart Observatory的指針記錄(PtrResourceRecord)。
  • 如果找不到指針記錄捣郊,打印日志并返回null辽狈。
  • 如果找到指針記錄,將其唯一的域名添加到集合中模她。
  • 如果提供了應(yīng)用程序ID,則在集合中查找以該ID開頭的唯一域名懂牧。如果找不到侈净,則拋出異常。
  • 如果未提供應(yīng)用程序ID僧凤,并且集合中有多個(gè)唯一的域名畜侦,則打印建議的應(yīng)用程序ID并拋出異常。
  • 如果未提供應(yīng)用程序ID躯保,并且集合中只有一個(gè)唯一的域名旋膳,則使用該唯一的域名。
  • 檢查所選域名上是否有可用端口途事。
  • 如果有多個(gè)服務(wù)記錄(SrvResourceRecord)验懊,則使用第一個(gè)記錄的端口擅羞。
  • 檢查所選域名上是否有身份驗(yàn)證代碼(authCode)。
  • 如果沒(méi)有身份驗(yàn)證代碼义图,則返回使用第一個(gè)服務(wù)記錄的端口和空的身份驗(yàn)證代碼的MDnsObservatoryDiscoveryResult减俏。
  • 如果有身份驗(yàn)證代碼,則從TXT資源記錄中提取該代碼碱工。
  • 如果找不到身份驗(yàn)證代碼娃承,則返回使用第一個(gè)服務(wù)記錄的端口和空的身份驗(yàn)證代碼的MDnsObservatoryDiscoveryResult。
  • 如果找到了身份驗(yàn)證代碼怕篷,則將其分配給MDnsObservatoryDiscoveryResult历筝,同時(shí)確保代碼以"/"結(jié)尾。
  • 停止MDNS客戶端廊谓。
  • 返回使用所選域名的第一個(gè)服務(wù)記錄的端口和身份驗(yàn)證代碼的MDnsObservatoryDiscoveryResult梳猪。

總的來(lái)說(shuō),每一次的attach都會(huì)啟動(dòng)一個(gè)啟動(dòng)MDNS客戶端蹂析,如果啟動(dòng)失敗舔示,則停止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;
  }
  • 檢查是否已經(jīng)啟動(dòng)或正在啟動(dòng)惕稻,如果是則直接返回。

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

  • 創(chuàng)建一個(gè) RawDatagramSocket 對(duì)象,用于接收網(wǎng)絡(luò)數(shù)據(jù)借帘。通過(guò) _rawDatagramSocketFactory 方法創(chuàng)建并設(shè)置監(jiān)聽地址蜘渣、端口、地址重用肺然、端口重用等選項(xiàng)蔫缸。

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

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

  • 獲取本地網(wǎng)絡(luò)接口列表,并對(duì)每個(gè)接口創(chuàng)建一個(gè) RawDatagramSocket 對(duì)象灾前,用于發(fā)送網(wǎng)絡(luò)數(shù)據(jù)防症。對(duì)每個(gè)接口設(shè)置監(jiān)聽地址、端口、地址重用蔫敲、端口重用等選項(xiàng)饲嗽,并添加到 _sockets 列表中。對(duì)于 IPv4 接口燕偶,使用 setRawOption 方法設(shè)置 IPv4 組播接口喝噪,對(duì)于 IPv6 接口,使用 setRawOption 方法設(shè)置 IPv6 組播接口指么。

_sockets.add(socket);會(huì)發(fā)現(xiàn)有3個(gè)sockets
0.0.0.0酝惧,127.0.0.1,253.53.111.111 這三個(gè)ip地址應(yīng)該對(duì)應(yīng)是同一個(gè)主機(jī)伯诬。

  • 對(duì)接收 RawDatagramSocket 對(duì)象調(diào)用 joinMulticast 方法晚唇,加入 mDNS 組播地址和本地網(wǎng)絡(luò)接口。
  • 對(duì)接收 RawDatagramSocket 對(duì)象調(diào)用 listen 方法盗似,監(jiān)聽網(wǎng)絡(luò)事件并調(diào)用 _handleIncoming 方法處理網(wǎng)絡(luò)數(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ù)長(zhǎng)這樣:

image.png
  • 設(shè)置 _started 標(biāo)志表示已啟動(dòng)赫舒,設(shè)置 _starting 標(biāo)志表示正在啟動(dòng)悍及。

總的來(lái)說(shuō),在Flutter中接癌,mdnsclient.start是啟動(dòng)一個(gè)mDNS客戶端的方法心赶,用于在本地網(wǎng)絡(luò)上發(fā)現(xiàn)可用的服務(wù)。

mDNS是一種廣泛使用的服務(wù)發(fā)現(xiàn)協(xié)議缺猛,可以通過(guò)在本地網(wǎng)絡(luò)中進(jìn)行廣播和響應(yīng)來(lái)發(fā)現(xiàn)可用的服務(wù)缨叫。mDNS客戶端使用查詢報(bào)文向本地網(wǎng)絡(luò)中的所有設(shè)備發(fā)送請(qǐng)求,以查找可用的服務(wù)荔燎。一旦某個(gè)設(shè)備響應(yīng)了請(qǐng)求耻姥,mDNS客戶端就會(huì)接收到包含服務(wù)信息的響應(yīng)報(bào)文。

mdnsclient.start方法會(huì)啟動(dòng)一個(gè)mDNS客戶端有咨,并開始向本地網(wǎng)絡(luò)中發(fā)送查詢報(bào)文琐簇。當(dāng)發(fā)現(xiàn)可用的服務(wù)時(shí),客戶端將回調(diào)一個(gè)提供服務(wù)信息的回調(diào)函數(shù)座享,以便應(yīng)用程序可以處理這些信息婉商。通過(guò)這種方式,應(yīng)用程序可以在本地網(wǎng)絡(luò)中發(fā)現(xiàn)可用的服務(wù)征讲,并使用這些服務(wù)進(jìn)行網(wǎng)絡(luò)通信据某。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末诗箍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子挽唉,更是在濱河造成了極大的恐慌滤祖,老刑警劉巖筷狼,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異匠童,居然都是意外死亡埂材,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門汤求,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)俏险,“玉大人,你說(shuō)我怎么就攤上這事扬绪∈溃” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵挤牛,是天一觀的道長(zhǎng)莹痢。 經(jīng)常有香客問(wèn)我,道長(zhǎng)墓赴,這世上最難降的妖魔是什么竞膳? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮诫硕,結(jié)果婚禮上坦辟,老公的妹妹穿的比我還像新娘。我一直安慰自己痘括,他們只是感情好长窄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纲菌,像睡著了一般挠日。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上翰舌,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天嚣潜,我揣著相機(jī)與錄音,去河邊找鬼椅贱。 笑死懂算,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的庇麦。 我是一名探鬼主播计技,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼山橄!你這毒婦竟也來(lái)了垮媒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎睡雇,沒(méi)想到半個(gè)月后萌衬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡它抱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年秕豫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片观蓄。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡混移,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出侮穿,到底是詐尸還是另有隱情沫屡,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布撮珠,位于F島的核電站沮脖,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏芯急。R本人自食惡果不足惜勺届,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娶耍。 院中可真熱鬧免姿,春花似錦、人聲如沸榕酒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)想鹰。三九已至紊婉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間辑舷,已是汗流浹背喻犁。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留何缓,地道東北人肢础。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像碌廓,于是被迫代替她去往敵國(guó)和親传轰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容