diff --git a/docs/concepts.rst b/docs/concepts.rst index 5bbf21b..fbe4275 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -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` diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 7370f40..e11b0db 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -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 `. -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