Zurück zum Blog
Transport19 min read

Wi-Fi Aware-Transport-Design — Neighbor Discovery & Data Paths

Dieser Artikel erklärt, wie PlainApp Wi-Fi Aware (NAN — Neighbor Awareness Networking) als mittlere Ebene seiner Peer-Transport-Fallback-Kette verwendet, zwischen LAN (gleiches Subnetz, HTTPS) und BLE (GATT-RPC als letzter Ausweg). Wi-Fi Aware ermöglicht es zwei PlainApp-Geräten, zu kommunizieren, wenn sie sich in verschiedenen SSIDs, Guest- vs. IoT-VLANs oder ganz ohne Wi-Fi-Infrastruktur befinden — ohne jemals eine IP-Adresse von einem DHCP-Server zu benötigen.

Der Artikel behandelt den Android-only Aware-Session-Lebenszyklus, das Publish-/Subscribe-Discovery-Modell, den zweiphasigen Role-Split-Handshake, der requestNetwork auf beiden Seiten innerhalb des ~500-ms-Fensters des Frameworks synchronisiert, den Per-Peer-Link-Pool mit Idle-Sweeping, den IPv6- + Custom-DNS-Trick, der einem einzigen OkHttp-Client erlaubt, sowohl LAN als auch Aware zu bedienen, und den Prewarmer, der den Aware-Start des Peers über BLE triggert.

Für die übergeordnete Fallback-Kette siehe Chat-Architektur. Für den BLE-Transport, der übernimmt, wenn Aware unavailable ist, siehe BLE-Transport. Wie der gemeinsame ChaCha20-Schlüssel, der als Aware-PMK wiederverwendet wird, etabliert wird, siehe Pairing-Ablauf.

Inhaltsverzeichnis

Warum Wi-Fi Aware? {#why-wi-fi-aware}

Wi-Fi Aware (IEEE 802.11bc, früher NAN — Neighbor Awareness Networking) ist eine Zertifizierung der Wi-Fi Alliance, die es zwei Geräten erlaubt, sich gegenseitig zu entdecken und Daten auszutauschen ohne jegliche Wi-Fi-Infrastruktur — kein AP, kein Router, kein DHCP. PlainApp verwendet es für zwei Szenarien, die LAN nicht abdecken kann:

  • Unterschiedliche SSIDs / VLANs. Ein Telefon im Guest-Netzwerk und ein Laptop im IoT-VLAN sind beide per Wi-Fi „online", können aber einander nicht per IP erreichen. Aware erzeugt einen direkten Device-to-Device-Data-Path, der die Infrastruktur komplett umgeht.
  • Gar keine Infrastruktur. Zwei Geräte in der Wildnis mit eingeschaltetem Wi-Fi, aber ohne AP, können trotzdem chatten. (BLE deckt das auch ab, aber Aware ist deutlich schneller — ~10 ms Round Trips vs. Sekunden, und MB/s vs. Zehner-KB/s.)

Diagram 1
1

Plattform-Beschränkungen

Wi-Fi Aware ist in PlainApp nur auf Android verfügbar:

  • Android 13 (API 33) ist das Minimum — die WifiAwareNetworkSpecifier.Builder-Overloads mit setPort() und setPmk(), auf die PlainApp angewiesen ist, erfordern isTPlus().
  • iOS exponiert Wi-Fi Aware nicht an Dritt-Apps. iOS-PlainApp fällt direkt von LAN auf BLE; das WifiAwareTransport-Objekt wird nicht einmal ins iOS-Target kompiliert (@RequiresApi(Build.VERSION_CODES.S) + androidMain-Source-Set).

Deshalb ruft PeerTransportRouter.buildListcreateWifiAwareTransport() auf — eine Factory, die auf iOS null zurückgibt.

Wo Aware in der Fallback-Kette sitzt {#where-aware-sits-in-the-fallback-chain}

PlainApps PeerTransportRouter ist eine geordnete Liste. Für jeden send- oder downloadFile-Aufruf durchläuft er die Liste und versucht jeden Transport, bis einer erfolgreich ist; Fehler kaskadieren nach unten.

Diagram 2
2

Warum ist Aware „die Mitte" und nicht „der Erste"?

Weil LAN fast immer schneller ist, wenn verfügbar. Ein Same-Subnet-Wi-Fi- Hop über einen AP ist ein einzelner 802.11-Frame-Austausch; ein Aware-Data-Path addiert ein NDP-Setup (~5 s bei Erstnutzung) plus einen zweiten Wi-Fi-Radio-Kontext für den Device-to-Device-Link. Wenn beide erreichbar sind, gewinnt LAN auf Latenz und Durchsatz.

Umgekehrt ist BLE immer langsamer — aber es funktioniert, sobald beide Geräte gepaart sind. Aware liegt in der Mitte: schneller als BLE, langsamer als LAN, und nur auf Android-13+-Geräten mit eingeschaltetem Wi-Fi verfügbar.

Session-Lebenszyklus: Attach → Publish + Subscribe {#session-lifecycle-attach--publish--subscribe}

Eine Wi-Fi Aware-Session ist prozessweit. Es gibt genau eine WifiAwareSession pro Gerät; darin betreibt PlainApp eine Publish-Session (sodass Peers uns entdecken können) und eine Subscribe-Session (sodass wir Peers entdecken können). Beide werden gestartet, sobald AwareSession.start() den attach-Callback abschließt.

Diagram 3
3

Warum Publish UND Subscribe auf demselben Gerät?

Das Wi-Fi Aware-Discovery-Modell ist asymmetrisch: ein Publisher advertised einen Service, ein Subscriber scannt danach. Um Discovery symmetrisch zu machen (beide Geräte entdecken einander), macht PlainApp beides gleichzeitig. Ohne das müsste Gerät A im Voraus wissen, ob es für einen gegebenen Peer der Publisher oder der Subscriber ist — aber Peer-Rollen werden später per clientId-Vergleich bestimmt (siehe Discovery & Rollenzuweisung).

Gleichzeitiges Publishen und Subscriben bedeutet, dass jedes Gerät das onServiceDiscovered des jeweils anderen (als Subscriber) UND die Hello-Nachrichten des jeweils anderen (als Publisher) empfängt — beide Richtungen des Handshakes sind immer verfügbar.

Auto-Restart bei Termination

Einige Android-Varianten (insbesondere MIUI) beenden lange laufende Aware-Sessionen, um Akku zu sparen. PlainApp behandelt das in den onSessionTerminated-Callbacks: Er nullt die beendete Session und ruft publishOwnService / subscribeOwnService sofort wieder auf der noch angehängten WifiAwareSession auf. Die Attach-Session selbst geht nicht verloren — nur die Publish/Subscribe-Discovery-Session. Peer-Handles von vor der Termination werden stale, weshalb awaitPeerHandle den discoveredAt-Zeitstempel prüft und Handles verwirft, die älter als 30 s sind.

Discovery & Rollenzuweisung {#discovery--role-assignment}

Das Wi-Fi Aware-Data-Path-Protokoll verlangt, dass eine Seite als Publisher (Server) und die andere als Subscriber (Client) agiert. Beide Seiten können nicht gleichzeitig der Initiator sein — das Framework lehnt Anfragen ohne passendes Gegenstück ab.

PlainApp weist Rollen deterministisch pro Peer zu, über einen einfachen lexikografischen Vergleich von clientIds:

Diagram 4
4

Warum deterministisch und nicht verhandelt?

Ein verhandelter Ansatz (z.B. „niedrigere MAC ist der Server") würde einen zusätzlichen Nachrichtenaustausch erfordern. Der lexikografische Vergleich ist idempotent, symmetrisch und zustandslos: Beide Geräte berechnen dieselbe Rolle für dasselbe Paar ohne jegliche Kommunikation. Die clientId ist eine 13-Zeichen-kurze UUID, sodass Gleichstände (clientId == peer.id) nur beim Vergleich eines Peers mit sich selbst auftreten — was nie die Transportebene erreicht.

Die Rolle bestimmt zwei Dinge flussabwärts:

  1. Wer die Retry-Schleife treibt. Nur der Client retried requestNetwork; der Server macht genau einen Versuch pro empfangenem Hello. Das ist kritisch für das 500-ms-Fenster (nächster Abschnitt).
  2. Wer den Port setzt. Der Publisher ruft setPort(httpsPort) auf, weil er derjenige ist, der eingehende Verbindungen auf seinem HTTPS-Server-Port akzeptiert. Der Subscriber setzt keinen Port — er erfährt den Port des Peers aus den WifiAwareNetworkInfo, nachdem der Data Path etabliert ist.

Der Zweiphasen-Handshake (hello + ready) {#the-two-phase-handshake-hello--ready}

Der schwierigste Teil des Wi-Fi Aware-Data-Path-Setups ist das Timing. Das Framework verlangt, dass beide Seiten connectivityManager.requestNetwork innerhalb von etwa 500 ms nacheinander aufrufen — ruft eine Seite es auf, bevor die andere ihre passende Anfrage registriert hat, lehnt das Framework es sofort mit onUnavailable („releaseRequestAsUnfulfillableByAnyFactory") ab.

PlainApp löst das mit einem Zwei-Nachrichten-Application-Layer-Handshake, der auf dem Aware-L2-Message-Channel läuft (derselbe sendMessage-API, der auch von onServiceDiscovered verwendet wird):

Diagram 5
5

Warum zwei Nachrichten (hello + ready) statt nur einer?

Das Hello allein reicht wegen der Richtungsasymmetrie nicht aus. Der Subscriber kann Hello in dem Moment senden, in dem er den Publisher entdeckt (in onServiceDiscovered), aber der Publisher kann requestNetwork nicht starten, bevor er das PeerHandle des Subscribers hat, das er erst durch Empfang des Hellos erfährt. Das Hello erfüllt also zwei Zwecke:

  1. Dem Publisher das PeerHandle des Subscribers liefern. Der Publisher braucht es, um den WifiAwareNetworkSpecifier zu bauen.
  2. Verbindungswillen signalisieren. Der Empfang des Hellos teilt dem Publisher mit: „Der Subscriber wird gleich requestNetwork aufrufen, das sollte ich auch tun."

Die ready-Quittung existiert für die entgegengesetzte Richtung — um dem Subscriber mitzuteilen: „Der Publisher hat sein requestNetwork registriert." Ohne sie könnte das requestNetwork des Subscribers dem des Publishers zuvorkommen und vom Framework abgelehnt werden. Die ready-Quittung ist ein nicht-blockierendes Signal: Der Subscriber wartet nicht darauf, bevor er requestNetwork aufruft (das würde einen Round-Trip addieren), aber wenn sie ankommt, während der Subscriber im IDLE-Zustand ist (zwischen Retry-Versuchen), kann der Subscriber sofort retrien, ohne auf die RETRY_DELAY_MS-Lücke zu warten.

Die Retry-Schleifen-Asymmetrie

Das ist der subtilste Teil des Designs. Nur der Subscriber retried. Der Publisher macht genau einen requestNetwork-Versuch pro empfangenem Hello. Das liegt daran, dass:

  • Wenn beide Seiten unabhängig retrien würden, würden ihre Retry-Zyklen außer Phase driften (unterschiedliche delay()-Dauern, unterschiedliche GC-Pausen), und die beiden requestNetwork-Aufrufe würden selten innerhalb des 500-ms-Fensters überlappen.
  • Die Retry-Schleife des Subscribers sendet bei jedem Versuch ein frisches Hello, was den buildLink des Publishers über publishHelloListeners erneut triggert. Das garantiert, dass das requestNetwork des Publishers dem Hello immer um ~50 ms folgt, wohl innerhalb des 500-ms-Fensters.

Das ist detailliert in AwarePeerLink.build dokumentiert.

NDP requestNetwork — das 500-ms-Fenster {#ndp-requestnetwork--the-500-ms-window}

Der requestNetwork-Aufruf ist die timing-kritischste Operation im Aware-Transport. Hier ist, was auf jeder Seite passiert:

Diagram 6
6

Was der onUnavailable-Callback bedeutet

onUnavailable feuert, wenn das Framework das requestNetwork ablehnt, bevor es eine passende Peer-Anfrage findet. Das PeerHandle selbst ist weiterhin gültig — nur das NDP (Neighbor Discovery Protocol)-Pairing ist fehlgeschlagen, weil die andere Seite noch nicht registriert war. PlainApp ruft in diesem Fall bewusst nicht session.invalidatePeerHandle auf, da das Invalidieren des Handles das einzige Signal verwerfen würde, dass onServiceDiscovered jemals aufgerufen wurde (es feuert einmal pro Peer pro Subscribe-Session-Lebenszeit). Mit erhaltenem Handle kann der Retry es wiederverwenden, statt auf eine frische Discovery zu warten.

Dasselbe gilt für das publisher-seitige Handle aus onMessageReceived — der Publisher behält den publishPeerHandles[fromCid]-Eintrag über fehlgeschlagene Versuche hinweg, sodass das nächste Hello des Subscribers das gecachte Handle wiederverwendet, statt verworfen zu werden.

Jeder gepaarte Peer erhält sein eigenes AwarePeerLink-Objekt, das der prozessweite AwareLinkPool besitzt. Der Pool behandelt Discovery-Events, Link-Wiederverwendung und Idle-Eviction.

Diagram 7
7

Warum kein Auto-Build bei Discovery?

Der Pool baut explizit keinen Link, wenn onServiceDiscovered feuert. Das ist eine kritische Entscheidung: Ein belebtes Café könnte 100 PlainApp-Geräte haben, die alle den „plain-peer"-Service publishen. Würde jede Discovery ein requestNetwork triggern, wäre das Framework mit NDP-Setup-Versuchen überflutet und das Wi-Fi-Radio saturiert.

Stattdessen zeichnet der Pool nur das PeerHandle auf und wartet auf eines von:

  1. Der lokale Nutzer sendet eine NachrichtWifiAwareTransport.sendpool.buildLink(peer) (Sender-seitiger Trigger).
  2. Der Remote-Peer sendet ein HelloonPublishHelloReceivedbuildLink(peer) (Empfänger-seitiger Trigger).
  3. Der Remote-Peer sendet ein ReadyonSubscribeReadyReceivedbuildLink(peer) (Empfänger-seitiger Trigger).

So werden Links nur für Peers gebaut, mit denen der Nutzer tatsächlich Nachrichten austauscht — nicht für jedes PlainApp-Gerät in Funkreichweite.

Idle-Sweep

Alle 10 Sekunden durchläuft der Pool alle Links und schließt jene, deren lastActiveAt älter als 60 Sekunden ist. Jeder send- und downloadFile-Aufruf ruft link.touch() auf, um den Zeitstempel aufzufrischen. Das gibt den Wi-Fi-Radio-Kontext und den OkHttp-Connection-Pool für Peers frei, mit denen der Nutzer aufgehört hat zu chatten — wichtig, weil Android die Anzahl simultaner Aware-Data-Paths auf etwa 4–10 limitiert (geräteabhängig).

IPv6-Adressierung & der plain-aware-peer-DNS-Trick {#ipv6-addressing--the-plain-aware-peer-dns-trick}

Wi-Fi Aware-Data-Paths verwenden nur Link-Local-IPv6. Es gibt kein IPv4, keinen DNS-Server, kein DHCP. Die IPv6-Adresse des Peers wird über das WifiAwareNetworkInfo.peerIpv6Addr-Feld in onCapabilitiesChanged geliefert — eine fe80::...-Adresse, die nur auf dem Aware-Netzwerkinterface Bedeutung hat.

PlainApp muss HTTPS-Anfragen an diese Adresse senden, aber OkHttps https://-URL-Parsing lehnt rohe IPv6-Literale in einem Hostnamen ab (https://[fe80::abcd]:8443/ funktioniert, aber das Routing über einen Custom-Dns-Resolver ist sauberer). Der Trick:

Diagram 8
8

Warum ein Sentinel-Hostname?

Die Alternative — das IPv6-Literal direkt in der URL übergeben — würde erfordern, dass jede Aufrufstelle die Link-Local-Adresse kennt. Durch die Verwendung eines Sentinel-Hostnamens ist die URL-Konstruktion für LAN und Aware identisch: Beide produzieren eine gültige https://<host>:<port>/peer_graphql-URL, die OkHttp parsen kann. Der einzige Unterschied ist die an den Client gebundene Dns-Implementierung — LAN verwendet das System-DNS, Aware verwendet awareDns(peerIpv6), das die gecachte Link-Local-Adresse für den Sentinel-Hostnamen zurückgibt und für alles andere auf Dns.SYSTEM durchfällt.

Warum network.socketFactory?

Androids Network-Objekt repräsentiert ein spezifisches Netzwerkinterface (in diesem Fall der Aware-Data-Path). Indem wir network.socketFactory aufrufen und es an OkHttps socketFactory-Config übergeben, erzwingen wir, dass alle TCP-Sockets auf dem Aware-Interface erzeugt werden — nicht auf dem Standard-Wi-Fi- oder Cellular-Interface. Ohne das würde das OS die Anfrage über das Default-Netzwerk routen, wo das Link-Local-IPv6 unreachable ist, und die Anfrage würde mit ENETUNREACH fehlschlagen.

Kryptografie: PMK-Derivation & ChaCha20-Wiederverwendung {#cryptography-pmk-derivation--chacha20-reuse}

Wi-Fi Aware unterstützt einen optionalen PMK (Pairwise Master Key) für den Data Path. Wenn gesetzt, ist der L2-Link selbst mit diesem PMK verschlüsselt — das Wi-Fi-Radio übernimmt die Verschlüsselung, keine Application-Layer-Crypto nötig.

PlainApp leitet den PMK aus demselben ChaCha20-gemeinsamen Schlüssel ab, den LanTransport und BleTransport für die Application-Layer-Verschlüsselung verwenden:

Diagram 9
9

Warum auf 32 Bytes abschneiden?

Der Wi-Fi Aware-PMK muss exakt 32 Bytes (256 Bits) betragen. Der ChaCha20-gemeinsame Schlüssel aus dem Pairing ist im Normalfall ebenfalls 32 Bytes, also ist der raw.size == 32-Zweig der Common Path. Der Truncation-/Padding-Fallback behandelt den (theoretischen) Fall, dass der Schlüssel kürzer gespeichert wurde — Padding mit Nullen auf 32 Bytes ist eine defensive Maßnahme, die in der Praxis bei korrekt gepaarten Peers nicht vorkommt.

Der signierte Umschlag ist identisch zu LAN

Weil createCryptoHttpClient dieselbe Factory ist, die auch von LanTransport verwendet wird, ist die L7-Crypto auf Aware byte-für-byte identisch zu LAN. Das serverseitige PeerGraphQLService weiß nicht (oder interessiert sich nicht dafür), welcher Transport die Anfrage geliefert hat — es sieht einfach eine signierte, verschlüsselte GraphQL-Nutzlast und entschlüsselt sie mit dem gemeinsamen Schlüssel des Peers. Das ist das „eine Codebasis, viele Transporte"-Prinzip, das in Chat-Architektur dokumentiert ist.

Sende-Pfad für Nachrichten (End-to-End) {#message-send-path-end-to-end}

Alles zusammengeführt — was passiert, wenn eine Chat-Nachricht über Wi-Fi Aware gesendet wird:

Diagram 10
10

Beachtenswerte Designentscheidungen

  • Verbindungswiederverwendung. Anders als BleTransport, das die GATT-Verbindung nach jeder Anfrage abbaut, verwendet WifiAwareTransport den Aware-Data-Path für so viele Anfragen weiter, wie der Nutzer innerhalb des 60-s-Idle-Fensters macht. Die erste Anfrage zahlt den ~400-ms-Handshake; nachfolgende Anfragen sind ~10-ms-Round-Trips.
  • Dieselbe Crypto wie LAN. Der ChaCha20-Interceptor und der signierte Umschlag sind byte-identisch zu LAN. Das PeerGraphQLService des Peers weiß nicht, welcher Transport die Anfrage geliefert hat.
  • Keine Preemption bei Link-Versagen. Schlägt buildLink fehl, wirft der Transport TransportUnavailable und der Router fällt auf BLE durch. Es gibt kein Retry innerhalb von sendAwarePeerLink.build macht bereits seine eigene interne Retry-Schleife (mit MAX_BUILD_ATTEMPTS = 1 auf dem Client, mehr, wenn der Prewarmer beide Seiten geprimed hat).

Download-Pfad für Dateien (End-to-End) {#file-download-path-end-to-end}

Datei-Downloads über Aware wiederverwenden denselben Data-Path wie Chat-Nachrichten, verwenden aber einen separaten OkHttp-Client, der für das Streamen großer Dateien konfiguriert ist:

Diagram 11
11

Warum ein separater Client für Downloads?

Der Chat-Client (AwareHttpClientFactory.build) hat ein 30-s-requestTimeoutMillis — angemessen für GraphQL-Mutationen, aber katastrophal für einen 100-MB- Datei-Download. Der Download-Client (buildFileDownload) setzt:

  • connectTimeoutMillis = 10_000 (länger als Chats 5 s, tolerant gegenüber langsamem First-Packet auf einem frischen Data Path)
  • readTimeout = 120 s pro Read (vs. der implizite Default von 10 s)
  • requestTimeoutMillis = 120_000 (2 Minuten — genug für die meisten Dateien)
  • retryOnConnectionFailure(true) — ein abgebrochener Mid-Download-Read wird retried, statt die ganze Übertragung fehlschlagen zu lassen

Er lässt auch den ChaCha20-Interceptor weg. Der /fs-Endpunkt dient rohe Datei-Bytes (keinen signierten GraphQL-Umschlag), und der L2-PMK (wenn vorhanden) verschlüsselt bereits den Funklink. Ein 50-MB-Video zusätzlich mit ChaCha20 in Software zu verschlüsseln, würde CPU verschwenden und die Übertragung verlangsamen.

Streaming, nicht Puffern

Wie der BLE-Pfad streamen Aware-Downloads die Datei durch einen ByteReadChannel — die Datei wird beim Eintreffen der Bytes in eine Temp-Datei geschrieben, nicht im Speicher gepuffert. PeerFileDownloader liest 8-KB-Chunks und emittet jede Sekunde Progress-Events. Dieselbe DownloadedResponse- / PeerFileDownloader- / DownloadQueue-Pipeline wird über alle Transporte wiederverwendet — transport-spezifisch ist nur die channel-Quelle.

Prewarming: BLE-getriggerter Aware-Start {#prewarming-ble-triggered-aware-startup}

Die größte nutzersichtbare Latenz im Aware-Pfad ist der erste Handshake — wenn beide Seiten Aware noch nicht gestartet haben, muss die erste Nachricht des Nutzers warten auf:

  1. Lokale Aware-Session-Attach (~1 s)
  2. Lokaler Publish + Subscribe-Start (~1 s)
  3. Aware-Startup des Remote-Peers (~2 s über BLE)
  4. Gegenseitige Discovery (~1 s)
  5. NDP-Handshake (~400 ms)

Das sind ~5 Sekunden, bevor das erste Byte gesendet wird. Um diese Latenz zu verbergen, läuft PeerTransportPrewarmer beim Betreten der ChatPage und triggert den Aware-Start des Remote-Peers über BLE:

Diagram 12
12

Die Doppelrolle von BLE

BLE erfüllt hier zwei Zwecke:

  1. Den aktuellen Aware-Status des Peers lesen (billig, kein GATT-Connect — das serviceData-Byte0 des Scan-Responses trägt die Aware-Flags).
  2. Den Peer triggern, Aware zu starten, wenn er es unterstützt, aber aktuell nicht läuft. Das geht über den regulären BleTransport.send-Pfad — eine startAware-GraphQL-Mutation, verschlüsselt mit dem gemeinsamen ChaCha20-Schlüssel, geliefert via GATT-RPC an den /peer_graphql-Endpunkt des Peers.

Das ist eine der wenigen Stellen, an denen die Transporte kooperieren statt nur fallbacken: BLE wird verwendet, um die Session präventiv auf den schnelleren Aware-Transport zu upgraden, bevor der Nutzer es überhaupt merkt.

Warum optimistisches setAwareRunning(true)?

Die startAware-Mutation gibt Success zurück, sobald der Resolver des Remote-Peers WifiAwareTransport.start() aufruft — aber die Aware-Session ist noch nicht tatsächlich attached (onAttached feuert asynchron). PlainApp markiert den Peer optimistisch als awareRunning = true, weil:

  • Wenn sie tatsächlich startete, wird der nächste send Aware verwenden (schnell).
  • Wenn nicht (z.B. Wi-Fi des Peers ist aus), wird buildLink des nächsten send mit TransportUnavailable fehlschlagen und natürlich auf BLE zurückfallen.
  • Die Kosten eines False Positive sind ein ~5-s-Timeout, kein dauerhafter Block — PeerCircuitBreaker zeichnet den Fehler auf, öffnet aber nicht das BLE-Leg (BLE öffnet nur bei eigenen Fehlern).

Warum auf 30 s drosseln?

PeerTransportPrewarmer.prewarm(peerId) zeichnet einen Zeitstempel pro Peer auf und verweigert einen Re-Run innerhalb von 30 s. Das liegt daran, dass der Nutzer häufig zwischen Chat-Liste und Chat-Seite hin- und hernavigiert — ohne Drosselung würde jede Navigation einen BLE-Scan + startAware-Mutation triggern, Akku entladen und das BLE-Radio spammen. Das 30-s-Fenster ist kurz genug, um einen Peer zu erwischen, der gerade online kam (z.B. Nutzer hat die App auf dem Remote-Gerät geöffnet), aber lang genug, um spurious Re-Runs zu vermeiden.

Fehlermodi & das Fast-Skip-Flag {#failure-modes--the-fast-skip-flag}

Aware hat mehr Fehlermodi als jeder andere Transport. Das isAwareRunning-Fast-Skip-Flag ist die wichtigste Einzeloptimierung im ganzen Modul — ohne es würde jeder send 10 s mit buildLink-Timeout verschwenden, bevor auf BLE gefallen wird.

Diagram 13
13

Das isAwareRunning-Flag ist der Dreh- und Angelpunkt

Ohne diesen einzigen Boolean würde jeder Aware-send entweder:

  • Immer buildLink versuchen → 10-s-Timeout bei jedem Send an einen Peer, dessen Aware nicht läuft.
  • Immer Aware überspringen → es nie verwenden, selbst wenn beide Seiten es laufen haben.

Das Flag wird aus zwei Quellen aufgefrischt, in der Reihenfolge der Autorität:

  1. BLE-Scan-Response (billig, kein GATT-Connect) — gesetzt durch PeerTransportPrewarmer.refreshAwareFlagFromScan. Der Peer advertised seinen Aware-Status in der 9-Byte-serviceData-Nutzlast (Byte0-Bitfield).
  2. GATT-DISCOVER-Antwort (autoritativ) — gesetzt durch PairingTransport.scanAndDiscover, wenn eine volle Discovery stattfindet. Das überschreibt den Scan-Hint.

Wenn false, werfen WifiAwareTransport.send und downloadFilesofort TransportUnavailable — kein Scan, kein Handshake, kein Timeout. Der Router fällt in Mikrosekunden auf BLE durch.

Referenz der Schlüsselkonstanten {#key-constants-reference}

KonstanteWertWoZweck
AwareSession.SERVICE_NAME"plain-peer"DiscoveryService-Name, das von jedem PlainApp-Gerät published & subscribed wird
AwareSession.PEER_HANDLE_MAX_AGE_MS30 000PeerHandle-CacheStale Handles verwerfen (Publish-Session des Peers könnte neu gestartet sein)
AwareSession.READY_TIMEOUT_MS15 000HandshakeSubscriber-Wartezeit auf ready-Quittung des Publishers
AwareSession.MSG_HELLO0HandshakeSubscriber → Publisher-Nachrichten-ID
AwareSession.MSG_READY1HandshakePublisher → Subscriber-Nachrichten-ID
AwarePeerLink.MAX_BUILD_ATTEMPTS1Handshake (nur Client)Einzelner Versuch — war 3, jetzt 1, weil Prewarmer beide Seiten primed
AwarePeerLink.ATTEMPT_TIMEOUT_MS5 000HandshakePer-Versuch-Timeout — war 10 s, halbiert, um Fallback zu beschleunigen
AwarePeerLink.RETRY_DELAY_MS500HandshakeVerzögerung zwischen Retry-Versuchen (nur Client)
AwarePeerLink.REQUEST_TIMEOUT_MS30 000NDPconnectivityManager.requestNetwork-Timeout
AwareLinkPool.IDLE_TIMEOUT_MS60 000Pool-SweepIdle-Links nach 60 s Inaktivität schließen
AwareLinkPool.IDLE_SWEEP_INTERVAL_MS10 000Pool-SweepSweep-Intervall
AwareHttpClientFactory.AWARE_HOST"plain-aware-peer"DNSSentinel-Hostname, der durch Custom-Dns zur Peer-IPv6 aufgelöst wird
build-Chat-ClientconnectTimeout 5 s, requestTimeout 30 s, ChaCha20-Interceptor
buildFileDownloadconnectTimeout 10 s, readTimeout 120 s, requestTimeout 120 s, keine Crypto
PeerTransportPrewarmer.PREWARM_TTL_MS30 000PrewarmThrottle pro Peer
PeerTransportPrewarmer.BLE_SCAN_TIMEOUT_MS15 000PrewarmBLE-Scan-Timeout für refreshAwareFlagFromScan
PeerCircuitBreaker.WINDOW_MS30 000Circuit-BreakerOpen-Dauer nach Schwellwert
PeerCircuitBreaker.MAX_FAILURES2Circuit-BreakerFehler im Fenster bis Open
TempData.httpsPort8443 (Default)ServerPort des Publishers, advertised via WifiAwareNetworkSpecifier.setPort
BleServiceData.AWARE_SUPPORTED0x01BLE-Scan-ResponseBit, das anzeigt, dass der Peer Wi-Fi Aware unterstützt
BleServiceData.AWARE_RUNNING0x02BLE-Scan-ResponseBit, das anzeigt, dass der Aware-Service des Peers aktuell läuft

Zusammenfassung der Design-Trade-offs {#design-trade-offs-recap}

Diagram 14
14

Weiterführende Literatur

  • Chat-Architektur — wie WifiAwareTransport in die LAN → Aware → BLE-Fallback-Kette und die übergeordnete Chat-Send/Empfangs-Pipeline passt.
  • BLE-Transport — der Transport der letzten Wahl, der übernimmt, wenn Aware unavailable ist; außerdem der Channel, den der Prewarmer nutzt, um den Aware-Start beim Remote-Peer zu triggern.
  • Pairing-Ablauf — wie der gemeinsame ChaCha20-Schlüssel, der als Aware-PMK wiederverwendet wird, etabliert wird, und wie die BLE-Scan-Response-Flags (AWARE_SUPPORTED / AWARE_RUNNING) bevölkert werden.