diff --git a/README b/README deleted file mode 100644 index e69de29..0000000 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..30421e4 --- /dev/null +++ b/README.rst @@ -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 `_ +and there is discussion about it on `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 `_. diff --git a/channels/__init__.py b/channels/__init__.py index c0b7065..883c43a 100644 --- a/channels/__init__.py +++ b/channels/__init__.py @@ -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 diff --git a/channels/backends/redis_py.py b/channels/backends/redis_py.py index 13ea81d..7c65fe9 100644 --- a/channels/backends/redis_py.py +++ b/channels/backends/redis_py.py @@ -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: diff --git a/channels/docs/message-standards.rst b/channels/docs/message-standards.rst index d22d2df..c1c6528 100644 --- a/channels/docs/message-standards.rst +++ b/channels/docs/message-standards.rst @@ -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. diff --git a/channels/interfaces/websocket_twisted.py b/channels/interfaces/websocket_twisted.py index a24f233..4cb7083 100644 --- a/channels/interfaces/websocket_twisted.py +++ b/channels/interfaces/websocket_twisted.py @@ -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): diff --git a/setup.py b/setup.py index e8274ab..ae21988 100644 --- a/setup.py +++ b/setup.py @@ -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,