Few more WS tweaks

This commit is contained in:
Andrew Godwin 2015-06-15 00:13:32 -07:00
parent 75ee13ff9c
commit d9681df69a
7 changed files with 142 additions and 7 deletions

0
README
View File

85
README.rst Normal file
View File

@ -0,0 +1,85 @@
django-channels
===============
This is a work-in-progress code branch of Django implemented as a third-party
app, which aims to bring some asynchrony to Django and expand the options
for code beyond the request-response model.
The proposal itself is detailed in `a very long Gist <https://gist.github.com/andrewgodwin/b3f826a879eb84a70625>`_
and there is discussion about it on `django-developers <https://groups.google.com/forum/#!forum/django-developers>`_.
If you wish to use this in your own project, there's basic integration
instructions below - but be warned! This is not stable and may change massively
at any time!
Integration
-----------
Make sure you're running Django 1.8. This doesn't work with 1.7 (yet?)
If you want to use WebSockets (and that's kind of the point) you'll need
``autobahn`` and ``twisted`` packages too. Python 3/asyncio support coming soon.
``pip install django-channels`` and then add ``channels`` to the **TOP**
of your ``INSTALLED_APPS`` list (if it is not at the top you won't get the
new runserver command).
You now have a ``runserver`` that actually runs a WSGI interface and a
worker in two different threads, ``runworker`` to run separate workers,
and ``runwsserver`` to run a Twisted-based WebSocket server.
You should place consumers in either your ``views.py`` or a ``consumers.py``.
Here's an example of WebSocket consumers for basic chat::
import redis
from channels import Channel
redis_conn = redis.Redis("localhost", 6379)
@Channel.consumer("django.websockets.connect")
def ws_connect(path, send_channel, **kwargs):
redis_conn.sadd("chatroom", send_channel)
@Channel.consumer("django.websocket.receive")
def ws_receive(channel, send_channel, content, binary, **kwargs):
# Ignore binary messages
if binary:
return
# Re-dispatch message
for channel in redis_conn.smembers("chatroom"):
Channel(channel).send(content=content, binary=False)
@Channel.consumer("django.websocket.disconnect")
def ws_disconnect(channel, send_channel, **kwargs):
redis_conn.srem("chatroom", send_channel)
# NOTE: this does not clean up server crash disconnects,
# you'd want expiring keys here in real life.
Alternately, you can just push some code outside of a normal view into a worker
thread::
from django.shortcuts import render
from channels import Channel
def my_view(request):
# Dispatch a task to run outside the req/response cycle
Channel("a_task_channel").send(value=3)
# Return a response
return render(request, "test.html")
@Channel.consumer("a_task_channel")
def some_task(channel, value):
print "My value was %s from channel %s" % (value, channel)
Limitations
-----------
The ``runserver`` this command provides currently does not support static
media serving, streamed responses or autoreloading.
In addition, this library is a preview and basically might do anything to your
code, or change drastically at any time.
If you have opinions, please provide feedback via the appropriate
`django-developers thread <https://groups.google.com/forum/#!forum/django-developers>`_.

View File

@ -1,3 +1,5 @@
__version__ = "0.1"
# Load backends, using settings if available (else falling back to a default) # Load backends, using settings if available (else falling back to a default)
DEFAULT_CHANNEL_BACKEND = "default" DEFAULT_CHANNEL_BACKEND = "default"
from .backends import BackendManager from .backends import BackendManager

View File

@ -27,16 +27,24 @@ class RedisChannelBackend(BaseChannelBackend):
return redis.Redis(host=self.host, port=self.port) return redis.Redis(host=self.host, port=self.port)
def send(self, channel, message): def send(self, channel, message):
# Write out message into expiring key (avoids big items in list)
key = uuid.uuid4() key = uuid.uuid4()
self.connection.set( self.connection.set(
key, key,
json.dumps(message), json.dumps(message),
ex = self.expiry + 10, ex = self.expiry + 10,
) )
# Add key to list
self.connection.rpush( self.connection.rpush(
self.prefix + channel, self.prefix + channel,
key, key,
) )
# Set list to expire when message does (any later messages will bump this)
self.connection.expire(
self.prefix + channel,
self.expiry + 10,
)
# TODO: Prune expired messages from same list (in case nobody consumes)
def receive_many(self, channels): def receive_many(self, channels):
if not channels: if not channels:

View File

@ -73,12 +73,33 @@ Sent when a datagram is received on the WebSocket.
Contains the same keys as WebSocket Connection, plus: Contains the same keys as WebSocket Connection, plus:
* content: String content of the datagram * content: String content of the datagram
* binary: If the content is to be interpreted as text or binary
WebSocket Close WebSocket Client Close
--------------- ----------------------
Sent when the WebSocket is closed by either the client or the server. Sent when the WebSocket is closed by either the client or the server.
Contains the same keys as WebSocket Connection, including send_channel, Contains the same keys as WebSocket Connection, including send_channel,
though nothing should be sent on it. though nothing should be sent on it.
WebSocket Send
--------------
Sent by a Django consumer to send a message back over the WebSocket to
the client.
Contains the keys:
* content: String content of the datagram
* binary: If the content is to be interpreted as text or binary
WebSocket Server Close
----------------------
Sent by a Django consumer to close the client's WebSocket.
Contains no keys.

View File

@ -14,7 +14,9 @@ class InterfaceProtocol(WebSocketServerProtocol):
def onConnect(self, request): def onConnect(self, request):
self.channel_backend = channel_backends[DEFAULT_CHANNEL_BACKEND] self.channel_backend = channel_backends[DEFAULT_CHANNEL_BACKEND]
self.request = request self.request_info = {
"path": request.path,
}
def onOpen(self): def onOpen(self):
# Make sending channel # Make sending channel
@ -23,6 +25,7 @@ class InterfaceProtocol(WebSocketServerProtocol):
# Send news that this channel is open # Send news that this channel is open
Channel("django.websocket.connect").send( Channel("django.websocket.connect").send(
send_channel = self.send_channel, send_channel = self.send_channel,
**self.request_info
) )
def onMessage(self, payload, isBinary): def onMessage(self, payload, isBinary):
@ -31,24 +34,36 @@ class InterfaceProtocol(WebSocketServerProtocol):
send_channel = self.send_channel, send_channel = self.send_channel,
content = payload, content = payload,
binary = True, binary = True,
**self.request_info
) )
else: else:
Channel("django.websocket.receive").send( Channel("django.websocket.receive").send(
send_channel = self.send_channel, send_channel = self.send_channel,
content = payload.decode("utf8"), content = payload.decode("utf8"),
binary = False, binary = False,
**self.request_info
) )
def onChannelSend(self, content, binary=False, **kwargs): def serverSend(self, content, binary=False, **kwargs):
"""
Server-side channel message to send a message.
"""
if binary: if binary:
self.sendMessage(content, binary) self.sendMessage(content, binary)
else: else:
self.sendMessage(content.encode("utf8"), binary) self.sendMessage(content.encode("utf8"), binary)
def serverClose(self):
"""
Server-side channel message to close the socket
"""
self.sendClose()
def onClose(self, wasClean, code, reason): def onClose(self, wasClean, code, reason):
del self.factory.protocols[self.send_channel] del self.factory.protocols[self.send_channel]
Channel("django.websocket.disconnect").send( Channel("django.websocket.disconnect").send(
send_channel = self.send_channel, send_channel = self.send_channel,
**self.request_info
) )
@ -68,7 +83,10 @@ class InterfaceFactory(WebSocketServerFactory):
return self.protocols.keys() return self.protocols.keys()
def dispatch_send(self, channel, message): def dispatch_send(self, channel, message):
self.protocols[channel].onChannelSend(**message) if message.get("close", False):
self.protocols[channel].serverClose()
else:
self.protocols[channel].serverSend(**message)
class WebsocketTwistedInterface(object): class WebsocketTwistedInterface(object):

View File

@ -1,11 +1,12 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
setup( setup(
name='django-channel', name='django-channels',
version="0.1", version="0.1",
url='http://github.com/andrewgodwin/django-channel', url='http://github.com/andrewgodwin/django-channels',
author='Andrew Godwin', author='Andrew Godwin',
author_email='andrew@aeracode.org', author_email='andrew@aeracode.org',
description="Brings event-driven capabilities to Django with a channel system. Django 1.8 and up only.",
license='BSD', license='BSD',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,