Merge pull request #26 from maikhoepfel/patch-1

Wording changes to docs
This commit is contained in:
Andrew Godwin 2015-11-20 13:07:18 -08:00
commit 17e42a85fb
2 changed files with 51 additions and 44 deletions

View File

@ -5,13 +5,13 @@ Django's traditional view of the world revolves around requests and responses;
a request comes in, Django is fired up to serve it, generates a response to
send, and then Django goes away and waits for the next request.
That was fine when the internet was all driven by simple browser interactions,
That was fine when the internet was driven by simple browser interactions,
but the modern Web includes things like WebSockets and HTTP2 server push,
which allow websites to communicate outside of this traditional cycle.
And, beyond that, there are plenty of non-critical tasks that applications
could easily offload until after a response as been sent - like saving things
into a cache, or thumbnailing newly-uploaded images.
into a cache or thumbnailing newly-uploaded images.
Channels changes the way Django runs to be "event oriented" - rather than
just responding to requests, instead Django responses to a wide array of events
@ -38,7 +38,7 @@ alternative is *at-least-once*, where normally one consumer gets the message
but when things crash it's sent to more than one, which is not the trade-off
we want.
There are a couple of other limitations - messages must be JSON-serialisable,
There are a couple of other limitations - messages must be JSON serializable,
and not be more than 1MB in size - but these are to make the whole thing
practical, and not too important to think about up front.
@ -59,13 +59,13 @@ channel, they're writing into the same channel.
How do we use channels?
-----------------------
That's what a channel is, but how is Django using them? Well, inside Django
you can write a function to consume a channel, like so::
So how is Django using those channels? Inside Django
you can write a function to consume a channel::
def my_consumer(message):
pass
And then assign a channel to it like this in the channel routing::
And then assign a channel to it in the channel routing::
channel_routing = {
"some-channel": "myapp.consumers.my_consumer",
@ -76,14 +76,11 @@ consumer function with a message object (message objects have a "content"
attribute which is always a dict of data, and a "channel" attribute which
is the channel it came from, as well as some others).
Django can do this as rather than run in a request-response mode, Channels
changes Django so that it runs in a worker mode - it listens on all channels
that have consumers assigned, and when a message arrives on one, runs the
relevant consumer.
In fact, this is illustrative of the new way Django runs to enable Channels to
work. Rather than running in just a single process tied to a WSGI server,
Django runs in three separate layers:
Instead of having Django run in the traditional request-response mode,
Channels changes Django so that it runs in a worker mode - it listens on
all channels that have consumers assigned, and when a message arrives on
one, it runs the relevant consumer. So rather than running in just a
single process tied to a WSGI server, Django runs in three separate layers:
* Interface servers, which communicate between Django and the outside world.
This includes a WSGI adapter as well as a separate WebSocket server - we'll
@ -104,8 +101,8 @@ message and can write out zero to many other channel messages.
Now, let's make a channel for requests (called ``http.request``),
and a channel per client for responses (e.g. ``http.response.o4F2h2Fd``),
with the response channel a property (``reply_channel``) of the request message.
Suddenly, a view is merely another example of a consumer::
where the response channel is a property (``reply_channel``) of the request
message. Suddenly, a view is merely another example of a consumer::
# Listens on http.request
def my_consumer(message):
@ -120,22 +117,20 @@ 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.
This may seem like it's still not very well designed to handle push-style
code - where you use HTTP2's server-sent events or a WebSocket to notify
clients of changes in real time (messages in a chat, perhaps, or live updates
in an admin as another user edits something).
However, the key here 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.
and forms. That approach comes in handy for push-style
code - where you use HTML5's server-sent events or a WebSocket to notify
clients of changes in real time (messages in a chat, perhaps, or live updates
in an admin as another user edits something).
.. _channel-types:
Channel Types
-------------
Now, if you think about it, there are actually two major uses for channels in
There are actually two major uses for channels in
this model. The first, and more obvious one, is the dispatching of work to
consumers - a message gets added to a channel, and then any one of the workers
can pick it up and run the consumer.
@ -240,15 +235,15 @@ Next Steps
----------
That's the high-level overview of channels and groups, and how you should
starting thinking about them - remember, Django provides some channels
start thinking about them. Remember, Django provides some channels
but you're free to make and consume your own, and all channels are
network-transparent.
One thing channels are not, however, is guaranteed delivery. If you want tasks
you're sure will complete, use a system designed for this with retries and
persistence like Celery, or you'll need to make a management command that
checks for completion and re-submits a message to the channel if nothing
is completed (rolling your own retry logic, essentially).
One thing channels do not, however, is guarantee delivery. If you need
certainty that tasks will complete, use a system designed for this with
retries and persistence (e.g. Celery), or alternatively make a management
command that checks for completion and re-submits a message to the channel
if nothing is completed (rolling your own retry logic, essentially).
We'll cover more about what kind of tasks fit well into Channels in the rest
of the documentation, but for now, let's progress to :doc:`getting-started`

View File

@ -30,7 +30,7 @@ Make a new project, a new app, and put this in a ``consumers.py`` file in the ap
message.reply_channel.send(response.channel_encode())
The most important thing to note here is that, because things we send in
messages must be JSON-serialisable, the request and response messages
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
@ -69,12 +69,14 @@ If you start up ``python manage.py runserver`` and go to
you get the Hello World response, so things are working. If you don't see
a response, check you :doc:`installed Channels correctly <installation>`.
Now, that's not very exciting - raw HTTP responses are something Django can
do any time. Let's try some WebSockets, and make a basic chat server!
Now, that's not very exciting - raw HTTP responses are something Django has
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::
# In consumers.py
from channels import Group
def ws_add(message):
@ -82,6 +84,7 @@ 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
channel_routing = {
"websocket.connect": "myproject.myapp.consumers.ws_add",
}
@ -102,19 +105,21 @@ connections you have open at any one time.
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 (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)::
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",
@ -124,6 +129,7 @@ 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
# Connected to websocket.disconnect
@ -135,6 +141,7 @@ Now, that's taken care of adding and removing WebSocket send channels for the
we're not going to store a history of messages or anything and just replay
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
@ -158,9 +165,9 @@ And what our routing should look like in ``routing.py``::
"websocket.disconnect": "myproject.myapp.consumers.ws_disconnect",
}
With all that code in your ``consumers.py`` file, you now have a working
set of a logic for a chat server. All you need to do now is get it deployed,
and as we'll see, that's not too hard.
With all that code, you now have a working set of a logic for a chat server.
All you need to do now is get it deployed, and as we'll see, that's not too
hard.
Running with Channels
---------------------
@ -168,7 +175,7 @@ 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.
There are multiple kinds of "interface server", and each one will service a
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.
@ -233,7 +240,7 @@ Persisting Data
Echoing messages is a nice simple example, but it's
skirting around the real design pattern - 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. ``http://host/websocket?room=abc``).
connection, as part of the query string (e.g. ``wss://host/websocket?room=abc``).
The ``reply_channel`` attribute you've seen before is our unique pointer to the
open WebSocket - because it varies between different clients, it's how we can
@ -253,6 +260,7 @@ just like a normal Django session.
Let's use it now to build a chat server that expects you to pass a chatroom
name in the path of your WebSocket request (we'll ignore auth for now - that's next)::
# In consumers.py
from channels import Group
from channels.decorators import channel_session
@ -282,7 +290,7 @@ name in the path of your WebSocket request (we'll ignore auth for now - that's n
If you play around with it from the console (or start building a simple
JavaScript chat client that appends received messages to a div), you'll see
that you can now request which chat room you want in the initial request.
that you can set a chat room with the initial request.
Authentication
--------------
@ -336,9 +344,10 @@ 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.
Bringing that all together, let's make a chat server one where users can only
Bringing that all together, let's make a chat server where users can only
chat to people with the same first letter of their username::
# In consumers.py
from channels import Channel, Group
from channels.decorators import channel_session
from channels.auth import http_session_user, channel_session_user, transfer_user
@ -375,7 +384,7 @@ Django session ID as part of the URL, like this::
You can get the current session key in a template with ``{{ request.session.session_key }}``.
Note that Channels can't work with signed cookie sessions - since only HTTP
responses can set cookies, it needs a backend it can write to separately to
responses can set cookies, it needs a backend it can write to to separately to
store state.
@ -393,7 +402,7 @@ easily integrate the send into the save flow of the model, rather than the
message receive - that way, any new message saved will be broadcast to all
the appropriate clients, no matter where it's saved from.
We'll even take some performance considerations into account - We'll make our
We'll even take some performance considerations into account: We'll make our
own custom channel for new chat messages and move the model save and the chat
broadcast into that, meaning the sending process/consumer can move on
immediately and not spend time waiting for the database save and the
@ -402,10 +411,12 @@ immediately and not spend time waiting for the database save and the
Let's see what that looks like, assuming we
have a ChatMessage model with ``message`` and ``room`` fields::
# In consumers.py
from channels import Channel
from channels.decorators import channel_session
from .models import ChatMessage
# Connected to chat-messages
def msg_consumer(message):
# Save to model
ChatMessage.objects.create(
@ -482,6 +493,7 @@ decorator, but generally you'll want to use it for most session-based WebSocket
and other "continuous protocol" things. Here's an example, improving our
first-letter-of-username chat from earlier::
# In consumers.py
from channels import Channel, Group
from channels.decorators import channel_session, linearize
from channels.auth import http_session_user, channel_session_user, transfer_user