mirror of
https://github.com/django/daphne.git
synced 2025-07-05 20:33:04 +03:00
Wording changes to docs
I eagerly read through the (excellent, thanks!) documentation for channels, and had to re-read one or two sentences. So being a good citizen, I'm suggesting a few fixes here and there.
This commit is contained in:
parent
f3c3a239b9
commit
50cb6d13d8
|
@ -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
|
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.
|
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,
|
but the modern Web includes things like WebSockets and HTTP2 server push,
|
||||||
which allow websites to communicate outside of this traditional cycle.
|
which allow websites to communicate outside of this traditional cycle.
|
||||||
|
|
||||||
And, beyond that, there are plenty of non-critical tasks that applications
|
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
|
could easily offload until after a response as been sent - like saving things
|
||||||
into a cache, or thumbnailing newly-uploaded images.
|
into a cache, sending emails or thumbnailing newly-uploaded images.
|
||||||
|
|
||||||
Channels changes the way Django runs to be "event oriented" - rather than
|
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
|
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
|
but when things crash it's sent to more than one, which is not the trade-off
|
||||||
we want.
|
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 serialisable,
|
||||||
and not be more than 1MB in size - but these are to make the whole thing
|
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.
|
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?
|
How do we use channels?
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
That's what a channel is, but how is Django using them? Well, inside Django
|
So how is Django using those channels? Inside Django
|
||||||
you can write a function to consume a channel, like so::
|
you can write a function to consume a channel::
|
||||||
|
|
||||||
def my_consumer(message):
|
def my_consumer(message):
|
||||||
pass
|
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 = {
|
channel_routing = {
|
||||||
"some-channel": "myapp.consumers.my_consumer",
|
"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
|
attribute which is always a dict of data, and a "channel" attribute which
|
||||||
is the channel it came from, as well as some others).
|
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
|
Instead of having Django run in the traditional request-response mode,
|
||||||
changes Django so that it runs in a worker mode - it listens on all channels
|
Channels changes Django so that it runs in a worker mode - it listens on
|
||||||
that have consumers assigned, and when a message arrives on one, runs the
|
all channels that have consumers assigned, and when a message arrives on
|
||||||
relevant consumer.
|
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:
|
||||||
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:
|
|
||||||
|
|
||||||
* Interface servers, which communicate between Django and the outside world.
|
* Interface servers, which communicate between Django and the outside world.
|
||||||
This includes a WSGI adapter as well as a separate WebSocket server - we'll
|
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``),
|
Now, let's make a channel for requests (called ``http.request``),
|
||||||
and a channel per client for responses (e.g. ``http.response.o4F2h2Fd``),
|
and a channel per client for responses (e.g. ``http.response.o4F2h2Fd``),
|
||||||
with the response channel a property (``reply_channel``) of the request message.
|
where the response channel is a property (``reply_channel``) of the request
|
||||||
Suddenly, a view is merely another example of a consumer::
|
message. Suddenly, a view is merely another example of a consumer::
|
||||||
|
|
||||||
# Listens on http.request
|
# Listens on http.request
|
||||||
def my_consumer(message):
|
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,
|
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.
|
||||||
|
|
||||||
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
|
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
|
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
|
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 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).
|
||||||
|
|
||||||
.. _channel-types:
|
.. _channel-types:
|
||||||
|
|
||||||
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
|
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
|
consumers - a message gets added to a channel, and then any one of the workers
|
||||||
can pick it up and run the consumer.
|
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
|
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
|
but you're free to make and consume your own, and all channels are
|
||||||
network-transparent.
|
network-transparent.
|
||||||
|
|
||||||
One thing channels are not, however, is guaranteed delivery. If you want tasks
|
One thing channels do not, however, is guaranteeing delivery. If you need
|
||||||
you're sure will complete, use a system designed for this with retries and
|
certainty that tasks will complete, use a system designed for this with
|
||||||
persistence like Celery, or you'll need to make a management command that
|
retries and persistence (e.g. Celery), or alternatively make a management
|
||||||
checks for completion and re-submits a message to the channel if nothing
|
command that checks for completion and re-submits a message to the channel
|
||||||
is completed (rolling your own retry logic, essentially).
|
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
|
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`
|
of the documentation, but for now, let's progress to :doc:`getting-started`
|
||||||
|
|
|
@ -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())
|
message.reply_channel.send(response.channel_encode())
|
||||||
|
|
||||||
The most important thing to note here is that, because things we send in
|
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 serialisable, the request and response messages
|
||||||
are in a key-value format. There are ``channel_decode()`` and
|
are in a key-value format. There are ``channel_decode()`` and
|
||||||
``channel_encode()`` methods on both Django's request and response classes,
|
``channel_encode()`` methods on both Django's request and response classes,
|
||||||
but here we just use the message's ``content`` attribute directly for simplicity
|
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
|
you get the Hello World response, so things are working. If you don't see
|
||||||
a response, check you :doc:`installed Channels correctly <installation>`.
|
a response, check you :doc:`installed Channels correctly <installation>`.
|
||||||
|
|
||||||
Now, that's not very exciting - raw HTTP responses are something Django can
|
Now, that's not very exciting - raw HTTP responses are something Django has
|
||||||
do any time. Let's try some WebSockets, and make a basic chat server!
|
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
|
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 - and make this WebSocket consumer instead::
|
||||||
|
|
||||||
|
# In consumers.py
|
||||||
from channels import Group
|
from channels import Group
|
||||||
|
|
||||||
def ws_add(message):
|
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::
|
Hook it up to the ``websocket.connect`` channel like this::
|
||||||
|
|
||||||
|
# In routing.py
|
||||||
channel_routing = {
|
channel_routing = {
|
||||||
"websocket.connect": "myproject.myapp.consumers.ws_add",
|
"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
|
The solution to this is that the WebSocket interface servers will send
|
||||||
periodic "keepalive" messages on the ``websocket.keepalive`` channel,
|
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
|
so we can hook that up to re-add the channel::
|
||||||
a group it's already in - similarly, it's safe to discard a channel from a
|
|
||||||
group it's not in)::
|
|
||||||
|
|
||||||
|
# In consumers.py
|
||||||
from channels import Group
|
from channels import Group
|
||||||
|
|
||||||
# Connected to websocket.keepalive
|
# Connected to websocket.keepalive
|
||||||
def ws_keepalive(message):
|
def ws_keepalive(message):
|
||||||
Group("chat").add(message.reply_channel)
|
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
|
Of course, this is exactly the same code as the ``connect`` handler, so let's
|
||||||
just route both channels to the same consumer::
|
just route both channels to the same consumer::
|
||||||
|
|
||||||
|
# In routing.py
|
||||||
channel_routing = {
|
channel_routing = {
|
||||||
"websocket.connect": "myproject.myapp.consumers.ws_add",
|
"websocket.connect": "myproject.myapp.consumers.ws_add",
|
||||||
"websocket.keepalive": "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
|
handler to clean up as people disconnect (most channels will cleanly disconnect
|
||||||
and get this called)::
|
and get this called)::
|
||||||
|
|
||||||
|
# In consumers.py
|
||||||
from channels import Group
|
from channels import Group
|
||||||
|
|
||||||
# Connected to websocket.disconnect
|
# 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
|
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::
|
any message sent in to all connected clients. Here's all the code::
|
||||||
|
|
||||||
|
# In consumers.py
|
||||||
from channels import Group
|
from channels import Group
|
||||||
|
|
||||||
# Connected to websocket.connect and websocket.keepalive
|
# 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",
|
"websocket.disconnect": "myproject.myapp.consumers.ws_disconnect",
|
||||||
}
|
}
|
||||||
|
|
||||||
With all that code in your ``consumers.py`` file, you now have a working
|
With all that code, you now have a working set of a logic for a chat server.
|
||||||
set of a logic for a chat server. All you need to do now is get it deployed,
|
All you need to do now is get it deployed, and as we'll see, that's not too
|
||||||
and as we'll see, that's not too hard.
|
hard.
|
||||||
|
|
||||||
Running with Channels
|
Running with Channels
|
||||||
---------------------
|
---------------------
|
||||||
|
@ -168,7 +175,7 @@ Running with Channels
|
||||||
Because Channels takes Django into a multi-process model, you can no longer
|
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.
|
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
|
different type of request - one might do WSGI requests, one might handle
|
||||||
WebSockets, or you might have one that handles both.
|
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
|
Echoing messages is a nice simple example, but it's
|
||||||
skirting around the real design pattern - persistent state for connections.
|
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
|
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. ``https://host/websocket?room=abc``).
|
||||||
|
|
||||||
The ``reply_channel`` attribute you've seen before is our unique pointer to the
|
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
|
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
|
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)::
|
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 import Group
|
||||||
from channels.decorators import channel_session
|
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
|
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
|
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
|
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
|
and a function called ``transfer_user`` which replicates a user from one session
|
||||||
to another.
|
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::
|
chat to people with the same first letter of their username::
|
||||||
|
|
||||||
|
# In consumers.py
|
||||||
from channels import Channel, Group
|
from channels import Channel, Group
|
||||||
from channels.decorators import channel_session
|
from channels.decorators import channel_session
|
||||||
from channels.auth import http_session_user, channel_session_user, transfer_user
|
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 }}``.
|
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
|
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.
|
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
|
message receive - that way, any new message saved will be broadcast to all
|
||||||
the appropriate clients, no matter where it's saved from.
|
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
|
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
|
broadcast into that, meaning the sending process/consumer can move on
|
||||||
immediately and not spend time waiting for the database save and the
|
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
|
Let's see what that looks like, assuming we
|
||||||
have a ChatMessage model with ``message`` and ``room`` fields::
|
have a ChatMessage model with ``message`` and ``room`` fields::
|
||||||
|
|
||||||
|
# In consumers.py
|
||||||
from channels import Channel
|
from channels import Channel
|
||||||
from channels.decorators import channel_session
|
from channels.decorators import channel_session
|
||||||
from .models import ChatMessage
|
from .models import ChatMessage
|
||||||
|
|
||||||
|
# Connected to chat-messages
|
||||||
def msg_consumer(message):
|
def msg_consumer(message):
|
||||||
# Save to model
|
# Save to model
|
||||||
ChatMessage.objects.create(
|
ChatMessage.objects.create(
|
||||||
|
@ -451,7 +462,7 @@ command run via ``cron``. If we wanted to write a bot, too, we could put its
|
||||||
listening logic inside the ``chat-messages`` consumer, as every message would
|
listening logic inside the ``chat-messages`` consumer, as every message would
|
||||||
pass through it.
|
pass through it.
|
||||||
|
|
||||||
Linearization
|
Linearisation
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
There's one final concept we want to introduce you to before you go on to build
|
There's one final concept we want to introduce you to before you go on to build
|
||||||
|
@ -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
|
and other "continuous protocol" things. Here's an example, improving our
|
||||||
first-letter-of-username chat from earlier::
|
first-letter-of-username chat from earlier::
|
||||||
|
|
||||||
|
# In consumers.py
|
||||||
from channels import Channel, Group
|
from channels import Channel, Group
|
||||||
from channels.decorators import channel_session, linearize
|
from channels.decorators import channel_session, linearize
|
||||||
from channels.auth import http_session_user, channel_session_user, transfer_user
|
from channels.auth import http_session_user, channel_session_user, transfer_user
|
||||||
|
|
Loading…
Reference in New Issue
Block a user