I hope that through the study of these two articles, I can have a deeper understanding of Channels and use them with ease and ease

From the previous article, Django uses Channels to implement WebSocket– Part 1, you should have a clear understanding of the various concepts of Channels and be able to integrate the Channels framework into your Django projects to implement WebSocket. This article will further introduce Channels with an example of Channels+Celery implementing web-side tailf function

First, let’s talk about the goals we want to achieve: all logged-in users can view the tailF log page and select log files to listen on the page. Multiple page terminals can listen on any log at the same time without affecting each other. The page also provides a button to stop listening to stop the output of the front-end and the reading of log files in the background

The result of the final implementation is shown below

Then let’s look at the implementation process

The technical implementation

All code is based on the following software versions:

  • Python = = 3.6.3
  • Django = = 2.2
  • Channels = = 2.1.7
  • Celery = = 4.3.0

Celery4 support is imperfect on Windows, so run your tests on Linux

Log data definition

We only want users to be able to query a fixed number of log files, rather than using a database to store data by writing global variables to settings.py

In settings.py, add a variable called TAILF, of type dictionary, key for the file number, and value for the file path

TAILF = {
    1: '/ops/coffee/error.log',
    2: '/ops/coffee/access.log',}Copy the code

Setting up basic Web pages

Suppose you have created an app called tailf and added it to INSTALLED_APPS in settings.py. The directory structure of your app looks something like this

tailf
    - migrations
        - __init__.py
    - __init__.py
    - admin.py
    - apps.py
    - models.py
    - tests.py
    - views.py
Copy the code

Still, build a standard Django page, with the following code

url:

from django.urls import path
from django.contrib.auth.views import LoginView,LogoutView

from tailf.views import tailf

urlpatterns = [
    path('tailf', tailf, name='tailf-url'),

    path('login', LoginView.as_view(template_name='login.html'), name='login-url'),
    path('logout', LogoutView.as_view(template_name='login.html'), name='logout-url'),]Copy the code

Since we specify that only logins can view logs, we introduced Django’s own LoginView, which helps us quickly build Login and Logout functions

If you specify a login template, use login.html, which is a standard login page, and pass in username and password

view:

from django.conf import settings
from django.shortcuts import render
from django.contrib.auth.decorators import login_required


# Create your views here.
@login_required(login_url='/login')
def tailf(request):
    logDict = settings.TAILF
    return render(request, 'tailf/index.html', {"logDict": logDict})
Copy the code

The login_required decorator was introduced to determine whether the user is logged in or not, and to jump to the /login login page if not

LogDict takes our TAILF dictionary assignment from Setting and passes it to the front end

template:

{% extends "base.html" %}

{% block content %}
<div class="col-sm-8">
  <select class="form-control" id="file">
    <option value=""> Select the log to listen on </option> {%for k,v in logDict.items %}
    <option value="{{ k }}">{{ v }}</option>
    {% endfor %}
  </select>
</div>
<div class="col-sm-2">
  <input class="btn btn-success btn-block" type="button" onclick="connect()" value="Start listening."/><br/>
</div>
<div class="col-sm-2">
  <input class="btn btn-warning btn-block" type="button" onclick="goclose()" value="Stop listening."/><br/>
</div>
<div class="col-sm-12">
  <textarea class="form-control" id="chat-log" disabled rows="20"></textarea>
</div>
{% endblock %}
Copy the code

The front end gets TAILF and circulates to the select box, because the data is in dictionary format, using logdict. items to loop out dictionary keys and values

This completes a log listening page, but does not enable log listening, so continue

Integrate Channels to implement WebSocket

The main design idea of log monitor function is that the page and the back end server establish websocket long connection, the back end through celery asynchronous while loop continuously read log files and then send them to the websocket channel to achieve real-time display on the page

Then we integrated channels

  1. Add routing routes and modify them directlywebapp/routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter

from django.urls import path, re_path
from chat.consumers import ChatConsumer
from tailf.consumers import TailfConsumer

application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(
        URLRouter([
            path('ws/chat/', ChatConsumer),
            re_path(r'^ws/tailf/(? P
      
       \d+)/$'
      , TailfConsumer),
        ])
    )
})
Copy the code

Write routing information directly to the URLRouter. Note that there is a list in the outer layer of the routing information, which is different from the way of writing the routing file path introduced in the previous article

The page needs to pass the listening log file to the back end. We use routing re P

\d+ to pass the file ID to the back end. The back end gets the ID and resolves the log path according to the TAILF specified in Settings

Routing is written exactly as urls are written in Django, using re_path to match regular routing routes

  1. Add the consumer in thetailf/consumers.pyIn the file
import json
from channels.generic.websocket import WebsocketConsumer
from tailf.tasks import tailf


class TailfConsumer(WebsocketConsumer):
    def connect(self):
        self.file_id = self.scope["url_route"] ["kwargs"] ["id"]

        self.result = tailf.delay(self.file_id, self.channel_name)

        print('connect:', self.channel_name, self.result.id)
        self.accept()

    def disconnect(self, close_code):
        Abort the Task
        self.result.revoke(terminate=True)
        print('disconnect:', self.file_id, self.channel_name)

    def send_message(self, event):
        self.send(text_data=json.dumps({
            "message": event["message"]}))Copy the code

Here, the single-channel mode of Channels is used. Each new connection will start a new channel, which does not affect each other and can terminate any request that listens to the log at will

connect

Self. scope[“url_route”][“kwargs”][“id”] fetch the log ID of the routing rematch. Self. scope[“url_route”][“kwargs”][“id”

Pass the id and channel_name to the celery task function tailf who retrieves the path to the log file according to the id, then loop through the file and write the new content to the corresponding channel according to the channel_name

disconnect

We need to terminate Celery Task execution when websocket connection breaks to clear Celery resource occupancy

Use the terminate Celery task to revoke command using the following code

self.result.revoke(terminate=True)
Copy the code

Note that self.result is a result object, not an ID

The terminate=True argument means whether the Task is terminated immediately, whether it is executing or not, and False (the default) means that the Task is terminated until it has finished. We use the While loop, which is never terminated unless it is set to True

Another way to terminate the Celery task is:

from webapp.celery import app
app.control.revoke(result.id, terminate=True)
Copy the code

send_message

Let’s send messages to channels via Django view/Celery task

Use Celery asynchronous loops to read logs

Celery task tailf (connect) not implemented with Channels (WebSocket

Full details about Celery can be seen in this article: ‘Django configure Celery to perform asynchronous tasks and timed tasks’, this article will not introduce the integration of tasks and details, just talk about tasks

Task implementation code is as follows:

from __future__ import absolute_import
from celery import shared_task

import time
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from django.conf import settings


@shared_task
def tailf(id, channel_name):
    channel_layer = get_channel_layer()
    filename = settings.TAILF[int(id)]

    try:
        with open(filename) as f:
            f.seek(0, 2)

            while True:
                line = f.readline()

                if line:
                    print(channel_name, line)
                    async_to_sync(channel_layer.send)(
                        channel_name,
                        {
                            "type": "send.message"."message": Copyright (C) 2015 all Rights Reserved + str(line)
                        }
                    )
                else: time. Sleep (0.5) except Exception as e:print(e)
Copy the code

This involves another very important point in Channels: sending a message to a Channel from outside the Channels

In fact, the method used in the previous article to check whether the Channel layer is working properly is the example of sending messages to the Channel Channel from outside. The code in this article is as follows

async_to_sync(channel_layer.send)(
    channel_name,
    {
        "type": "send.message"."message": Copyright (C) 2015 all Rights Reserved + str(line)
    }
)
Copy the code

Channel_name corresponds to the channel_name passed to the task, sending messages to the channel with that name

Type corresponds to the send_message method in our Channels TailfConsumer class, replacing the _ in the method. Can be

Message is the specific message to be sent to this channel

The above is the case of sending to a single Channel, if you need to send to a Group, you need to use the following code

async_to_sync(channel_layer.group_send)(
    group_name,
    {
        'type': 'chat.message'.'message': 'Welcome to pay attention to the public account [Yunwei Coffee Bar]'})Copy the code

You only need to change send for single channel to group_send and channel_name to group_name

It is important to note that the channel layer must be used asynchronously through asynC_to_sync

Page to add WebSocket support

Now that the back-end functionality is complete, we finally need to add front-end page support for WebSocket

  function connect() {
    if($('#file').val() ) {
      window.chatSocket = new WebSocket(
        'ws://' + window.location.host + '/ws/tailf/' + $('#file').val() + '/');

      chatSocket.onmessage = function(e) {
        var data = JSON.parse(e.data);
        var message = data['message'];
        document.querySelector('#chat-log').value += (message); // Jump to the bottom of the page $('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
      };

      chatSocket.onerror = function(e) {
        toastr.error('Abnormal server connection! ')}; chatSocket.onclose =function(e) {
        toastr.error('Websocket closed! ')}; }else {
      toastr.warning('Please select log file to listen on')}}Copy the code

Websocket message types were covered in detail in the last article, but I won’t cover them here

At this point we have a log listening page complete with full listening functionality, but not yet terminated, so let’s move on

The Web page disconnects the WebSocket

The main logic of the ‘Stop listen’ button on the Web page is to trigger the onclose method of the WebSocket which triggers the Disconnect method of the consumer at the back end of Channels which in turn terminates the Celery loop read log task

Close () can trigger WebSocket close directly, of course you can also trigger WebSocket onClose if you close the page directly, so don’t worry about not ending the Celery task

  function goclose() {
    console.log(window.chatSocket);

    window.chatSocket.close();
    window.chatSocket.onclose = function(e) {
      toastr.success('Log listening has been terminated! ')}; }Copy the code

At this point, our Tailf log listening, termination page with full functionality is complete

Write in the last

At the end of the two articles, I wonder if you have a deeper understanding of Channels and can start to use Channels in your own projects to achieve ideal functions. Personally, I think the focus and difficulty of Channels lies in the understanding and application of channel layer. With a real understanding and skilled use of Channels, I believe that you will be able to achieve more requirements by drawing on one example from another. Finally, if you are interested in the demo source code of this article, you can pay attention to the wechat public number [Operation and maintenance coffee bar]


Related articles recommended reading:

  • Django uses Channels to implement WebSocket
  • Django configures tasks with asynchronous tasks and timed tasks