File: channel_layers.rst

package info (click to toggle)
python-django-channels 4.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 872 kB
  • sloc: python: 2,394; makefile: 154; sh: 8
file content (314 lines) | stat: -rw-r--r-- 10,842 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
Channel Layers
==============

Channel layers allow you to talk between different instances of an application.
They're a useful part of making a distributed realtime application if you don't
want to have to shuttle all of your messages or events through a database.

Additionally, they can also be used in combination with a worker process
to make a basic task queue or to offload tasks - read more in
:doc:`/topics/worker`.


.. note::

    Channel layers are an entirely optional part of Channels. If you don't want
    to use them, just leave ``CHANNEL_LAYERS`` unset, or set it to the empty
    dict ``{}``.

.. warning::

    Channel layers have a purely async interface (for both send and receive);
    you will need to wrap them in a converter if you want to call them from
    synchronous code (see below).


Configuration
-------------

Channel layers are configured via the ``CHANNEL_LAYERS`` Django setting.

You can get the default channel layer from a project with
``channels.layers.get_channel_layer()``, but if you are using consumers, then a copy
is automatically provided for you on the consumer as ``self.channel_layer``.

Redis Channel Layer
*******************

`channels_redis`_ is the only official Django-maintained channel layer
supported for production use. The layer uses Redis as its backing store,
and it supports both a single-server and sharded configurations as well as
group support. To use this layer you'll need to install the `channels_redis`_
package.

.. _`channels_redis`: https://pypi.org/project/channels-redis/

In this example, Redis is running on localhost (127.0.0.1) port 6379:

.. code-block:: python

    CHANNEL_LAYERS = {
        "default": {
            "BACKEND": "channels_redis.core.RedisChannelLayer",
            "CONFIG": {
                "hosts": [("127.0.0.1", 6379)],
            },
        },
    }

In-Memory Channel Layer
***********************

Channels also comes packaged with an in-memory Channels Layer. This layer can
be helpful in :doc:`/topics/testing` or for local-development purposes:

.. code-block:: python

    CHANNEL_LAYERS = {
        "default": {
            "BACKEND": "channels.layers.InMemoryChannelLayer"
        }
    }

.. warning::

    **Do Not Use In Production**

    In-memory channel layers operate with each process as a separate layer.
    This means that no cross-process messaging is possible. As the core value
    of channel layers is to provide distributed messaging, in-memory usage will
    result in sub-optimal performance, and ultimately data-loss in a
    multi-instance environment.

Synchronous Functions
---------------------

By default the ``send()``, ``group_send()``, ``group_add()`` and other
functions are async functions, meaning you have to ``await`` them. If you need
to call them from synchronous code, you'll need to use the handy
``asgiref.sync.async_to_sync`` wrapper:

.. code-block:: python

    from asgiref.sync import async_to_sync

    async_to_sync(channel_layer.send)("channel_name", {...})


What To Send Over The Channel Layer
-----------------------------------

The channel layer is for high-level application-to-application communication.
When you send a message, it is received by the consumers listening to the group
or channel on the other end. What this means is that you should send high-level
events over the channel layer, and then have consumers handle those events, and
do appropriate low-level networking to their attached client.

For example, a chat application could send events like this over the channel
layer:

.. code-block:: python

    await self.channel_layer.group_send(
        room.group_name,
        {
            "type": "chat.message",
            "room_id": room_id,
            "username": self.scope["user"].username,
            "message": message,
        }
    )

And then the consumers define a handling function to receive those events
and turn them into WebSocket frames:

.. code-block:: python

    async def chat_message(self, event):
        """
        Called when someone has messaged our chat.
        """
        # Send a message down to the client
        await self.send_json(
            {
                "msg_type": settings.MSG_TYPE_MESSAGE,
                "room": event["room_id"],
                "username": event["username"],
                "message": event["message"],
            },
        )

Any consumer based on Channels' ``SyncConsumer`` or ``AsyncConsumer`` will
automatically provide you a ``self.channel_layer`` and ``self.channel_name``
attribute, which contains a pointer to the channel layer instance and the
channel name that will reach the consumer respectively.

Any message sent to that channel name - or to a group the channel name was
added to - will be received by the consumer much like an event from its
connected client, and dispatched to a named method on the consumer. The name
of the method will be the ``type`` of the event with periods replaced by
underscores - so, for example, an event coming in over the channel layer
with a ``type`` of ``chat.join`` will be handled by the method ``chat_join``.

.. note::

    If you are inheriting from the ``AsyncConsumer`` class tree, all your
    event handlers, including ones for events over the channel layer, must
    be asynchronous (``async def``). If you are in the ``SyncConsumer`` class
    tree instead, they must all be synchronous (``def``).


Single Channels
---------------

Each application instance - so, for example, each long-running HTTP request
or open WebSocket - results in a single Consumer instance, and if you have
channel layers enabled, Consumers will generate a unique *channel name* for
themselves, and start listening on it for events.

This means you can send those consumers events from outside the process -
from other consumers, maybe, or from management commands - and they will react
to them and run code just like they would events from their client connection.

The channel name is available on a consumer as ``self.channel_name``. Here's
an example of writing the channel name into a database upon connection,
and then specifying a handler method for events on it:

.. code-block:: python

    class ChatConsumer(WebsocketConsumer):

        def connect(self):
            # Make a database row with our channel name
            Clients.objects.create(channel_name=self.channel_name)

        def disconnect(self, close_code):
            # Note that in some rare cases (power loss, etc) disconnect may fail
            # to run; this naive example would leave zombie channel names around.
            Clients.objects.filter(channel_name=self.channel_name).delete()

        def chat_message(self, event):
            # Handles the "chat.message" event when it's sent to us.
            self.send(text_data=event["text"])

Note that, because you're mixing event handling from the channel layer and
from the protocol connection, you need to make sure that your type names do not
clash. It's recommended you prefix type names (like we did here with ``chat.``)
to avoid clashes.

To send to a single channel, just find its channel name (for the example above,
we could crawl the database), and use ``channel_layer.send``:

.. code-block:: python

    from channels.layers import get_channel_layer

    channel_layer = get_channel_layer()
    await channel_layer.send("channel_name", {
        "type": "chat.message",
        "text": "Hello there!",
    })


.. _groups:

Groups
------

Obviously, sending to individual channels isn't particularly useful - in most
cases you'll want to send to multiple channels/consumers at once as a broadcast.
Not only for cases like a chat where you want to send incoming messages to
everyone in the room, but even for sending to an individual user who might have
more than one browser tab or device connected.

You can construct your own solution for this if you like using your existing
datastores, or you can use the Groups system built-in to some channel layers. Groups
is a broadcast system that:

* Allows you to add and remove channel names from named groups, and send to
  those named groups.

* Provides group expiry for clean-up of connections whose disconnect handler
  didn't get to run (e.g. power failure)

They do not allow you to enumerate or list the channels in a group; it's a
pure broadcast system. If you need more precise control or need to know who
is connected, you should build your own system or use a suitable third-party
one.

You use groups by adding a channel to them during connection, and removing it
during disconnection, illustrated here on the WebSocket generic consumer:

.. code-block:: python

    # This example uses WebSocket consumer, which is synchronous, and so
    # needs the async channel layer functions to be converted.
    from asgiref.sync import async_to_sync

    class ChatConsumer(WebsocketConsumer):

        def connect(self):
            async_to_sync(self.channel_layer.group_add)("chat", self.channel_name)

        def disconnect(self, close_code):
            async_to_sync(self.channel_layer.group_discard)("chat", self.channel_name)

.. note::

    Group names are restricted to ASCII alphanumerics, hyphens, and periods
    only and are limited to a maximum length of 100 in the default backend.

Then, to send to a group, use ``group_send``, like in this small example
which broadcasts chat messages to every connected socket when combined with
the code above:

.. code-block:: python

    class ChatConsumer(WebsocketConsumer):

        ...

        def receive(self, text_data):
            async_to_sync(self.channel_layer.group_send)(
                "chat",
                {
                    "type": "chat.message",
                    "text": text_data,
                },
            )

        def chat_message(self, event):
            self.send(text_data=event["text"])


Using Outside Of Consumers
--------------------------

You'll often want to send to the channel layer from outside of the scope of
a consumer, and so you won't have ``self.channel_layer``. In this case, you
should use the ``get_channel_layer`` function to retrieve it:

.. code-block:: python

    from channels.layers import get_channel_layer
    channel_layer = get_channel_layer()

Then, once you have it, you can just call methods on it. Remember that
**channel layers only support async methods**, so you can either call it
from your own asynchronous context:

.. code-block:: python

    for chat_name in chats:
        await channel_layer.group_send(
            chat_name,
            {"type": "chat.system_message", "text": announcement_text},
        )

Or you'll need to use async_to_sync:

.. code-block:: python

    from asgiref.sync import async_to_sync

    async_to_sync(channel_layer.group_send)("chat", {"type": "chat.force_disconnect"})