Haciendo movimientos w / websockets y python / django (/ twisted?)

La parte divertida de websockets es el envío de contenido esencialmente no solicitado desde el servidor al navegador, ¿no?

Bueno, estoy usando django-websocket por Gregor Müllegger. Es una grieta temprana realmente maravillosa para hacer que los websockets funcionen en Django.

He logrado “hola mundo”. La forma en que funciona es: cuando una solicitud es un websocket, un objeto, websocket, se añade al objeto de la solicitud. Por lo tanto, puedo, en la vista interpretando el websocket, hacer algo como:

request.websocket.send('We are the knights who say ni!') 

Eso funciona bien. Recibo el mensaje de vuelta en el navegador como un encanto.

Pero, ¿qué sucede si deseo hacerlo sin emitir ninguna solicitud desde el navegador?

OK, así que primero guardo el websocket en el diccionario de sesión:

 request.session['websocket'] = request.websocket 

Luego, en un shell, voy y tomo la sesión por clave de sesión. Efectivamente, hay un objeto websocket en el diccionario de la sesión. ¡Feliz!

Sin embargo, cuando trato de hacer:

 >>> session.get_decoded()['websocket'].send('With a herring!') 

Yo obtengo:

 Traceback (most recent call last): File "", line 1, in  error: [Errno 9] Bad file descriptor 

Triste. 🙁

De acuerdo, no sé mucho acerca de los sockets, pero sé lo suficiente como para husmear en un depurador, y he aquí que veo el socket en mi depurador (que está vinculado al websocket genuino de la solicitud) tiene fd = 6, mientras que el que tomé del websocket guardado en la sesión tiene fd = -1.

¿Puede una persona orientada a los sockets ayudarme a resolver esto?

Soy el autor de django-websocket. No soy un verdadero experto en el tema de websockets y redes, sin embargo, creo que tengo una comprensión decente de lo que está pasando. Lo siento por entrar en gran detalle. Incluso si la mayor parte de la respuesta no es específica a su pregunta, podría ayudarlo en algún otro punto. 🙂


Cómo funcionan los websockets

Permítanme explicar brevemente qué es un websocket. Un websocket comienza como algo que realmente parece una simple solicitud HTTP, establecida desde el navegador. A través de un encabezado HTTP, indica que quiere “actualizar” el protocolo para que sea un websocket en lugar de una solicitud HTTP. Si el servidor admite websockets, acepta el protocolo de enlace y ambos, servidor y cliente, ahora saben que usarán el socket TCP establecido anteriormente utilizado para la solicitud HTTP como una conexión para intercambiar mensajes de websocket.

Además de enviar y esperar mensajes, también tienen la capacidad de cerrar la conexión en cualquier momento.

Cómo django-websocket abusa del entorno de solicitud wsgi de python para secuestrar el socket

Ahora veamos los detalles de cómo django-websocket implementa la “actualización” de la solicitud HTTP en un ciclo de solicitud-respuesta de django.

Django usualmente usa la especificación WSGI para comunicarse con el servidor web como apache o gunicorn, etc. Esta especificación fue diseñada solo con el modelo de comunicación muy limitado de HTTP en mente. Se supone que recibe una solicitud HTTP (solo datos entrantes) y devuelve la respuesta (solo datos salientes). Esto hace que sea difícil forzar a django en el concepto de un websocket donde se permite la comunicación bidireccional.

Lo que estoy haciendo en django-websocket para lograr esto es que profundizo mucho en los elementos internos de WSGI y el objeto de solicitud de django para recuperar el zócalo subyacente. Este socket TCP se usa para manejar la actualización de la solicitud HTTP a una instancia de websocket directamente.

Ahora a tu pregunta original …

Espero que lo anterior ponga en evidencia que cuando se establece un websocket, no tiene sentido devolver un HttpResponse. Esta es la razón por la que normalmente no devuelve nada en una vista que maneja django-websocket.

Sin embargo, quería mantenerme cerca del concepto de una vista que contiene la lógica y devuelve datos basados ​​en la entrada. Es por eso que solo debe usar el código en su vista para manejar el websocket.

Después de regresar de la vista, el websocket se cierra automáticamente. Esto se hace por una razón: no queremos mantener el socket abierto por un tiempo indefinido y confiar en el cliente (el navegador) para cerrarlo.

Es por esto que no puede acceder a un websocket con django-websocket fuera de su vista. Por supuesto, el descriptor de archivo se establece en -1, lo que indica que ya está cerrado.

Renuncia

Anteriormente expliqué que estoy cavando en el entorno circundante de django para obtener de alguna manera, de una manera muy hackish, el acceso al zócalo subyacente. ¡Esto es muy frágil y tampoco se supone que funcione, ya que WSGI no está diseñado para esto! También expliqué anteriormente que el websocket se cierra después de que finaliza la vista; sin embargo, después de que el websocket se cerró (Y cerró el socket TCP), la implementación WSGI de Django intenta enviar una respuesta HTTP. No sabe sobre WebSockets y piensa que está en un ciclo de solicitud / respuesta HTTP normal. Pero el socket ya está cerrado y el envío fallará. Esto usualmente causa una excepción en django.

Esto no afectó mis pruebas con el servidor de desarrollo. El navegador nunca lo notará (ya sabe … el socket ya está cerrado 😉 – pero generar un error no manejado en cada solicitud no es un concepto muy bueno y puede perder memoria, no maneja el cierre de la conexión de la base de datos correctamente y muchas otras cosas eso se romperá en algún momento si usas django-websocket para más que experimentar.

Es por eso que realmente te aconsejaría que no utilices websockets con django todavía. No funciona por diseño. Django y especialmente WSGI necesitarían una revisión total para resolver estos problemas (consulte esta discusión para websockets y WSGI ). Desde entonces sugeriría usar algo como eventlet . Eventlet tiene una implementación de websocket en funcionamiento (tomé prestado algo de código de eventlet para la versión inicial de django-websocket) y desde su simple código de Python puedes importar tus modelos y todo lo demás desde django. El único inconveniente es que necesita un segundo servidor web que se ejecute solo para manejar websockets.

Como señaló Gregor Müllegger, WSGI no puede manejar adecuadamente los Websockets, porque ese protocolo nunca fue diseñado para manejar tal característica. uWSGI , desde la versión 1.9.11, puede manejar Websockets fuera de la caja. Aquí, uWSGI se comunica con el servidor de aplicaciones utilizando HTTP sin procesar en lugar del protocolo WSGI. Un servidor escrito de esa manera, por lo tanto, puede manejar los protocolos internos y mantener la conexión abierta durante un largo período. Tener conexiones de larga duración manejadas por una vista de Django tampoco es una buena idea, ya que bloquearían un subproceso de trabajo, que es un recurso limitado.

El objective principal de Websockets es que el servidor envíe mensajes al cliente de forma asíncrona. Esta puede ser una vista Django activada por otros navegadores (ej .: clientes de chat, juegos multijugador) o un evento activado por, digamos, django-apio (ej .: resultados deportivos). Por lo tanto, es fundamental para estos servicios de Django usar una cola de mensajes para enviar mensajes al cliente.

Para manejar esto de una manera escalable, escribí django-websocket-redis , un módulo de Django que puede mantener abiertas todas esas conexiones Websocket de larga duración en un solo hilo / proceso usando Redis como la cola de mensajes de back-end.

Puedes darle un golpe al stargate: http://boothead.github.com/stargate/ y http://pypi.python.org/pypi/stargate/ .

Está construido sobre pirámide y eventlet (también contribuí con un poco de soporte y pruebas websocket a eventlet). La gran ventaja de la pirámide para este tipo de cosas es que tiene el concepto de un recurso al que se asigna la url, en lugar de solo el resultado de una llamada. Así que terminará con un gráfico de recursos persistentes que se asigna a su estructura de url y las conexiones de websocket simplemente se enrutan y se conectan a esos recursos.

Así que terminas solo por hacer dos cosas:

 class YourView(WebSocketView): def handler(self, websocket): self.request.context.add_listener(websocket) while True: msg = websocket.wait() # Do something with message 

Para recibir mensajes y

 resource.send(some_other_message) 

Aquí el recurso es una instancia de un stargate.resource.WebSocketAwareContext (como es self.request.context) anterior y el método de envío envía el mensaje a todos los clientes conectados con el método add_listener.

Para publicar un mensaje para todos los clientes conectados que acaba de llamar node.send(message)

Espero escribir una pequeña aplicación de ejemplo en la próxima semana o dos para demostrar esto un poco mejor.

Siéntete libre de hacerme ping en github si quieres ayuda con eso.

request.websocket probablemente se cierre cuando regrese del manejador de solicitudes (ver). La solución simple es mantener el controlador vivo (al no regresar de la vista). Si su servidor no es multiproceso, no podrá aceptar ninguna otra solicitud simultánea.