******************* WebSockets Concepts ******************* **Last Updated:** October 2019 This tutorial introduces ``WebSocket`` communication concepts for Tethys developers. A consumer will be created to notify other users when a dam has been added to the app database by someone else, giving the user the option to reload the app to visualize the location of the new dam. The topics covered include: * Django Channels Consumers * WebSocket Connections * Channel Layers * New Dam Notification 0. Start From Advanced Solution (Optional) ========================================== If you wish to use the advanced solution as a starting point: .. parsed-literal:: git clone https://github.com/tethysplatform/tethysapp-dam_inventory.git cd tethysapp-dam_inventory git checkout -b advanced-solution advanced-|version| .. note:: This tutorial can also be built on any other advanced solution, such as the Tethys Quotas tutorial solution. 1. Django Channels Consumers ============================ ``Consumer classes`` are the equivalent of ``controller functions`` when working with `WebSockets` on Tethys. a. Create a new file called ``consumers.py`` and add the following code: :: from channels.generic.websocket import AsyncWebsocketConsumer class NotificationsConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() print("-----------WebSocket Connected-----------") async def disconnect(self, close_code): pass b. Create a ``UrlMap`` for the ``consumer`` in ``app.py`` by adding the following code. :: UrlMap( name='dam_notification', url='dam-inventory/dams/notifications', controller='dam_inventory.consumers.NotificationsConsumer', protocol='websocket' ), .. note:: The controller parameter of the ``UrlMap`` is pointing to the consumer added in the previous step. A new ``protocol parameter`` with a string value equal to `websocket` has been added to the ``UrlMap``. 2. WebSocket Connections ======================== A ``handshake`` needs to be established between the client and server when creating a ``WebSocket connection``. We will use the standard ``JavaScript WebSocket API`` to do this. Create a ``WebSocket connection`` by adding the following code to the ``home.html`` template after the ``app_content`` block. .. code-block:: html+django ... {% block app_content %} {% gizmo dam_inventory_map %}
{% endblock %} {% block after_app_content %} {% endblock %} ... A ``WebSocket URL`` follows a pattern similar to tethys app ``HTTP URLs``. The differences being that the URL starts with ``ws://`` instead of ``http(s)://``, and instead of the "apps" part at the beginning of the URL pattern, the URL ends with a "ws". For example: ``ws://tethys.host.com/base-app-name/base-ws-url/ws/``. If the base name of the app is included in the ``WebSocket URL``, it will not be duplicated. This is the same behavior for ``HTTP URLs``. Upon loading the app home page, the "WebSocket Connected" message will be printed to the terminal. The ``WebSocket connection`` can also be accessed from the browser by right-clicking and selecting inspect, network and filtering by "WS" as displayed in the image below. .. image:: ../images/tutorial/advanced/ws-conn-browser.png :width: 600px :align: center 3. Channel Layers ================= A ``channel layer`` is needed for two or more app instances to communicate between each other (e.g. two different users interacting with the same app at the same time). A ``channel layer`` provides a backend where ``WebSocket messages`` can be stored and then accessed by the different app instances. The updated ``consumer`` in this step opens a communication link (channel_name) in the "notification" channel group on connect, and closes it on disconnect. A new async function has also been added to handle messages. a. Update the ``consumer class`` to look like this. :: ... import json ... class NotificationsConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() await self.channel_layer.group_add("notifications", self.channel_name) print(f"Added {self.channel_name} channel to notifications") async def disconnect(self, close_code): await self.channel_layer.group_discard("notifications", self.channel_name) print(f"Removed {self.channel_name} channel from notifications") async def dam_notifications(self, event): message = event['message'] await self.send(text_data=json.dumps({'message': message})) print(f"Got message {event} at {self.channel_name}") The respective print messages set on connect and disconnect will appear in the terminal when the app home is opened or closed. b. ``Channel layers`` require a backend to store the ``WebSocket messages`` coming from different app instances. These messages can be stored in memory. Add the following peace of code to the :file:`portal_config.yml` file. :: settings: CHANNEL_LAYERS: default: BACKEND: channels.layers.InMemoryChannelLayer .. note:: ``Django Channels`` recommends the use of an external backend store for production environments. The ``channels-redis`` python package plus ``Redis Server`` are the default recommendation. For more information see ``Django Channels`` `channel layers