# 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 :
- 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.BufferedProtocolpour 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
memoryviewoubytearray— pas de conversion enstrsauf 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 :
- Pas de
async for— à la place, un callbackon_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 :
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
aiohttpde 3–5x,websocketsde 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/awaitconstant 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
Aucun commentaire pour le moment.