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)
DEFAULT_CHANNEL_BACKEND = "default"
from .backends import BackendManager

View File

@ -27,16 +27,24 @@ class RedisChannelBackend(BaseChannelBackend):
return redis.Redis(host=self.host, port=self.port)
def send(self, channel, message):
# Write out message into expiring key (avoids big items in list)
key = uuid.uuid4()
self.connection.set(
key,
json.dumps(message),
ex = self.expiry + 10,
)
# Add key to list
self.connection.rpush(
self.prefix + channel,
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):
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:
* 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.
Contains the same keys as WebSocket Connection, including send_channel,
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):
self.channel_backend = channel_backends[DEFAULT_CHANNEL_BACKEND]
self.request = request
self.request_info = {
"path": request.path,
}
def onOpen(self):
# Make sending channel
@ -23,6 +25,7 @@ class InterfaceProtocol(WebSocketServerProtocol):
# Send news that this channel is open
Channel("django.websocket.connect").send(
send_channel = self.send_channel,
**self.request_info
)
def onMessage(self, payload, isBinary):
@ -31,24 +34,36 @@ class InterfaceProtocol(WebSocketServerProtocol):
send_channel = self.send_channel,
content = payload,
binary = True,
**self.request_info
)
else:
Channel("django.websocket.receive").send(
send_channel = self.send_channel,
content = payload.decode("utf8"),
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:
self.sendMessage(content, binary)
else:
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):
del self.factory.protocols[self.send_channel]
Channel("django.websocket.disconnect").send(
send_channel = self.send_channel,
**self.request_info
)
@ -68,7 +83,10 @@ class InterfaceFactory(WebSocketServerFactory):
return self.protocols.keys()
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):

View File

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