Retour à l'accueil

Picows : le WebSocket le plus rapide pour asyncio sans compromis

Analyse de l'architecture de la bibliothèque picows — le moteur WebSocket le plus rapide pour asyncio. Décomposition du traitement zero-copy, interface de rappel et rejet de async dans le hot-path. Exemples de code et comparaison des performances.

Picows : pourquoi est-ce le WebSocket le plus rapide en Python ?
Advertisement 728x90

# Comment picows est devenu le moteur WebSocket le plus rapide pour asyncio : Une architecture sans compromis

La bibliothèque picows a réécrit les règles de la performance WebSocket en Python, en sacrifiant le confort au profit d'une vitesse brute. Oubliez async for et l'assemblage automatique des messages — obtenez un accès direct aux frames via une interface de callback, un traitement zero-copy et un contrôle total sur les buffers. Pour le trading algorithmique, où chaque milliseconde coûte de l'argent, ce n'est pas qu'une optimisation — c'est une nécessité.

Pourquoi les solutions standard sont insuffisantes

Pour le traitement de données à haute fréquence — par exemple, les mises à jour de marché des échanges crypto — aiohttp et websockets affichent une latence inacceptable. Sous une charge de 10–20 mille messages par seconde (200 octets chacun), ils commencent à accumuler les données dans des files internes. Le délai entre la réception d'un paquet du réseau et sa livraison à votre logique métier peut atteindre 100 ms — fatal pour les stratégies de trading.

Le problème ne vient pas de Python lui-même, mais de l'architecture :

Google AdInline article slot
  • Les données sont copiées plusieurs fois : du socket → buffer du parseur → file → coroutines utilisateur.
  • Les parseurs s'exécutent en Python (sauf le parseur C partiellement réécrit dans aiohttp), ajoutant une surcharge.
  • Les messages TEXT sont convertis en str, nécessitant une allocation mémoire supplémentaire.
  • L'envoi de gros messages (par ex., 1 Mo) exige de copier toute la charge utile juste pour ajouter un en-tête de 2–14 octets.

Ce n'est pas seulement lent — c'est prévisiblement inefficace. Surtout avec asyncio.Protocol, qui fournit toujours de nouveaux objets bytes au lieu du plus rapide BufferedProtocol qui permet de travailler avec des buffers externes.

Architecture de picows : Zero-Copy et abstraction minimale

Picows repose sur des principes que d'autres bibliothèques jugeraient trop bas niveau :

  • Utilise asyncio.BufferedProtocol pour minimiser les copies à la lecture.
  • Un parseur C décompose l'en-tête de frame et transmet immédiatement les limites de la charge utile au code utilisateur — sans objets intermédiaires.
  • Les utilisateurs travaillent directement avec memoryview ou bytearray — pas de conversion en str sauf si nécessaire.
  • L'envoi utilise send_reuse_external_bytearray — l'en-tête est écrit directement dans l'espace réservé avant la charge utile, avec masquage appliqué sur place.

Renoncer aux commodités n'a pas été facile, mais ça en vaut la peine :

Google AdInline article slot
  • Pas de async for — à la place, un callback on_ws_frame.
  • Pas d'assemblage automatique de messages multi-frames — les utilisateurs décident comment les concaténer.
  • Pas de support intégré de permessage-deflate — gérez la compression au niveau applicatif.
  • send() n'est pas asynchrone — si le socket est occupé, les données vont en file sans blocage.

Ce n'est pas un bug, c'est une fonctionnalité : appeler une fonction asynchrone à chaque frame ajoute des dizaines de microsecondes de latence. Picows vous permet de traiter les frames sans aucun changement de contexte asyncio.

Exemples de code : Client et serveur avec picows

Client minimal :

import asyncio
from picows import ws_connect, WSFrame, WSTransport, WSListener, WSMsgType, WSCloseCode

class ClientListener(WSListener):
    def on_ws_connected(self, transport: WSTransport):
        transport.send(WSMsgType.TEXT, b"Hello world")

    def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
        print(f"Echo reply: {frame.get_payload_as_ascii_text()}")
        transport.send_close(WSCloseCode.OK)
        transport.disconnect()

async def main():
    transport, client = await ws_connect(ClientListener, "ws://127.0.0.1:9001")
    await transport.wait_disconnected()

if __name__ == '__main__':
    asyncio.run(main())

Serveur echo :

Google AdInline article slot
import asyncio
from picows import ws_create_server, WSFrame, WSTransport, WSListener, WSMsgType, WSUpgradeRequest

class ServerClientListener(WSListener):
    def on_ws_connected(self, transport: WSTransport):
        print("New client connected")

    def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
        if frame.msg_type == WSMsgType.CLOSE:
            transport.send_close(frame.get_close_code(), frame.get_close_message())
            transport.disconnect()
        else:
            transport.send(frame.msg_type, frame.get_payload_as_memoryview())

async def main():
    def listener_factory(r: WSUpgradeRequest):
        return ServerClientListener()

    server: asyncio.Server = await ws_create_server(listener_factory, "127.0.0.1", 9001)
    for s in server.sockets:
        print(f"Server started on {s.getsockname()}")

    await server.serve_forever()

if __name__ == "__main__":
    asyncio.run(main())

Remarque : pas d'await à l'intérieur de on_ws_frame. Le traitement se fait de manière synchrone, sans changements de contexte. Pour un comportement asynchrone, utilisez create_task ou Queue — mais c'est un choix délibéré du développeur, pas imposé par l'architecture.

Benchmarks : Là où picows laisse les concurrents sur place

Les tests montrent la différence en RPS (requêtes par seconde) pour les échanges echo avec des messages de diverses tailles :

  • Pour les petits messages (64–512 octets), picows surpasse aiohttp de 3–5x, websockets de 8–10x.
  • Pour les gros messages (1 Mo+), l'avance est encore plus grande — jusqu'à 20x, grâce à l'envoi zero-copy.
  • La latence entre réception et traitement d'une frame reste sous 100 microsecondes même à 50 kRPS.

Facteurs clés :

  • Aucune copie inutile — les données sont lues et écrites sur place.
  • Création minimale d'objets — pas d'str, de liste ou de dict intermédiaires.
  • Aucune abstraction sur le traitement des frames — le callback s'active juste après l'analyse de l'en-tête.
  • Code C optimisé — tout le parsing et la construction de frames se fait en C, sans intervention de l'interpréteur Python.

Points clés

  • Picows n'est pas pour tout le monde — c'est pour ceux qui privilégient la performance maximale à une API confortable.
  • L'architecture suit le modèle de transport de asyncio : transport/protocol, pas d'abstractions de haut niveau.
  • Le traitement zero-copy et l'accès direct à la mémoire sont les avantages décisifs sur les concurrents.
  • Éviter l'asynchrone dans le chemin chaud a réduit la latence d'ordres de grandeur.
  • Le support TLS et les scénarios avancés (multi-frames, masquage) sont gérés sans perte de vitesse.

Quand choisir picows — et quand ne pas le faire

Utilisez picows si :

  • Une latence sous 1 ms est critique pour gérer des milliers de messages par seconde.
  • Vous êtes prêt à troquer le confort contre un contrôle sur la mémoire et la performance.
  • Votre logique peut s'exécuter sans async/await constant dans le gestionnaire de frames.
  • Vous traitez des données binaires ou du JSON qui peut être parsé directement depuis memoryview.

Évitez-le si :

  • Vous privilégiez une API simple et des démarrages rapides.
  • Vous attendez un assemblage automatique de messages multi-frames.
  • Vous avez besoin d'une compression intégrée ou d'extensions WebSocket.
  • Votre charge est légère (< 1 kRPS) et vous ne voulez pas compliquer votre architecture.

Picows n'est pas un remplacement pour aiohttp ou websockets. C'est un outil spécialisé pour les cas extrêmes où chaque cycle CPU compte. Pour le trading algorithmique, les serveurs de jeux, les passerelles IoT et les pipelines de données à haute fréquence — c'est l'une des meilleures options de l'écosystème Python.

— Editorial Team

Advertisement 728x90

Lire ensuite