django.rst (10983B)
1 Integrate with Django 2 ===================== 3 4 If you're looking at adding real-time capabilities to a Django project with 5 WebSocket, you have two main options. 6 7 1. Using Django Channels_, a project adding WebSocket to Django, among other 8 features. This approach is fully supported by Django. However, it requires 9 switching to a new deployment architecture. 10 11 2. Deploying a separate WebSocket server next to your Django project. This 12 technique is well suited when you need to add a small set of real-time 13 features — maybe a notification service — to an HTTP application. 14 15 .. _Channels: https://channels.readthedocs.io/ 16 17 This guide shows how to implement the second technique with websockets. It 18 assumes familiarity with Django. 19 20 Authenticate connections 21 ------------------------ 22 23 Since the websockets server runs outside of Django, we need to integrate it 24 with ``django.contrib.auth``. 25 26 We will generate authentication tokens in the Django project. Then we will 27 send them to the websockets server, where they will authenticate the user. 28 29 Generating a token for the current user and making it available in the browser 30 is up to you. You could render the token in a template or fetch it with an API 31 call. 32 33 Refer to the topic guide on :doc:`authentication <../topics/authentication>` 34 for details on this design. 35 36 Generate tokens 37 ............... 38 39 We want secure, short-lived tokens containing the user ID. We'll rely on 40 `django-sesame`_, a small library designed exactly for this purpose. 41 42 .. _django-sesame: https://github.com/aaugustin/django-sesame 43 44 Add django-sesame to the dependencies of your Django project, install it, and 45 configure it in the settings of the project: 46 47 .. code-block:: python 48 49 AUTHENTICATION_BACKENDS = [ 50 "django.contrib.auth.backends.ModelBackend", 51 "sesame.backends.ModelBackend", 52 ] 53 54 (If your project already uses another authentication backend than the default 55 ``"django.contrib.auth.backends.ModelBackend"``, adjust accordingly.) 56 57 You don't need ``"sesame.middleware.AuthenticationMiddleware"``. It is for 58 authenticating users in the Django server, while we're authenticating them in 59 the websockets server. 60 61 We'd like our tokens to be valid for 30 seconds. We expect web pages to load 62 and to establish the WebSocket connection within this delay. Configure 63 django-sesame accordingly in the settings of your Django project: 64 65 .. code-block:: python 66 67 SESAME_MAX_AGE = 30 68 69 If you expect your web site to load faster for all clients, a shorter lifespan 70 is possible. However, in the context of this document, it would make manual 71 testing more difficult. 72 73 You could also enable single-use tokens. However, this would update the last 74 login date of the user every time a WebSocket connection is established. This 75 doesn't seem like a good idea, both in terms of behavior and in terms of 76 performance. 77 78 Now you can generate tokens in a ``django-admin shell`` as follows: 79 80 .. code-block:: pycon 81 82 >>> from django.contrib.auth import get_user_model 83 >>> User = get_user_model() 84 >>> user = User.objects.get(username="<your username>") 85 >>> from sesame.utils import get_token 86 >>> get_token(user) 87 '<your token>' 88 89 Keep this console open: since tokens expire after 30 seconds, you'll have to 90 generate a new token every time you want to test connecting to the server. 91 92 Validate tokens 93 ............... 94 95 Let's move on to the websockets server. 96 97 Add websockets to the dependencies of your Django project and install it. 98 Indeed, we're going to reuse the environment of the Django project, so we can 99 call its APIs in the websockets server. 100 101 Now here's how to implement authentication. 102 103 .. literalinclude:: ../../example/django/authentication.py 104 105 Let's unpack this code. 106 107 We're calling ``django.setup()`` before doing anything with Django because 108 we're using Django in a `standalone script`_. This assumes that the 109 ``DJANGO_SETTINGS_MODULE`` environment variable is set to the Python path to 110 your settings module. 111 112 .. _standalone script: https://docs.djangoproject.com/en/stable/topics/settings/#calling-django-setup-is-required-for-standalone-django-usage 113 114 The connection handler reads the first message received from the client, which 115 is expected to contain a django-sesame token. Then it authenticates the user 116 with ``get_user()``, the API for `authentication outside a view`_. If 117 authentication fails, it closes the connection and exits. 118 119 .. _authentication outside a view: https://django-sesame.readthedocs.io/en/stable/howto.html#outside-a-view 120 121 When we call an API that makes a database query such as ``get_user()``, we 122 wrap the call in :func:`~asyncio.to_thread`. Indeed, the Django ORM doesn't 123 support asynchronous I/O. It would block the event loop if it didn't run in a 124 separate thread. :func:`~asyncio.to_thread` is available since Python 3.9. In 125 earlier versions, use :meth:`~asyncio.loop.run_in_executor` instead. 126 127 Finally, we start a server with :func:`~websockets.server.serve`. 128 129 We're ready to test! 130 131 Save this code to a file called ``authentication.py``, make sure the 132 ``DJANGO_SETTINGS_MODULE`` environment variable is set properly, and start the 133 websockets server: 134 135 .. code-block:: console 136 137 $ python authentication.py 138 139 Generate a new token — remember, they're only valid for 30 seconds — and use 140 it to connect to your server. Paste your token and press Enter when you get a 141 prompt: 142 143 .. code-block:: console 144 145 $ python -m websockets ws://localhost:8888/ 146 Connected to ws://localhost:8888/ 147 > <your token> 148 < Hello <your username>! 149 Connection closed: 1000 (OK). 150 151 It works! 152 153 If you enter an expired or invalid token, authentication fails and the server 154 closes the connection: 155 156 .. code-block:: console 157 158 $ python -m websockets ws://localhost:8888/ 159 Connected to ws://localhost:8888. 160 > not a token 161 Connection closed: 1011 (internal error) authentication failed. 162 163 You can also test from a browser by generating a new token and running the 164 following code in the JavaScript console of the browser: 165 166 .. code-block:: javascript 167 168 websocket = new WebSocket("ws://localhost:8888/"); 169 websocket.onopen = (event) => websocket.send("<your token>"); 170 websocket.onmessage = (event) => console.log(event.data); 171 172 If you don't want to import your entire Django project into the websockets 173 server, you can build a separate Django project with ``django.contrib.auth``, 174 ``django-sesame``, a suitable ``User`` model, and a subset of the settings of 175 the main project. 176 177 Stream events 178 ------------- 179 180 We can connect and authenticate but our server doesn't do anything useful yet! 181 182 Let's send a message every time a user makes an action in the admin. This 183 message will be broadcast to all users who can access the model on which the 184 action was made. This may be used for showing notifications to other users. 185 186 Many use cases for WebSocket with Django follow a similar pattern. 187 188 Set up event bus 189 ................ 190 191 We need a event bus to enable communications between Django and websockets. 192 Both sides connect permanently to the bus. Then Django writes events and 193 websockets reads them. For the sake of simplicity, we'll rely on `Redis 194 Pub/Sub`_. 195 196 .. _Redis Pub/Sub: https://redis.io/topics/pubsub 197 198 The easiest way to add Redis to a Django project is by configuring a cache 199 backend with `django-redis`_. This library manages connections to Redis 200 efficiently, persisting them between requests, and provides an API to access 201 the Redis connection directly. 202 203 .. _django-redis: https://github.com/jazzband/django-redis 204 205 Install Redis, add django-redis to the dependencies of your Django project, 206 install it, and configure it in the settings of the project: 207 208 .. code-block:: python 209 210 CACHES = { 211 "default": { 212 "BACKEND": "django_redis.cache.RedisCache", 213 "LOCATION": "redis://127.0.0.1:6379/1", 214 }, 215 } 216 217 If you already have a default cache, add a new one with a different name and 218 change ``get_redis_connection("default")`` in the code below to the same name. 219 220 Publish events 221 .............. 222 223 Now let's write events to the bus. 224 225 Add the following code to a module that is imported when your Django project 226 starts. Typically, you would put it in a ``signals.py`` module, which you 227 would import in the ``AppConfig.ready()`` method of one of your apps: 228 229 .. literalinclude:: ../../example/django/signals.py 230 231 This code runs every time the admin saves a ``LogEntry`` object to keep track 232 of a change. It extracts interesting data, serializes it to JSON, and writes 233 an event to Redis. 234 235 Let's check that it works: 236 237 .. code-block:: console 238 239 $ redis-cli 240 127.0.0.1:6379> SELECT 1 241 OK 242 127.0.0.1:6379[1]> SUBSCRIBE events 243 Reading messages... (press Ctrl-C to quit) 244 1) "subscribe" 245 2) "events" 246 3) (integer) 1 247 248 Leave this command running, start the Django development server and make 249 changes in the admin: add, modify, or delete objects. You should see 250 corresponding events published to the ``"events"`` stream. 251 252 Broadcast events 253 ................ 254 255 Now let's turn to reading events and broadcasting them to connected clients. 256 We need to add several features: 257 258 * Keep track of connected clients so we can broadcast messages. 259 * Tell which content types the user has permission to view or to change. 260 * Connect to the message bus and read events. 261 * Broadcast these events to users who have corresponding permissions. 262 263 Here's a complete implementation. 264 265 .. literalinclude:: ../../example/django/notifications.py 266 267 Since the ``get_content_types()`` function makes a database query, it is 268 wrapped inside :func:`asyncio.to_thread()`. It runs once when each WebSocket 269 connection is open; then its result is cached for the lifetime of the 270 connection. Indeed, running it for each message would trigger database queries 271 for all connected users at the same time, which would hurt the database. 272 273 The connection handler merely registers the connection in a global variable, 274 associated to the list of content types for which events should be sent to 275 that connection, and waits until the client disconnects. 276 277 The ``process_events()`` function reads events from Redis and broadcasts them 278 to all connections that should receive them. We don't care much if a sending a 279 notification fails — this happens when a connection drops between the moment 280 we iterate on connections and the moment the corresponding message is sent — 281 so we start a task with for each message and forget about it. Also, this means 282 we're immediately ready to process the next event, even if it takes time to 283 send a message to a slow client. 284 285 Since Redis can publish a message to multiple subscribers, multiple instances 286 of this server can safely run in parallel. 287 288 Does it scale? 289 -------------- 290 291 In theory, given enough servers, this design can scale to a hundred million 292 clients, since Redis can handle ten thousand servers and each server can 293 handle ten thousand clients. In practice, you would need a more scalable 294 message bus before reaching that scale, due to the volume of messages.