블로그로 돌아가기
Transport19 min read

Wi-Fi Aware 트랜스포트 설계 — 이웃 발견과 데이터 경로

이 글에서는 PlainApp이 피어 트랜스포트 폴백 체인의 중간 계층으로 Wi-Fi Aware(NAN — Neighbor Awareness Networking)를 사용하는 방법을 설명합니다. LAN(동일 서브넷 HTTPS)과 BLE(최후 수단 GATT RPC) 사이에 위치합니다. Wi-Fi Aware는 두 PlainApp 기기가 다른 SSID, 게스트 대 IoT VLAN, 또는 Wi-Fi 인프라가 전혀 없을 때 — DHCP 서버로부터 IP 주소가 전혀 필요 없이 — 통신하게 만드는 핵심 기술입니다.

이 글은 Android 전용 Aware 세션 라이프사이클, 발행(publish) / 구독(subscribe) 발견 모델, 프레임워크의 ~500ms 윈도우 내에 양쪽의 requestNetwork를 동기화하는 2단계 역할 분할 핸드셰이크, 유휴 스위핑을 갖춘 피어별 링크 풀, 단일 OkHttp 클라이언트가 LAN과 Aware 모두에 서비스하게 하는 IPv6 + 커스텀 DNS 트릭, 그리고 BLE를 통해 피어 Aware 시작을 트리거하는 prewarmer를 다룹니다.

더 넓은 폴백 체인은 Chat Architecture를 참조하십시오. Aware를 사용할 수 없을 때 인계하는 BLE 트랜스포트는 BLE Transport를 참조하십시오. Aware PMK로 재사용되는 공유 ChaCha20 키가 어떻게 설정되는지는 Pairing Flow를 참조하십시오.

목차

Wi-Fi Aware인 이유? {#why-wi-fi-aware}

Wi-Fi Aware(IEEE 802.11bc, 이전 NAN — Neighbor Awareness Networking)는 Wi-Fi Alliance 인증으로, 두 기기가 어떤 Wi-Fi 인프라 없이도 서로를 발견하고 데이터를 교환할 수 있게 합니다 — AP 없음, 라우터 없음, DHCP 없음. PlainApp은 LAN이 커버할 수 없는 두 시나리오에 이를 사용합니다:

  • 다른 SSID / VLAN. 게스트 네트워크의 폰과 IoT VLAN의 노트북은 모두 Wi-Fi를 통해 "온라인" 상태지만 서로의 IP에 도달할 수 없습니다. Aware는 인프라를 완전히 우회하는 직접 기기 간 데이터 경로를 만듭니다.
  • 인프라가 전혀 없는 경우. Wi-Fi는 켜져 있으나 AP가 없는 야외의 두 기기도 여전히 채팅할 수 있습니다. (BLE도 이를 커버하지만, Aware가 훨씬 빠릅니다 — 수 초 대 ~10ms 왕복, 수십 KB/s 대 MB/s.)

Diagram 1
1

플랫폼 제약

Wi-Fi Aware는 PlainApp에서 Android 전용입니다:

  • Android 13(API 33)이 최소입니다 — PlainApp이 의존하는 setPort()와 setPmk() 오버로드를 갖는 WifiAwareNetworkSpecifier.Builder는 isTPlus()가 필요합니다.
  • iOS는 서드파티 앱에 Wi-Fi Aware를 노출하지 않습니다. iOS PlainApp은 LAN에서 BLE로 곧바로 폴백됩니다; WifiAwareTransport 객체는 iOS 타겟에 컴파일조차 되지 않습니다(@RequiresApi(Build.VERSION_CODES.S) + androidMain 소스 세트).

이것이 PeerTransportRouter.buildList가 createWifiAwareTransport()를 호출하는 이유입니다 — iOS에서 null을 반환하는 팩토리입니다.

폴백 체인에서 Aware의 위치 {#where-aware-sits-in-the-fallback-chain}

PlainApp의 PeerTransportRouter는 순서화된 목록입니다. 각 send 또는 downloadFile 호출마다 목록을 순회하며 각 트랜스포트를 성공할 때까지 시도합니다; 실패는 아래로 전파됩니다.

Diagram 2
2

Aware가 "중간"이지 "첫 번째"가 아닌 이유?

LAN은 사용 가능할 때 거의 항상 더 빠르기 때문입니다. AP를 통한 동일 서브넷 Wi-Fi 홉은 단일 802.11 프레임 교환입니다; Aware 데이터 경로는 NDP 설정(첫 사용 시 ~5초)에 기기 간 링크를 위한 두 번째 Wi-Fi 라디오 컨텍스트를 더합니다. 양쪽 모두 도달 가능하면, LAN이 지연과 처리량에서 이깁니다.

반대로 BLE는 항상 더 느립니다 — 하지만 두 기기가 페어링된 경우 언제나 작동합니다. Aware는 중간에 있습니다: BLE보다 빠르고, LAN보다 느리며, Wi-Fi가 켜진 Android 13+ 기기에서만 사용 가능합니다.

세션 라이프사이클: Attach → Publish + Subscribe {#session-lifecycle-attach--publish--subscribe}

Wi-Fi Aware 세션은 프로세스 전체입니다. 기기당 정확히 하나의 WifiAwareSession이 있습니다; 그 안에서 PlainApp은 하나의 발행 세션(피어가 우리를 발견하게)과 하나의 구독 세션(우리가 피어를 발견하게)을 실행합니다. 둘 다 AwareSession.start()가 attach 콜백을 완료하는 순간 시작됩니다.

Diagram 3
3

동일 기기에서 발행과 구독을 모두 하는 이유?

Wi-Fi Aware 발견 모델은 비대칭입니다: 발행자가 서비스를 광고하고, 구독자가 이를 스캔합니다. 발견을 대칭으로 만들기 위해(두 기기가 서로를 발견), PlainApp은 둘 모두를 한 번에 합니다. 이것 없이, 기기 A가 주어진 피어에 대해 자신이 발행자인지 구독자인지 미리 알아야 했을 것입니다 — 하지만 피어 역할은 나중에 clientId 비교로 결정됩니다 (발견과 역할 할당 참조).

동시에 발행하고 구독하면 각 기기가 (구독자로서) 상대의 onServiceDiscovered를 보는 동시에 (발행자로서) 상대의 hello 메시지를 수신합니다 — 핸드셰이크의 양 방향이 항상 사용 가능합니다.

종료 시 자동 재시작

일부 Android 변형(특히 MIUI)은 배터리 절약을 위해 장기 실행 Aware 세션을 종료합니다. PlainApp은 onSessionTerminated 콜백에서 이를 처리합니다: 종료된 세션을 null로 만들고, 여전히 attach된 WifiAwareSession에서 publishOwnService / subscribeOwnService를 즉시 다시 호출합니다. attach 세션 자체는 손실되지 않습니다 — 발행/구독 발견 세션만 손실됩니다. 종료 이전의 피어 핸들은 stale해지며, 이것이 awaitPeerHandle이 discoveredAt 타임스탬프를 확인하고 30초 이상 된 핸들을 폐기하는 이유입니다.

발견과 역할 할당 {#discovery--role-assignment}

Wi-Fi Aware 데이터 경로 프로토콜은 한쪽이 발행자(서버), 다른 쪽이 구독자(클라이언트) 역할을 해야 합니다. 양쪽이 동시에 개시자가 될 수는 없습니다 — 프레임워크가 매칭되는 상대 없이는 요청을 거부합니다.

PlainApp은 clientId의 단순한 사전식 비교로 피어별로 결정적으로 역할을 할당합니다:

Diagram 4
4

결정적이고 협상형이 아닌 이유?

협상형 접근(예: "더 낮은 MAC이 서버")은 추가 메시지 교환이 필요했을 것입니다. 사전식 비교는 멱등적이고, 대칭적이며, 무상태입니다: 두 기기가 통신 없이 동일한 쌍에 대해 동일한 역할을 계산합니다. clientId는 13문자 short UUID이므로, 동점(clientId == peer.id)은 피어를 자신과 비교할 때만 일어나는데 — 이것은 트랜스포트에 결코 도달하지 않습니다.

역할은 다운스트림의 두 가지를 결정합니다:

  1. 재시도 루프를 누가 구동하는가. 클라이언트만 requestNetwork를 재시도합니다; 서버는 hello 수신당 정확히 한 번 시도합니다. 이것이 500ms 윈도우에 중요합니다(다음 섹션).
  2. 포트를 누가 설정하는가. 발행자가 자신의 HTTPS 서버 포트에서 들어오는 연결을 수락하는 쪽이므로 setPort(httpsPort)를 호출합니다. 구독자는 포트를 설정하지 않습니다 — 데이터 경로가 설정된 후 WifiAwareNetworkInfo에서 피어의 포트를 학습합니다.

2단계 핸드셰이크(hello + ready) {#the-two-phase-handshake-hello--ready}

Wi-Fi Aware 데이터 경로 설정에서 가장 어려운 부분은 타이밍입니다. 프레임워크는 양쪽이 서로 약 500ms 이내에 connectivityManager.requestNetwork를 호출해야 합니다 — 한쪽이 상대가 매칭 요청을 등록하기 전에 호출하면, 프레임워크는 onUnavailable("releaseRequestAsUnfulfillableByAnyFactory")로 즉시 거부합니다.

PlainApp은 Aware L2 메시지 채널(onServiceDiscovered가 사용하는 동일한 sendMessage API) 위에서 실행되는 2메시지 애플리케이션 계층 핸드셰이크로 이를 해결합니다:

Diagram 5
5

왜 한 개가 아닌 두 메시지(hello + ready)인가?

hello만으로는 방향 비대칭 때문에 부족합니다. 구독자는 (발행자를 onServiceDiscovered에서 발견하는 즉시) hello를 보낼 수 있지만, 발행자는 구독자의 PeerHandle을 가지기 전까지 requestNetwork를 시작할 수 없으며, 이는 hello를 수신해서만 알게 됩니다. 따라서 hello는 두 가지 목적을 수행합니다:

  1. 구독자의 PeerHandle을 발행자에게 전달. 발행자는 WifiAwareNetworkSpecifier를 만들기 위해 이것이 필요합니다.
  2. 연결 의도 시그널. hello 수신은 발행자에게 "구독자가 requestNetwork하려 하니, 나도 해야 한다"고 알립니다.

ready 영수증은 반대 방향을 위해 존재합니다 — "발행자가 자신의 requestNetwork를 등록했다"를 구독자에게 알리기 위해서입니다. 이것 없이, 구독자의 requestNetwork가 발행자의 것보다 앞서 경쟁하여 프레임워크에 거부당할 수 있습니다. ready 영수증은 논블로킹 시그널입니다: 구독자는 requestNetwork 호출 전에 이것을 기다리지 않습니다(왕복이 추가될 것), 하지만 구독자가 IDLE 상태(재시도 사이)일 때 도착하면, RETRY_DELAY_MS 갭을 기다리지 않고 즉시 재시도할 수 있습니다.

재시도 루프 비대칭

이것이 설계에서 가장 미묘한 부분입니다. 구독자만 재시도합니다. 발행자는 hello 수신당 정확히 한 번의 requestNetwork 시도를 합니다. 그 이유:

  • 양쪽이 독립적으로 재시도했다면, 재시도 주기가 위상에서 벗어났을 것입니다 (서로 다른 delay() 지속 시간, 서로 다른 GC 일시정지), 그리고 두 requestNetwork 호출이 500ms 윈도우 내에서 겹칠 일이 거의 없었을 것입니다.
  • 구독자의 재시도 루프는 매 시도마다 새로운 hello를 보내며, 이것이 publishHelloListeners를 통해 발행자의 buildLink를 재트리거합니다. 이는 발행자의 requestNetwork가 항상 hello 뒤 약 50ms에 뒤따름을 보장하며, 500ms 윈도우 안에 충분히 들어옵니다.

이것은 AwarePeerLink.build 에 상세히 문서화되어 있습니다.

NDP requestNetwork — 500ms 윈도우 {#ndp-requestnetwork--the-500-ms-window}

requestNetwork 호출은 Aware 트랜스포트에서 가장 타이밍에 민감한 오퍼레이션입니다. 각 측에서 일어나는 일:

Diagram 6
6

onUnavailable 콜백의 의미

onUnavailable은 프레임워크가 매칭되는 피어 요청을 찾기 전에 requestNetwork를 거부할 때 발생합니다. PeerHandle 자체는 여전히 유효합니다 — NDP(Neighbor Discovery Protocol) 페어링만 실패했을 뿐, 다른 쪽이 아직 등록하지 않았기 때문입니다. PlainApp은 이 경우 의도적으로 session.invalidatePeerHandle을 호출하지 않습니다. 핸들을 무효화하면 onServiceDiscovered가 호출되었다는 유일한 시그널을 폐기할 것이기 때문입니다(구독 세션 라이프타임 동안 피어당 한 번 발생). 핸들을 보존하면, 재시도가 새로운 발견을 기다리는 대신 이를 재사용할 수 있습니다.

onMessageReceived로부터의 발행자 측 핸들에도 동일하게 적용됩니다 — 발행자는 실패한 시도에 걸쳐 publishPeerHandles[fromCid] 항목을 유지하므로, 구독자의 다음 hello가 캐시된 핸들을 재사용하는 대신 드롭되지 않습니다.

각 페어링된 피어는 자신만의 AwarePeerLink 객체를 갖으며, 프로세스 전체 AwareLinkPool이 소유합니다. 풀은 발견 이벤트, 링크 재사용, 유휴 제거를 처리합니다.

Diagram 7
7

발견 시 자동 빌드를 하지 않는 이유?

풀은 onServiceDiscovered가 발생할 때 링크를 명시적으로 빌드 하지 않습니다. 이것은 중요한 결정입니다: 바쁜 커피숍에 100대의 PlainApp 기기가 모두 "plain-peer" 서비스를 발행하고 있을 수 있습니다. 각 발견이 requestNetwork를 트리거했다면, 프레임워크가 NDP 설정 시도로 넘쳐나고 Wi-Fi 라디오가 포화되었을 것입니다.

대신, 풀은 PeerHandle만 기록하고 다음 중 하나를 기다립니다:

  1. 로컬 사용자가 메시지 전송 → WifiAwareTransport.send → pool.buildLink(peer)(전송자 측 트리거).
  2. 원격 피어가 hello 전송 → onPublishHelloReceived → buildLink(peer)(수신자 측 트리거).
  3. 원격 피어가 ready 전송 → onSubscribeReadyReceived → buildLink(peer)(수신자 측 트리거).

이렇게, 링크는 사용자가 실제로 메시지를 교환 중인 피어에 대해서만 빌드됩니다 — 라디오 범위 내의 모든 PlainApp 기기에 대해 빌드되지 않습니다.

유휴 스위프

10초마다 풀은 모든 링크를 순회하며 lastActiveAt이 60초 이상 된 것을 닫습니다. 각 send와 downloadFile은 타임스탬프를 갱신하기 위해 link.touch()를 호출합니다. 이것은 사용자가 채팅을 멈춘 피어에 대해 Wi-Fi 라디오 컨텍스트와 OkHttp 연결 풀을 회수합니다 — Android가 동시 Aware 데이터 경로 수를 대략 4–10(기기 의존적)으로 제한하므로 중요합니다.

IPv6 어드레싱과 plain-aware-peer DNS 트릭 {#ipv6-addressing--the-plain-aware-peer-dns-trick}

Wi-Fi Aware 데이터 경로는 link-local IPv6 전용을 사용합니다. IPv4도, DNS 서버도, DHCP도 없습니다. 피어의 IPv6 주소는 onCapabilitiesChanged에서 WifiAwareNetworkInfo.peerIpv6Addr 필드를 통해 전달됩니다 — Aware 네트워크 인터페이스에서만 의미 있는 fe80::... 주소입니다.

PlainApp은 이 주소로 HTTPS 요청을 보낼 필요가 있지만, OkHttp의 https:// URL 파싱은 호스트네임에서 raw IPv6 리터럴을 거부합니다 (https://[fe80::abcd]:8443/는 작동하지만, 커스텀 Dns 리졸버를 통해 라우팅하는 것이 더 깔끔합니다). 트릭은 다음과 같습니다:

Diagram 8
8

센티널 호스트네임인 이유?

대안 — IPv6 리터럴을 URL에 직접 전달 — 은 모든 호출 지점이 link-local 주소를 알아야 했을 것입니다. 센티널 호스트네임을 사용하면, URL 생성이 LAN과 Aware에서 동일합니다: 둘 다 OkHttp가 파싱할 수 있는 유효한 https://<host>:<port>/peer_graphql URL을 만듭니다. 유일한 차이는 클라이언트에 바인딩된 Dns 구현입니다 — LAN은 시스템 DNS를 사용하고, Aware는 센티널 호스트네임에 대해 캐시된 link-local 주소를 반환하고 그 외에는 Dns.SYSTEM으로 떨어지는 awareDns(peerIpv6)를 사용합니다.

network.socketFactory인 이유?

Android의 Network 객체는 특정 네트워크 인터페이스(이 경우 Aware 데이터 경로)를 나타냅니다. network.socketFactory를 호출하고 이를 OkHttp의 socketFactory 설정에 전달함으로써, 모든 TCP 소켓이 Aware 인터페이스에서 — 기본 Wi-Fi나 셀룰러 인터페이스가 아닌 — 생성되도록 강제합니다. 이것 없이, OS가 기본 네트워크를 통해 요청을 라우팅할 것이고, 거기서는 link-local IPv6가 도달 불가능하여 요청이 ENETUNREACH로 실패했을 것입니다.

암호학: PMK 파생과 ChaCha20 재사용 {#cryptography-pmk-derivation--chacha20-reuse}

Wi-Fi Aware는 데이터 경로를 위한 선택적 PMK(Pairwise Master Key)를 지원합니다. 설정 시, L2 링크 자체가 그 PMK로 암호화됩니다 — Wi-Fi 라디오가 암호화를 처리하며, 애플리케이션 계층 암호화가 필요 없습니다.

PlainApp은 LanTransport와 BleTransport가 애플리케이션 계층 암호화에 사용하는 동일한 ChaCha20 공유 키로부터 PMK를 파생합니다:

Diagram 9
9

32바이트로 잘라내는 이유?

Wi-Fi Aware PMK는 정확히 32바이트(256비트)여야 합니다. 페어링의 ChaCha20 공유 키도 정상적인 경우 32바이트이므로, raw.size == 32 분기가 일반 경로입니다. 잘라내기/패딩 폴백은 키가 더 짧게 저장된 (이론적) 경우를 처리합니다 — 0으로 32바이트까지 패딩하는 것은 방어적 조치이지, 적절히 페어링된 피어에서 실제로 일어나는 일은 아닙니다.

서명된 봉투는 LAN과 동일

createCryptoHttpClient가 LanTransport가 사용하는 것과 동일한 팩토리이므로, Aware의 L7 암호화는 LAN과 바이트 단위로 동일합니다. 서버 측 PeerGraphQLService는 어느 트랜스포트가 요청을 전달했는지 알지도 못하고 관심도 없습니다 — 그저 서명되고 암호화된 GraphQL 페이로드를 보고 피어의 공유 키로 복호화합니다. 이것이 Chat Architecture에 문서화된 "하나의 코드베이스, 다수의 트랜스포트" 원칙입니다.

메시지 전송 경로(종단 간) {#message-send-path-end-to-end}

모두 합쳐서 — 채팅 메시지가 Wi-Fi Aware로 전송될 때 일어나는 일:

Diagram 10
10

주목할 만한 설계 선택

  • 연결 재사용. 모든 요청 후 GATT 연결을 해체하는 BleTransport와 달리, WifiAwareTransport는 60초 유휴 윈도우 내에서 사용자가 만드는 만큼의 요청에 대해 Aware 데이터 경로를 재사용합니다. 첫 요청은 ~400ms 핸드셰이크를 지불합니다; 이후 요청은 ~10ms 왕복입니다.
  • LAN과 동일한 암호화. ChaCha20 인터셉터와 서명된 봉투는 LAN과 바이트 단위로 동일합니다. 피어의 PeerGraphQLService는 어느 트랜스포트가 요청을 전달했는지 알지 못합니다.
  • 링크 실패 시 선점 없음. buildLink가 실패하면, 트랜스포트는 TransportUnavailable을 throw하고 라우터가 BLE로 넘어갑니다. send 내 재시도는 없습니다 — AwarePeerLink.build가 자체 내부 재시도 루프를 이미 수행합니다(클라이언트에서 MAX_BUILD_ATTEMPTS = 1, prewarmer가 양쪽을 프라이밍했다면 더 많이).

파일 다운로드 경로(종단 간) {#file-download-path-end-to-end}

Aware를 통한 파일 다운로드는 채팅 메시지와 동일한 데이터 경로를 재사용하지만, 큰 파일 스트리밍을 위해 설정된 별도의 OkHttp 클라이언트를 사용합니다:

Diagram 11
11

다운로드를 위해 별도 클라이언트인 이유?

채팅 클라이언트(AwareHttpClientFactory.build)는 30초 requestTimeoutMillis를 갖습니다 — GraphQL 뮤테이션에는 적절하지만 100MB 파일 다운로드에는 재앙입니다. 다운로드 클라이언트 (buildFileDownload)는 다음을 설정합니다:

  • connectTimeoutMillis = 10_000(채팅의 5초보다 길고, 새 데이터 경로에서 느린 첫 패킷에 더 관대)
  • 읽기당 readTimeout = 120초(암묵적 기본값 10초 대비)
  • requestTimeoutMillis = 120_000(2분 — 대부분의 파일에 충분)
  • retryOnConnectionFailure(true) — 다운로드 중간에 드롭된 읽기는 전체 전송을 실패시키는 대신 재시도됩니다

또한 ChaCha20 인터셉터를 생략합니다. /fs 엔드포인트는 날것의 파일 바이트를 서비스(서명된 GraphQL 봉투가 아님)하며, (있는 경우) L2 PMK가 이미 라디오 링크를 암호화합니다. 50MB 비디오를 소프트웨어에서 ChaCha20으로 이중 암호화하면 CPU를 낭비하고 전송이 느려집니다.

버퍼링이 아닌 스트리밍

BLE 경로와 마찬가지로, Aware 다운로드는 파일을 ByteReadChannel을 통해 스트리밍합니다 — 파일은 바이트가 도착하는 대로 임시 파일에 쓰여지며, 메모리에 버퍼링되지 않습니다. PeerFileDownloader는 8KB 청크를 읽고 매 초마다 진행 이벤트를 내보냅니다. 동일한 DownloadedResponse / PeerFileDownloader / DownloadQueue 파이프라인이 모든 트랜스포트에 걸쳐 재사용됩니다 — 트랜스포트 고유의 것은 channel 소스뿐입니다.

사전 워밍: BLE로 트리거되는 Aware 시작 {#prewarming-ble-triggered-aware-startup}

Aware 경로에서 가장 큰 사용자 가시적 지연은 첫 핸드셰이크입니다 — 양쪽 모두 Aware를 아직 시작하지 않았다면, 사용자의 첫 메시지는 다음을 기다려야 합니다:

  1. 로컬 Aware 세션 attach(~1초)
  2. 로컬 발행 + 구독 시작(~1초)
  3. 원격 피어의 Aware 시작(~2초, BLE 경유)
  4. 상호 발견(~1초)
  5. NDP 핸드셰이크(~400ms)

첫 바이트가 전송되기 전에 약 5초입니다. 이 지연을 숨기기 위해, PeerTransportPrewarmer가 ChatPage 진입 시 실행되어 BLE를 통해 원격 피어의 Aware 시작을 트리거합니다:

Diagram 12
12

BLE의 이중 역할

BLE는 여기서 두 가지 목적을 수행합니다:

  1. 피어의 현재 Aware 상태 읽기(저렴, GATT 연결 불필요 — 스캔 응답의 serviceData byte0이 Aware 플래그를 전달).
  2. 지원하지만 현재 실행 중이 아닌 경우 피어에게 Aware 시작을 트리거. 이것은 정규 BleTransport.send 경로를 거칩니다 — 공유 ChaCha20 키로 암호화된 startAware GraphQL 뮤테이션, GATT RPC를 통해 피어의 /peer_graphql 엔드포인트로 전달.

이것은 트랜스포트들이 단순히 폴백하는 대신 협력하는 몇 안 되는 곳 중 하나입니다: BLE는 사용자가 알아채기도 전에 세션을 더 빠른 Aware 트랜스포트로 선제적으로 업그레이드하기 위해 사용됩니다.

낙관적 setAwareRunning(true)인 이유?

startAware 뮤테이션은 원격 피어의 리졸버가 WifiAwareTransport.start()를 호출하는 즉시 성공을 반환합니다 — 하지만 Aware 세션이 실제로 attach된 것은 아닙니다(onAttached가 비동기로 발생). PlainApp은 피어를 낙관적으로 awareRunning = true로 표시합니다, 그 이유는:

  • 실제로 시작되었다면, 다음 send는 Aware를 사용합니다(빠름).
  • 그렇지 않았다면(예: 피어의 Wi-Fi가 꺼져 있음), 다음 send의 buildLink가 TransportUnavailable으로 실패하고 자연스럽게 BLE로 폴백합니다.
  • 거짓 양성의 비용은 ~5초 타임아웃 한 번이지, 영구적 차단이 아닙니다 — PeerCircuitBreaker는 실패를 기록하지만 BLE 구간을 열지는 않습니다 (BLE는 자체 실패에 대해서만 열림).

30초로 스로틀하는 이유?

PeerTransportPrewarmer.prewarm(peerId)는 피어당 타임스탬프를 기록하고 30초 내에는 재실행을 거부합니다. 이것은 사용자가 채팅 목록과 채팅 페이지 사이를 자주 앞뒤로 탐색하기 때문입니다 — 스로틀 없으면, 모든 탐색이 BLE 스캔 + startAware 뮤테이션을 트리거하여 배터리를 소모하고 BLE 라디오를 스팸했을 것입니다. 30초 윈도우는 방금 온라인이 된 피어(예: 사용자가 원격 기기에서 앱을 열었을 때)를 잡기에는 충분히 짧고, 잘못된 재실행을 피하기에는 충분히 깁니다.

실패 모드와 빠른 건너뛰기 플래그 {#failure-modes--the-fast-skip-flag}

Aware는 다른 어떤 트랜스포트보다 더 많은 실패 모드를 갖습니다. isAwareRunning 빠른 건너뛰기 플래그는 전체 모듈에서 가장 중요한 최적화입니다 — 이것 없으면, 모든 send가 BLE로 폴백하기 전에 buildLink 타임아웃으로 10초를 낭비했을 것입니다.

Diagram 13
13

isAwareRunning 플래그가 핵심입니다

이 단일 불리언 없이, 모든 Aware send는 어느 하나였을 것입니다:

  • 항상 buildLink 시도 → Aware가 실행 중이 아닌 피어에 대한 매 전송마다 10초 타임아웃.
  • 항상 Aware 건너뛰기 → 양쪽 모두 실행 중일 때도 절대 사용하지 않음.

이 플래그는 권위 순으로 두 소스에서 갱신됩니다:

  1. BLE 스캔 응답(저렴, GATT 연결 불필요) — PeerTransportPrewarmer.refreshAwareFlagFromScan이 설정. 피어가 9바이트 serviceData 페이로드(byte0 비트필드)에 Aware 상태를 광고.
  2. GATT DISCOVER 응답(권위적) — 전체 발견이 일어날 때 PairingTransport.scanAndDiscover가 설정. 이것은 스캔 힌트를 덮어씁니다.

false일 때, WifiAwareTransport.send와 downloadFile은 즉시TransportUnavailable을 throw합니다 — 스캔 없음, 핸드셰이크 없음, 타임아웃 없음. 라우터는 마이크로초 단위로 BLE로 넘어갑니다.

주요 상수 참조 {#key-constants-reference}

상수값위치목적
AwareSession.SERVICE_NAME"plain-peer"발견모든 PlainApp 기기가 발행 & 구독하는 서비스 이름
AwareSession.PEER_HANDLE_MAX_AGE_MS30 000PeerHandle 캐시stale 핸들 폐기 (피어의 발행 세션이 재시작되었을 수 있음)
AwareSession.READY_TIMEOUT_MS15 000핸드셰이크발행자의 ready 영수증에 대한 구독자 대기
AwareSession.MSG_HELLO0핸드셰이크구독자 → 발행자 메시지 ID
AwareSession.MSG_READY1핸드셰이크발행자 → 구독자 메시지 ID
AwarePeerLink.MAX_BUILD_ATTEMPTS1핸드셰이크 (클라이언트 전용)단일 시도 — 과거 3, 지금 1 (prewarmer가 양쪽을 프라이밍하므로)
AwarePeerLink.ATTEMPT_TIMEOUT_MS5 000핸드셰이크시도당 타임아웃 — 과거 10초, 폴백 속도를 위해 반으로 줄임
AwarePeerLink.RETRY_DELAY_MS500핸드셰이크재시도 간 지연 (클라이언트 전용)
AwarePeerLink.REQUEST_TIMEOUT_MS30 000NDPconnectivityManager.requestNetwork 타임아웃
AwareLinkPool.IDLE_TIMEOUT_MS60 000풀 스위프60초 비활동 후 유휴 링크 닫기
AwareLinkPool.IDLE_SWEEP_INTERVAL_MS10 000풀 스위프스위프 간격
AwareHttpClientFactory.AWARE_HOST"plain-aware-peer"DNS커스텀 Dns에 의해 피어 IPv6로 해석되는 센티널 호스트네임
build 채팅 클라이언트connectTimeout 5초, requestTimeout 30초, ChaCha20 인터셉터
buildFileDownloadconnectTimeout 10초, readTimeout 120초, requestTimeout 120초, 암호화 없음
PeerTransportPrewarmer.PREWARM_TTL_MS30 000사전 워밍피어당 스로틀
PeerTransportPrewarmer.BLE_SCAN_TIMEOUT_MS15 000사전 워밍refreshAwareFlagFromScan을 위한 BLE 스캔 타임아웃
PeerCircuitBreaker.WINDOW_MS30 000Circuit breaker임계값 후 열린 기간
PeerCircuitBreaker.MAX_FAILURES2Circuit breaker열기 위한 윈도우 내 실패
TempData.httpsPort8443 (기본)서버WifiAwareNetworkSpecifier.setPort로 광고되는 발행자 포트
BleServiceData.AWARE_SUPPORTED0x01BLE 스캔 응답피어가 Wi-Fi Aware를 지원함을 나타내는 비트
BleServiceData.AWARE_RUNNING0x02BLE 스캔 응답피어의 Aware 서비스가 현재 실행 중임을 나타내는 비트

설계 트레이드오프 요약 {#design-trade-offs-recap}

Diagram 14
14

더 읽어보기

  • Chat Architecture — WifiAwareTransport가 LAN → Aware → BLE 폴백 체인과 더 넓은 채팅 송수신 파이프라인에 어떻게 들어맞는지.
  • BLE Transport — Aware를 사용할 수 없을 때 인계하는 최후 수단 트랜스포트; 또한 prewarmer가 원격 피어에서 Aware 시작을 트리거하기 위해 사용하는 채널.
  • Pairing Flow — Aware PMK로 재사용되는 공유 ChaCha20 키가 어떻게 설정되는지, 그리고 BLE 스캔 응답 플래그 (AWARE_SUPPORTED / AWARE_RUNNING)가 어떻게 채워지는지.