mirror of
https://github.com/django/daphne.git
synced 2025-07-10 16:02:18 +03:00
Few more WS tweaks
This commit is contained in:
parent
75ee13ff9c
commit
d9681df69a
85
README.rst
Normal file
85
README.rst
Normal 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>`_.
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user