Files
games/crates/common/game-realtime-webtransport-lib/README.md
2026-09-22 11:38:58 +02:00

11 KiB

game-realtime-webtransport-lib

Backend WebTransport/QUIC retenu comme second backend realtime de games.sasedev, aux côtés de WebSocket qui reste la baseline/fallback de référence.

Responsabilité

La crate possède le transport WebTransport concret sans introduire de sémantique gameplay, room, joueur, tick ou snapshot. Le chemin natif repose sur web-transport-quinn; le chemin client wasm32-unknown-unknown repose sur web-transport-wasm et l'API WebTransport du navigateur. Les deux conservent les erreurs publiques dans game-realtime-transport-lib.

La frontière fiable disponible couvre désormais :

  • configuration client HTTPS avec pin SHA-256 exact ;
  • identité serveur X.509 DER + clé privée PKCS#8 DER injectables ;
  • génération locale d'une identité self-signed ECDSA P-256 à validité courte pour localhost, IPv4 loopback et IPv6 loopback ;
  • bind UDP/QUIC sur adresse explicite ou port éphémère ;
  • établissement HTTP/3 WebTransport client/server natif ;
  • sélection d'un unique stream bidirectionnel fiable comme chemin realtime principal ;
  • framing privé u32 big-endian + payload binaire ;
  • limite de message configurable, 1 MiB par défaut, vérifiée avant allocation côté réception et avant écriture côté émission ;
  • deadlines configurables sur les chemins natif et navigateur pour la connexion, l'ouverture/accept du stream primaire et un envoi complet ;
  • adaptation RealtimeConnection / RealtimeSender / RealtimeReceiver ;
  • FIN propre via RealtimeSender::close() ;
  • reset/STOP_SENDING backend-spécifiques via WebTransportSender::abort(...) et WebTransportReceiver::abort(...) ;
  • cancellation/drop terminale : un sender abandonné est reset plutôt que transformé implicitement en FIN ;
  • parseur de framing réception incrémental conservant son état si une future receive() est annulée ;
  • mapping stable des erreurs reset/close/session/protocole vers TransportErrorKind ;
  • tracing sous games::realtime::webtransport;
  • client WASM avec endpoint HTTPS, hash certificat SHA-256 explicite, établissement de session navigateur et ouverture du stream bidirectionnel primaire;
  • adaptation WASM du framing u32 big-endian et des traits realtime, y compris FIN, reset/STOP et réception incrémentale.
  • capacité datagram WebTransport backend-spécifique sur les sessions native et navigateur, sans extension de RealtimeConnection.

Chemin navigateur/WASM

Pour wasm32-unknown-unknown, la crate remplace les dépendances natives Quinn/Tokio/rcgen par web-transport-wasm. Le build final reçoit --cfg=web_sys_unstable_apis uniquement pour cette cible via .cargo/config.toml, conformément à l'exigence actuelle des bindings WebTransport de web-sys.

L'API client garde les mêmes noms de surface que le client natif : WebTransportCertificateHash, WebTransportClientConfig, connect, WebTransportSession, WebTransportConnection, WebTransportSender et WebTransportReceiver. Le navigateur ouvre toujours le stream bidirectionnel primaire côté client ; le serveur reste le backend Rust natif.

Le pin SHA-256 est transmis à WebTransportOptions.serverCertificateHashes; aucune variante navigateur sans validation TLS n'est ajoutée. Le framing applicatif reste strictement identique au natif.

À partir de alpha.7, les deadlines connect_timeout, primary_stream_timeout et send_timeout sont aussi matérialisées côté navigateur par des timers WASM. Une deadline navigateur doit tenir dans la plage u32 millisecondes ; une valeur supérieure est rejetée comme InvalidConfiguration. Une réception idle reste volontairement sans timeout implicite, comme sur le chemin natif. Le smoke navigateur technique séparé prouve ensuite cette surface avec un vrai navigateur sans fallback WebSocket.

Datagrams backend-spécifiques

WebTransportSession expose max_datagram_size(), send_datagram(...) et receive_datagram() sur les chemins natif et navigateur. Cette surface reste volontairement propre au backend WebTransport : un datagram n'est ni fiable ni ordonné et ne peut donc pas satisfaire le contrat RealtimeConnection.

La taille maximale dépend de la session et du chemin réseau. Le backend rejette localement un payload supérieur à max_datagram_size() avec TransportErrorKind::MessageTooLarge; cette valeur peut évoluer entre sessions et ne doit pas être traitée comme une constante produit.

receive_datagram() n'impose aucun timeout. Le consommateur doit borner l'attente selon son scénario. Le smoke game-realtime-webtransport-datagram-smoke utilise une deadline locale précisément parce que la perte d'un datagram est un résultat autorisé par le protocole. Il vérifie un échange loopback dans les deux sens sans en déduire de garantie de livraison ou d'ordre.

Le chemin navigateur compile la même capacité via web-transport-wasm; alpha.9 ne réouvre pas le smoke navigateur interactif et limite la preuve runtime datagram au backend natif.

TLS de développement

WebTransportServerIdentity::generate_loopback() crée une identité en mémoire. La clé privée n'est ni écrite ni versionnée. Le certificat est valide sept jours, avec une petite marge de clock skew, et son SHA-256 est exposé à travers WebTransportCertificateHash afin que le client puisse utiliser le pinning fourni par web-transport-quinn.

Une identité préexistante peut être injectée en DER avec WebTransportServerIdentity::from_pkcs8_der(...). La compatibilité certificat/clé est alors vérifiée par le builder TLS au bind du serveur.

Aucune option de désactivation globale de la vérification TLS n'est exposée.

Stream fiable principal

Une session WebTransport établie n'est pas encore le contrat realtime lui-même. Le client appelle WebTransportSession::open_primary_connection() ; le serveur appelle WebTransportSession::accept_primary_connection().

Le chemin logique devient ensuite :

one WebTransport session
    -> one primary bidirectional reliable stream
        -> u32 big-endian payload length
        -> payload bytes

open_primary_connection() écrit déjà l'en-tête de stream WebTransport requis par HTTP/3 avant de retourner. Le pair peut donc terminer accept_primary_connection() avant l'envoi de la première frame applicative ; aucun préambule propre à games.sasedev n'est nécessaire.

RealtimeConnection::split() conserve la session WebTransport dans les deux moitiés afin que la session ne soit pas fermée au moment où l'objet connexion est consommé.

Limites et deadlines

WebTransportConfig::default() conserve la baseline de 1 MiB par message. La limite peut être réduite ou augmentée tant qu'elle reste strictement positive et représentable dans le champ de longueur u32 du framing.

Sur les chemins natif et navigateur, les deadlines configurables couvrent :

  • connexion client et réponse finale à une requête WebTransport déjà surfacée côté serveur ;
  • ouverture ou accept du stream bidirectionnel principal ;
  • écriture complète header + payload d'une frame.

L'attente d'un nouveau pair sur le listener reste volontairement non bornée : un serveur inactif ne doit pas produire périodiquement une erreur uniquement parce qu'aucun client ne se présente.

QUIC applique sa propre flow-control. Le backend n'ajoute pas une seconde file applicative : si un envoi reste bloqué par flow-control/réseau au-delà de send_timeout, l'opération retourne TransportErrorKind::Timeout et le stream est reset afin qu'une frame partiellement transmise ne puisse pas être suivie d'une nouvelle frame invalide. Côté navigateur, le timer est porté par gloo-timers et la future WebTransport abandonnée reste traitée selon la sémantique cancel-safe du wrapper amont.

Lifecycle, abort et cancellation

RealtimeSender::close() reste la fermeture propre de la direction d'émission et produit un FIN. À l'inverse :

  • WebTransportSender::abort(code) envoie un RESET_STREAM WebTransport ;
  • WebTransportReceiver::abort(code) envoie un STOP_SENDING WebTransport ;
  • dropper un WebTransportSender encore actif provoque un reset explicite ;
  • dropper un WebTransportReceiver encore actif provoque un stop explicite ;
  • annuler une future send() en cours provoque également un reset via une garde de cancellation.

Une erreur terminale de lecture/écriture rend la moitié concernée indisponible pour une réutilisation silencieuse.

La réception n'utilise plus une lecture exacte monolithique. Le header et le payload sont lus progressivement avec l'API de lecture cancel-safe de Quinn ; header_read/payload_read restent dans le receiver. Une future receive() annulée peut donc être relancée sans perdre les octets déjà consommés ni décaler le framing.

Mapping d'erreurs

Le backend distingue notamment :

  • payload hors limite -> MessageTooLarge ;
  • reset/STOP_SENDING valide -> Aborted ;
  • stream déjà fermé -> Closed ;
  • fermeture de session WebTransport explicite -> Closed ;
  • erreur de session/connexion non classée comme fermeture propre -> Io ;
  • reset/stop invalide ou framing tronqué -> Protocol ;
  • deadline dépassée -> Timeout sur les chemins natif et navigateur.

Une longueur entrante hors limite ou un framing tronqué provoque aussi l'arrêt de la direction de réception afin d'éviter de poursuivre sur un flux désynchronisé.

Frontières actuelles

La crate ne possède toujours pas :

  • d'API datagram transport-neutral ;
  • de serveur WebTransport WASM ;
  • de fallback WebSocket dans le backend ;
  • de sélection dynamique de transport ;
  • de protocole wire/session ou de synchronisation gameplay.

Le fallback WebSocket est prouvé au niveau composition par game-realtime-transport-fallback-smoke. La caractérisation comparative est portée par game-realtime-transport-measure et documentée dans docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md; elle ne fait pas partie de la responsabilité de cette crate.

Le chemin natif s'exécute sous un runtime Tokio fourni par le consommateur ; la crate ne crée ni runtime ni thread privé. Les dépendances navigateur sont target-specific et ne sont donc pas tirées par le backend natif. Réciproquement, web-transport-quinn, Tokio et rcgen ne sont pas requis pour construire la crate en wasm32-unknown-unknown.

Portabilité validée en 0.3.5

La validation de la version couvre Linux natif client/server, le client navigateur/WASM réel et la cross-compilation du backend pour aarch64-linux-android avec API 21. Cette dernière preuve est une preuve de compilation, pas un smoke réseau sur appareil Android. Les plateformes Apple et les autres ABI Android ne sont pas déclarées validées par 0.3.5.