Cómo picows se convirtió en el motor WebSocket más rápido para asyncio: Una arquitectura sin compromisos
La biblioteca picows ha reescrito las reglas del rendimiento de WebSocket en Python, sacrificando la comodidad por velocidad pura. Olvídate de async for y el ensamblado automático de mensajes: obtén acceso directo a los frames mediante una interfaz de callbacks, procesamiento zero-copy y control total sobre los buffers. Para el trading algorítmico, donde cada milisegundo cuesta dinero, esto no es solo una optimización, es una necesidad.
Por qué las soluciones estándar se quedan cortas
Cuando se trata de procesamiento de datos de alta frecuencia —por ejemplo, actualizaciones de mercado de exchanges de criptomonedas—, aiohttp y websockets ofrecen una latencia inaceptable. Bajo una carga de 10–20 mil mensajes por segundo (200 bytes cada uno), comienzan a acumular datos en colas internas. El retraso entre recibir un paquete de la red y entregarlo a tu lógica de negocio puede alcanzar los 100 ms, fatal para estrategias de trading.
El problema no es Python en sí, sino la arquitectura:
- Los datos se copian múltiples veces: desde socket → buffer del parser → cola → corutina del usuario.
- Los parsers se ejecutan en Python (excepto el parser en C parcialmente reescrito en aiohttp), añadiendo sobrecarga.
- Los mensajes TEXT se convierten a str, requiriendo asignación extra de memoria.
- Enviar mensajes grandes (p. ej., 1 MB) requiere copiar toda la carga solo para añadir un encabezado de 2–14 bytes.
Esto no es solo lento, es ineficientemente predecible. Especialmente al usar asyncio.Protocol, que siempre entrega nuevos objetos bytes en lugar del más rápido BufferedProtocol que te permite trabajar con buffers externos.
Arquitectura de picows: Zero-Copy y Abstracción Mínima
Picows está construida sobre principios que otras bibliotecas considerarían demasiado de bajo nivel:
- Usa asyncio.BufferedProtocol para minimizar copias en lecturas.
- Un parser en C descompone el encabezado del frame y pasa inmediatamente los límites de la carga al código del usuario, sin objetos intermedios.
- Los usuarios trabajan directamente con memoryview o bytearray, sin conversión a str a menos que sea necesario.
- El envío usa send_reuse_external_bytearray: el encabezado se escribe directamente en el espacio reservado antes de la carga, con el enmascaramiento aplicado in situ.
Dejar de lado las comodidades no fue fácil, pero vale la pena:
- No hay async for; en su lugar, un callback on_ws_frame.
- No hay ensamblado automático de mensajes multi-frame: los usuarios deciden cómo concatenarlos.
- No hay soporte integrado para permessage-deflate: maneja la compresión a nivel de la app.
- send() no es async: si el socket está ocupado, los datos van a una cola sin bloquear.
Esto no es un bug, es una característica: llamar a una función async en cada frame añade decenas de microsegundos de latencia. Picows te permite procesar frames sin cambios de contexto de asyncio.
Ejemplos de código: Cliente y servidor con picows
Cliente mínimo:
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())
Servidor 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())
Observa: no hay await dentro de on_ws_frame. El procesamiento ocurre de forma síncrona, sin cambios de contexto. Para comportamiento async, usa create_task o Queue, pero eso es una elección deliberada del desarrollador, no impuesta por la arquitectura.
Pruebas de rendimiento: Dónde picows aplasta a la competencia
Las pruebas muestran la diferencia en RPS (solicitudes por segundo) para intercambios echo con mensajes de varios tamaños:
- Para mensajes pequeños (64–512 bytes), picows supera a aiohttp por 3–5x, a websockets por 8–10x.
- Para mensajes grandes (1 MB+), la ventaja crece aún más, hasta 20x, gracias al envío zero-copy.
- La latencia entre recibir y procesar un frame se mantiene por debajo de 100 microsegundos incluso a 50K RPS.
Factores clave:
- Sin copias innecesarias: los datos se leen y escriben in situ.
- Creación mínima de objetos: sin str, lista o dict intermedios.
- Cero abstracción en el manejo de frames: el callback se dispara justo después del parsing del encabezado.
- Código C optimizado: todo el parsing y construcción de frames ocurre en C, sin intervención del intérprete de Python.
Lecciones clave
- Picows no es para todos: es para quienes necesitan rendimiento máximo por encima de una API cómoda.
- La arquitectura sigue el modelo de transport de asyncio: transport/protocol, no abstracciones de alto nivel.
- El procesamiento zero-copy y el acceso directo a memoria son las ventajas letales sobre la competencia.
- Saltarse async en la ruta crítica redujo la latencia en órdenes de magnitud.
- El soporte TLS y escenarios avanzados (multi-frame, enmascaramiento) se manejan sin pérdida de velocidad.
Cuándo elegir picows — y cuándo no
Usa picows si:
- La latencia por debajo de 1 ms es crítica para manejar miles de mensajes por segundo.
- Estás dispuesto a cambiar comodidad por control sobre memoria y rendimiento.
- Tu lógica puede ejecutarse sin async/await constante en el manejador de frames.
- Trabajas con datos binarios o JSON que se pueden parsear directamente desde memoryview.
Sáltatelo si:
- Priorizas una API simple y arranques rápidos.
- Esperas ensamblado automático de mensajes multi-frame.
- Necesitas compresión integrada o extensiones WebSocket.
- Tu carga es ligera (< 1K RPS) y no quieres complicar tu arquitectura.
Picows no es un reemplazo para aiohttp o websockets. Es una herramienta especializada para casos extremos donde cada ciclo de CPU cuenta. Para trading algorítmico, servidores de juegos, gateways IoT y pipelines de datos de alta frecuencia, es una de las mejores opciones en el ecosistema de Python.
— Editorial Team
Aún no hay comentarios.