mirror of
https://github.com/django/daphne.git
synced 2025-04-21 01:02:06 +03:00
Start updating docs to reflect new interaction pattern
This commit is contained in:
parent
c7d417dd33
commit
899e180c21
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ __pycache__/
|
|||
.tox/
|
||||
*.swp
|
||||
*.pyc
|
||||
TODO
|
||||
|
|
|
@ -94,7 +94,7 @@ single process tied to a WSGI server, Django runs in three separate layers:
|
|||
* The workers, that listen on all relevant channels and run consumer code
|
||||
when a message is ready.
|
||||
|
||||
This may seem quite simplistic, but that's part of the design; rather than
|
||||
This may seem relatively simplistic, but that's part of the design; rather than
|
||||
try and have a full asynchronous architecture, we're just introducing a
|
||||
slightly more complex abstraction than that presented by Django views.
|
||||
|
||||
|
@ -108,22 +108,25 @@ message. Suddenly, a view is merely another example of a consumer::
|
|||
|
||||
# Listens on http.request
|
||||
def my_consumer(message):
|
||||
# Decode the request from JSON-compat to a full object
|
||||
django_request = Request.channel_decode(message.content)
|
||||
# Decode the request from message format to a Request object
|
||||
django_request = AsgiRequest(message)
|
||||
# Run view
|
||||
django_response = view(django_request)
|
||||
# Encode the response into JSON-compat format
|
||||
message.reply_channel.send(django_response.channel_encode())
|
||||
# Encode the response into message format
|
||||
for chunk in AsgiHandler.encode_response(django_response):
|
||||
message.reply_channel.send(chunk)
|
||||
|
||||
In fact, this is how Channels works. The interface servers transform connections
|
||||
from the outside world (HTTP, WebSockets, etc.) into messages on channels,
|
||||
and then you write workers to handle these messages.
|
||||
and then you write workers to handle these messages. Usually you leave normal
|
||||
HTTP up to Django's built-in consumers that plug it into the view/template
|
||||
system, but you can override it to add functionality if you want.
|
||||
|
||||
However, the key here is that you can run code (and so send on channels) in
|
||||
However, the crucial part is that you can run code (and so send on channels) in
|
||||
response to any event - and that includes ones you create. You can trigger
|
||||
on model saves, on other incoming messages, or from code paths inside views
|
||||
and forms. That approach comes in handy for push-style
|
||||
code - where you use HTML5's server-sent events or a WebSocket to notify
|
||||
code - where you WebSockets or HTTP long-polling to notify
|
||||
clients of changes in real time (messages in a chat, perhaps, or live updates
|
||||
in an admin as another user edits something).
|
||||
|
||||
|
@ -168,8 +171,8 @@ Because channels only deliver to a single listener, they can't do broadcast;
|
|||
if you want to send a message to an arbitrary group of clients, you need to
|
||||
keep track of which response channels of those you wish to send to.
|
||||
|
||||
Say I had a live blog where I wanted to push out updates whenever a new post is
|
||||
saved, I would register a handler for the ``post_save`` signal and keep a
|
||||
If I had a liveblog where I wanted to push out updates whenever a new post is
|
||||
saved, I could register a handler for the ``post_save`` signal and keep a
|
||||
set of channels (here, using Redis) to send updates to::
|
||||
|
||||
redis_conn = redis.Redis("localhost", 6379)
|
||||
|
@ -194,7 +197,7 @@ listens to ``websocket.disconnect`` to do that, but we'd also need to
|
|||
have some kind of expiry in case an interface server is forced to quit or
|
||||
loses power before it can send disconnect signals - your code will never
|
||||
see any disconnect notification but the response channel is completely
|
||||
invalid and messages you send there will never get consumed and just expire.
|
||||
invalid and messages you send there will sit there until they expire.
|
||||
|
||||
Because the basic design of channels is stateless, the channel server has no
|
||||
concept of "closing" a channel if an interface server goes away - after all,
|
||||
|
@ -202,15 +205,17 @@ channels are meant to hold messages until a consumer comes along (and some
|
|||
types of interface server, e.g. an SMS gateway, could theoretically serve
|
||||
any client from any interface server).
|
||||
|
||||
That means that we need to follow a keepalive model, where the interface server
|
||||
(or, if you want even better accuracy, the client browser/connection) sends
|
||||
a periodic message saying it's still connected (though only for persistent
|
||||
connection types like WebSockets; normal HTTP doesn't need this as it won't
|
||||
stay connected for more than its own timeout).
|
||||
We don't particularly care if a disconnected client doesn't get the messages
|
||||
sent to the group - after all, it disconnected - but we do care about
|
||||
cluttering up the channel backend tracking all of these clients that are no
|
||||
longer around (and possibly, eventually getting a collision on the reply
|
||||
channel name and sending someone messages not meant for them, though that would
|
||||
likely take weeks).
|
||||
|
||||
Now, we could go back into our example above and add an expiring set and keep
|
||||
track of expiry times and so forth, but this is such a common pattern that
|
||||
we don't need to; Channels has it built in, as a feature called Groups::
|
||||
track of expiry times and so forth, but what would be the point of a framework
|
||||
if it made you add boilerplate code? Instead, Channels implements this
|
||||
abstraction as a core concept called Groups::
|
||||
|
||||
@receiver(post_save, sender=BlogUpdate)
|
||||
def send_update(sender, instance, **kwargs):
|
||||
|
@ -219,19 +224,27 @@ we don't need to; Channels has it built in, as a feature called Groups::
|
|||
content=instance.content,
|
||||
)
|
||||
|
||||
# Connected to websocket.connect and websocket.keepalive
|
||||
# Connected to websocket.connect
|
||||
def ws_connect(message):
|
||||
# Add to reader group
|
||||
Group("liveblog").add(message.reply_channel)
|
||||
|
||||
# Connected to websocket.disconnect
|
||||
def ws_disconnect(message):
|
||||
# Remove from reader group on clean disconnect
|
||||
Group("liveblog").discard(message.reply_channel)
|
||||
|
||||
Not only do groups have their own ``send()`` method (which backends can provide
|
||||
an efficient implementation of), they also automatically manage expiry of
|
||||
the group members. You'll have to re-call ``Group.add()`` every so often to
|
||||
keep existing members from expiring, but that's easy, and can be done in the
|
||||
same handler for both ``connect`` and ``keepalive``, as you can see above.
|
||||
the group members - when the channel starts having messages expire on it due
|
||||
to non-consumption, we go in and remove it from all the groups it's in as well.
|
||||
Of course, you should still remove things from the group on disconnect if you
|
||||
can; the expiry code is there to catch cases where the disconnect message
|
||||
doesn't make it for some reason.
|
||||
|
||||
Groups are generally only useful for response channels (ones starting with
|
||||
the character ``!``), as these are unique-per-client.
|
||||
the character ``!``), as these are unique-per-client, but can be used for
|
||||
normal channels as well if you wish.
|
||||
|
||||
Next Steps
|
||||
----------
|
||||
|
|
|
@ -18,34 +18,40 @@ only one consumer can listen to any given channel.
|
|||
|
||||
As a very basic example, let's write a consumer that overrides the built-in
|
||||
handling and handles every HTTP request directly. This isn't something you'd
|
||||
usually do in a project, but it's a good illustration of how channels
|
||||
now underlie every part of Django.
|
||||
usually do in a project, but it's a good illustration of how Channels
|
||||
actually underlies even core Django.
|
||||
|
||||
Make a new project, a new app, and put this in a ``consumers.py`` file in the app::
|
||||
|
||||
from django.http import HttpResponse
|
||||
from channels.handler import AsgiHandler
|
||||
|
||||
def http_consumer(message):
|
||||
# Make standard HTTP response - access ASGI path attribute directly
|
||||
response = HttpResponse("Hello world! You asked for %s" % message.content['path'])
|
||||
message.reply_channel.send(response.channel_encode())
|
||||
# Encode that response into message format (ASGI)
|
||||
for chunk in AsgiHandler.encode_response(response):
|
||||
message.reply_channel.send(chunk)
|
||||
|
||||
The most important thing to note here is that, because things we send in
|
||||
messages must be JSON serializable, the request and response messages
|
||||
are in a key-value format. There are ``channel_decode()`` and
|
||||
``channel_encode()`` methods on both Django's request and response classes,
|
||||
but here we just use the message's ``content`` attribute directly for simplicity
|
||||
(message content is always a dict).
|
||||
are in a key-value format. You can read more about that format in the
|
||||
:doc:`ASGI specification <asgi>`, but you don't need to worry about it too much;
|
||||
just know that there's an ``AsgiRequest`` class that translates from ASGI into
|
||||
Django request objects, and the ``AsgiHandler`` class handles translation of
|
||||
``HttpResponse`` into ASGI messages, which you see used above. Usually,
|
||||
Django's built-in code will do all this for you when you're using normal views.
|
||||
|
||||
Now, go into your ``settings.py`` file, and set up a channel backend; by default,
|
||||
Django will just use a local backend and route HTTP requests to the normal
|
||||
URL resolver (we'll come back to backends in a minute).
|
||||
Now, go into your ``settings.py`` file, and set up a channel layer; by default,
|
||||
Django will just use an in-memory layer and route HTTP requests to the normal
|
||||
URL resolver (we'll come back to channel layers in a minute).
|
||||
|
||||
For now, we want to override the *channel routing* so that, rather than going
|
||||
to the URL resolver and our normal view stack, all HTTP requests go to our
|
||||
custom consumer we wrote above. Here's what that looks like::
|
||||
|
||||
# In settings.py
|
||||
CHANNEL_BACKENDS = {
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels.database_layer.DatabaseChannelLayer",
|
||||
"ROUTING": "myproject.routing.channel_routing",
|
||||
|
@ -58,11 +64,12 @@ custom consumer we wrote above. Here's what that looks like::
|
|||
}
|
||||
|
||||
As you can see, this is a little like Django's ``DATABASES`` setting; there are
|
||||
named channel backends, with a default one called ``default``. Each backend
|
||||
named channel layers, with a default one called ``default``. Each layer
|
||||
needs a class specified which powers it - we'll come to the options there later -
|
||||
and a routing scheme, which points to a dict containing the routing settings.
|
||||
It's recommended you call this ``routing.py`` and put it alongside ``urls.py``
|
||||
in your project.
|
||||
in your project, but you can put it wherever you like, as long as the path is
|
||||
correct.
|
||||
|
||||
If you start up ``python manage.py runserver`` and go to
|
||||
``http://localhost:8000``, you'll see that, rather than a default Django page,
|
||||
|
@ -74,7 +81,8 @@ been able to do for a long time. Let's try some WebSockets, and make a basic
|
|||
chat server!
|
||||
|
||||
Delete that consumer and its routing - we'll want the normal Django view layer to
|
||||
serve HTTP requests from now on - and make this WebSocket consumer instead::
|
||||
serve HTTP requests from now on, which happens if you don't specify a consumer
|
||||
for ``http.request`` - and make this WebSocket consumer instead::
|
||||
|
||||
# In consumers.py
|
||||
from channels import Group
|
||||
|
@ -85,8 +93,10 @@ serve HTTP requests from now on - and make this WebSocket consumer instead::
|
|||
Hook it up to the ``websocket.connect`` channel like this::
|
||||
|
||||
# In routing.py
|
||||
from myproject.myapp.consumers import ws_add
|
||||
|
||||
channel_routing = {
|
||||
"websocket.connect": "myproject.myapp.consumers.ws_add",
|
||||
"websocket.connect": ws_add,
|
||||
}
|
||||
|
||||
Now, let's look at what this is doing. It's tied to the
|
||||
|
@ -98,39 +108,15 @@ is the unique response channel for that client, and adds it to the ``chat``
|
|||
group, which means we can send messages to all connected chat clients.
|
||||
|
||||
Of course, if you've read through :doc:`concepts`, you'll know that channels
|
||||
added to groups expire out after a while unless you keep renewing their
|
||||
membership. This is because Channels is stateless; the worker processes
|
||||
don't keep track of the open/close states of the potentially thousands of
|
||||
connections you have open at any one time.
|
||||
added to groups expire out if their messages expire (every channel layer has
|
||||
a message expiry time, usually between 30 seconds and a few minutes, and it's
|
||||
often configurable).
|
||||
|
||||
The solution to this is that the WebSocket interface servers will send
|
||||
periodic "keepalive" messages on the ``websocket.keepalive`` channel,
|
||||
so we can hook that up to re-add the channel::
|
||||
|
||||
# In consumers.py
|
||||
from channels import Group
|
||||
|
||||
# Connected to websocket.keepalive
|
||||
def ws_keepalive(message):
|
||||
Group("chat").add(message.reply_channel)
|
||||
|
||||
It's safe to add the channel to a group it's already in - similarly, it's
|
||||
safe to discard a channel from a group it's not in.
|
||||
Of course, this is exactly the same code as the ``connect`` handler, so let's
|
||||
just route both channels to the same consumer::
|
||||
|
||||
# In routing.py
|
||||
channel_routing = {
|
||||
"websocket.connect": "myproject.myapp.consumers.ws_add",
|
||||
"websocket.keepalive": "myproject.myapp.consumers.ws_add",
|
||||
}
|
||||
|
||||
And, even though channels will expire out, let's add an explicit ``disconnect``
|
||||
handler to clean up as people disconnect (most channels will cleanly disconnect
|
||||
and get this called)::
|
||||
|
||||
# In consumers.py
|
||||
from channels import Group
|
||||
However, we'll still get disconnection messages most of the time when a
|
||||
WebSocket disconnects; the expiry/garbage collection of group membership is
|
||||
mostly there for when a disconnect message gets lost (channels are not
|
||||
guaranteed delivery, just mostly reliable). Let's add an explicit disconnect
|
||||
handler::
|
||||
|
||||
# Connected to websocket.disconnect
|
||||
def ws_disconnect(message):
|
||||
|
@ -144,13 +130,17 @@ any message sent in to all connected clients. Here's all the code::
|
|||
# In consumers.py
|
||||
from channels import Group
|
||||
|
||||
# Connected to websocket.connect and websocket.keepalive
|
||||
# Connected to websocket.connect
|
||||
def ws_add(message):
|
||||
Group("chat").add(message.reply_channel)
|
||||
|
||||
# Connected to websocket.receive
|
||||
def ws_message(message):
|
||||
Group("chat").send(message.content)
|
||||
# ASGI WebSocket packet-received and send-packet message types
|
||||
# both have a "text" key for their textual data.
|
||||
Group("chat").send({
|
||||
"text": "[user] %s" % message.content['text'],
|
||||
})
|
||||
|
||||
# Connected to websocket.disconnect
|
||||
def ws_disconnect(message):
|
||||
|
@ -158,11 +148,12 @@ any message sent in to all connected clients. Here's all the code::
|
|||
|
||||
And what our routing should look like in ``routing.py``::
|
||||
|
||||
from myproject.myapp.consumers import ws_add, ws_message, ws_disconnect
|
||||
|
||||
channel_routing = {
|
||||
"websocket.connect": "myproject.myapp.consumers.ws_add",
|
||||
"websocket.keepalive": "myproject.myapp.consumers.ws_add",
|
||||
"websocket.receive": "myproject.myapp.consumers.ws_message",
|
||||
"websocket.disconnect": "myproject.myapp.consumers.ws_disconnect",
|
||||
"websocket.connect": ws_add,
|
||||
"websocket.receive": ws_message,
|
||||
"websocket.disconnect": ws_disconnect,
|
||||
}
|
||||
|
||||
With all that code, you now have a working set of a logic for a chat server.
|
||||
|
@ -172,49 +163,52 @@ hard.
|
|||
Running with Channels
|
||||
---------------------
|
||||
|
||||
Because Channels takes Django into a multi-process model, you can no longer
|
||||
just run one process if you want to serve more than one protocol type.
|
||||
Because Channels takes Django into a multi-process model, you no longer run
|
||||
everything in one process along with a WSGI server (of course, you're still
|
||||
free to do that if you don't want to use Channels). Instead, you run one or
|
||||
more *interface servers*, and one or more *worker servers*, connected by
|
||||
that *channel layer* you configured earlier.
|
||||
|
||||
There are multiple kinds of "interface servers", and each one will service a
|
||||
different type of request - one might do WSGI requests, one might handle
|
||||
WebSockets, or you might have one that handles both.
|
||||
different type of request - one might do both WebSocket and HTTP requests, while
|
||||
another might act as an SMS message gateway, for example.
|
||||
|
||||
These are separate from the "worker servers" where Django will run actual logic,
|
||||
though, and so you'll need to configure a channel backend to allow the
|
||||
channels to run over the network. By default, when you're using Django out of
|
||||
the box, the channel backend is set to an in-memory one that only works in
|
||||
process; this is enough to serve normal WSGI style requests (``runserver`` is
|
||||
just running a WSGI interface and a worker in two separate threads), but now we want
|
||||
WebSocket support we'll need a separate process to keep things clean.
|
||||
though, and so the *channel layer* transports the content of channels across
|
||||
the network. In a production scenario, you'd usually run *worker servers*
|
||||
as a separate cluster from the *interface servers*, though of course you
|
||||
can run both as separate processes on one machine too.
|
||||
|
||||
If you notice, in the example above we switched our default backend to the
|
||||
database channel backend. This uses two tables
|
||||
in the database to do message handling, and isn't particularly fast but
|
||||
requires no extra dependencies. When you deploy to production, you'll want to
|
||||
use a backend like the Redis backend that has much better throughput.
|
||||
By default, Django doesn't have a channel layer configured - it doesn't need one to run
|
||||
normal WSGI requests, after all. As soon as you try to add some consumers,
|
||||
though, you'll need to configure one.
|
||||
|
||||
In the example above we used the database channel layer implementation
|
||||
as our default channel layer. This uses two tables
|
||||
in the ``default`` database to do message handling, and isn't particularly fast but
|
||||
requires no extra dependencies, so it's handy for development.
|
||||
When you deploy to production, though, you'll want to
|
||||
use a backend like the Redis backend that has much better throughput and
|
||||
lower latency.
|
||||
|
||||
The second thing, once we have a networked channel backend set up, is to make
|
||||
sure we're running the WebSocket interface server. Even in development, we need
|
||||
to do this; ``runserver`` will take care of normal Web requests and running
|
||||
a worker for us, but WebSockets isn't compatible with WSGI and needs to run
|
||||
separately.
|
||||
sure we're running an interface server that's capable of serving WebSockets.
|
||||
Luckily, installing Channels will also install ``daphne``, an interface server
|
||||
that can handle both HTTP and WebSockets at the same time, and then ties this
|
||||
in to run when you run ``runserver`` - you shouldn't notice any difference
|
||||
from the normal Django ``runserver``, though some of the options may be a little
|
||||
different.
|
||||
|
||||
The easiest way to do this is to use the ``runwsserver`` management command
|
||||
that ships with Django; just make sure you've installed the latest release
|
||||
of ``autobahn`` first::
|
||||
*(Under the hood, runserver is now running Daphne in one thread and a worker
|
||||
with autoreload in another - it's basically a miniature version of a deployment,
|
||||
but all in one process)*
|
||||
|
||||
pip install -U autobahn[twisted]
|
||||
python manage.py runwsserver
|
||||
Now, let's test our code. Open a browser and put the following into the
|
||||
JavaScript console to open a WebSocket and send some data down it::
|
||||
|
||||
Run that alongside ``runserver`` and you'll have two interface servers, a
|
||||
worker thread, and the channel backend all connected and running. You can
|
||||
even launch separate worker processes with ``runworker`` if you like (you'll
|
||||
need at least one of those if you're not also running ``runserver``).
|
||||
|
||||
Now, just open a browser and put the following into the JavaScript console
|
||||
to test your new code::
|
||||
|
||||
socket = new WebSocket("ws://127.0.0.1:9000");
|
||||
// Note that the path doesn't matter right now; any WebSocket
|
||||
// connection gets bumped over to WebSocket consumers
|
||||
socket = new WebSocket("ws://127.0.0.1:8000/chat/");
|
||||
socket.onmessage = function(e) {
|
||||
alert(e.data);
|
||||
}
|
||||
|
@ -230,15 +224,16 @@ receive the message and show an alert, as any incoming message is sent to the
|
|||
been put into the ``chat`` group when they connected.
|
||||
|
||||
Feel free to put some calls to ``print`` in your handler functions too, if you
|
||||
like, so you can understand when they're called. If you run three or four
|
||||
copies of ``runworker`` you'll probably be able to see the tasks running
|
||||
on different workers.
|
||||
like, so you can understand when they're called. You can also run separate
|
||||
worker processes with ``manage.py runworker`` as well - if you do this, you
|
||||
should see some of the consumers being handled in the ``runserver`` thread and
|
||||
some in the separate worker process.
|
||||
|
||||
Persisting Data
|
||||
---------------
|
||||
|
||||
Echoing messages is a nice simple example, but it's
|
||||
skirting around the real design pattern - persistent state for connections.
|
||||
Echoing messages is a nice simple example, but it's ignoring the real
|
||||
need for a system like this - persistent state for connections.
|
||||
Let's consider a basic chat site where a user requests a chat room upon initial
|
||||
connection, as part of the query string (e.g. ``wss://host/websocket?room=abc``).
|
||||
|
||||
|
@ -250,8 +245,8 @@ global variables or similar.
|
|||
|
||||
Instead, the solution is to persist information keyed by the ``reply_channel`` in
|
||||
some other data store - sound familiar? This is what Django's session framework
|
||||
does for HTTP requests, only there it uses cookies as the lookup key rather
|
||||
than the ``reply_channel``.
|
||||
does for HTTP requests, using a cookie as the key. Wouldn't it be useful if
|
||||
we could get a session using the ``reply_channel`` as a key?
|
||||
|
||||
Channels provides a ``channel_session`` decorator for this purpose - it
|
||||
provides you with an attribute called ``message.channel_session`` that acts
|
||||
|
@ -273,11 +268,6 @@ name in the path of your WebSocket request (we'll ignore auth for now - that's n
|
|||
message.channel_session['room'] = room
|
||||
Group("chat-%s" % room).add(message.reply_channel)
|
||||
|
||||
# Connected to websocket.keepalive
|
||||
@channel_session
|
||||
def ws_keepalive(message):
|
||||
Group("chat-%s" % message.channel_session['room']).add(message.reply_channel)
|
||||
|
||||
# Connected to websocket.receive
|
||||
@channel_session
|
||||
def ws_message(message):
|
||||
|
@ -316,16 +306,16 @@ In addition, we don't want the interface servers storing data or trying to run
|
|||
authentication; they're meant to be simple, lean, fast processes without much
|
||||
state, and so we'll need to do our authentication inside our consumer functions.
|
||||
|
||||
Fortunately, because Channels has standardised WebSocket event
|
||||
:doc:`message-standards`, it ships with decorators that help you with
|
||||
Fortunately, because Channels has an underlying spec for WebSockets and other
|
||||
messages (:doc:`ASGI <asgi>`), it ships with decorators that help you with
|
||||
both authentication and getting the underlying Django session (which is what
|
||||
Django authentication relies on).
|
||||
|
||||
Channels can use Django sessions either from cookies (if you're running your websocket
|
||||
server on the same port as your main site, which requires a reverse proxy that
|
||||
understands WebSockets), or from a ``session_key`` GET parameter, which
|
||||
is much more portable, and works in development where you need to run a separate
|
||||
WebSocket server (by default, on port 9000).
|
||||
Channels can use Django sessions either from cookies (if you're running your
|
||||
websocket server on the same port as your main site, using something like Daphne),
|
||||
or from a ``session_key`` GET parameter, which is works if you want to keep
|
||||
running your HTTP requests through a WSGI server and offload WebSockets to a
|
||||
second server process on another port.
|
||||
|
||||
You get access to a user's normal Django session using the ``http_session``
|
||||
decorator - that gives you a ``message.http_session`` attribute that behaves
|
||||
|
@ -334,12 +324,12 @@ which will provide a ``message.user`` attribute as well as the session attribute
|
|||
|
||||
Now, one thing to note is that you only get the detailed HTTP information
|
||||
during the ``connect`` message of a WebSocket connection (you can read more
|
||||
about what you get when in :doc:`message-standards`) - this means we're not
|
||||
about that in the :doc:`ASGI spec <asgi>`) - this means we're not
|
||||
wasting bandwidth sending the same information over the wire needlessly.
|
||||
|
||||
This also means we'll have to grab the user in the connection handler and then
|
||||
store it in the session; thankfully, Channels ships with both a ``channel_session_user``
|
||||
decorator that works like the ``http_session_user`` decorator you saw above but
|
||||
decorator that works like the ``http_session_user`` decorator we mentioned above but
|
||||
loads the user from the *channel* session rather than the *HTTP* session,
|
||||
and a function called ``transfer_user`` which replicates a user from one session
|
||||
to another.
|
||||
|
@ -361,12 +351,6 @@ chat to people with the same first letter of their username::
|
|||
# Add them to the right group
|
||||
Group("chat-%s" % message.user.username[0]).add(message.reply_channel)
|
||||
|
||||
# Connected to websocket.keepalive
|
||||
@channel_session_user
|
||||
def ws_keepalive(message):
|
||||
# Keep them in the right group
|
||||
Group("chat-%s" % message.user.username[0]).add(message.reply_channel)
|
||||
|
||||
# Connected to websocket.receive
|
||||
@channel_session_user
|
||||
def ws_message(message):
|
||||
|
@ -377,7 +361,9 @@ chat to people with the same first letter of their username::
|
|||
def ws_disconnect(message):
|
||||
Group("chat-%s" % message.user.username[0]).discard(message.reply_channel)
|
||||
|
||||
Now, when we connect to the WebSocket we'll have to remember to provide the
|
||||
If you're just using ``runserver`` (and so Daphne), you can just connect
|
||||
and your cookies should transfer your auth over. If you were running WebSockets
|
||||
on a separate port, you'd have to remember to provide the
|
||||
Django session ID as part of the URL, like this::
|
||||
|
||||
socket = new WebSocket("ws://127.0.0.1:9000/?session_key=abcdefg");
|
||||
|
@ -437,11 +423,6 @@ have a ChatMessage model with ``message`` and ``room`` fields::
|
|||
message.channel_session['room'] = room
|
||||
Group("chat-%s" % room).add(message.reply_channel)
|
||||
|
||||
# Connected to websocket.keepalive
|
||||
@channel_session
|
||||
def ws_add(message):
|
||||
Group("chat-%s" % message.channel_session['room']).add(message.reply_channel)
|
||||
|
||||
# Connected to websocket.receive
|
||||
@channel_session
|
||||
def ws_message(message):
|
||||
|
|
Loading…
Reference in New Issue
Block a user