Django Channels realizes point to point real time chat and message push function

  • 2021-07-18 08:38:15
  • OfStack

Brief introduction in many actual project development, we need to realize many real-time functions; In this article, we use django channels to simply implement peer-to-peer chat and message push functions.

There is a project at hand that needs to use the function of background message push and 1-to-1 online chat between users. For example, if user A comments on user B's post, user B should receive a notice showing that his post has been commented. This function can be completed by the most basic refreshing page after accessing the database, but this will increase the pressure on the background server, and if it is a mobile phone client, it will also cause traffic loss. Therefore, we consider establishing a connection using websocket to complete this function.

However, django does not support websocket, so after a search, django-channels was found, which allowed Django project to handle not only HTTP, but also protocols requiring long connection-WebSockets, MQTT, chatbots, amateur radio and so on.

The author himself also contact channels not long, in order to engage in these two functions see channels documents to see autism, and finally simple to achieve these two functions, specially record 1

1: Install channels

If you are using django 1.9 and above, you can install channels without adding the-U parameter on pip

pip install channels

After installation, we added channels as an app to our django project, in settings. py


INSTALLED_APPS = [
  'django.contrib.admin',
  'django.contrib.auth',
  'django.contrib.contenttypes',
  'django.contrib.sessions',
  'django.contrib.messages',
  'django.contrib.staticfiles',
  'Your-app',
  'channels',
]

Here, we use redis as the channel back end of channels in order to support more functions, and the specific functions involved will be mentioned later. So we also need to install 1 dependency package to support its normal operation

pip install channels_redis

Then add in the settings. py file


CHANNEL_LAYERS = {
  "default": {
    "BACKEND": "channels_redis.core.RedisChannelLayer",
    "CONFIG": {
      "hosts": [('127.0.0.1', 6379)],
    },
    #  Configure the path of the route 
    # "ROUTING": "exmchannels.routing.channel_routing",
  },
}
ASGI_APPLICATION = 'exmchannels.routing.application'

2: Peer-to-peer chat

Create a new file under the project directory to store our channels code, which is channel. Create a new comsumers. py file in channel, and create a new ChatComsumer class to handle websocket requests when we chat. Instead of setting up a chat room, the difference here is that we added an chats to ChatComsumer to record the number of connections per group. Based on the number of connections, it is judged whether both sides of the chat have been connected to the chat group.

At the same time, we set the name form of chat group as id of user_a plus underscore _ plus id of user_b, where id value is placed from small to large, for example: 195752_748418


class ChatConsumer(AsyncJsonWebsocketConsumer):
  chats = dict()
  async def connect(self):
    self.group_name = self.scope['url_route']['kwargs']['group_name']
    await self.channel_layer.group_add(self.group_name, self.channel_name)
    #  Add users to chat group information chats Medium 
    try:
      ChatConsumer.chats[self.group_name].add(self)
    except:
      ChatConsumer.chats[self.group_name] = set([self])
    #print(ChatConsumer.chats)
    #  Called when a connection is created 
    await self.accept()
  async def disconnect(self, close_code):
    #  Called when the connection is closed 
    #  Remove a closed connection from the group 
    await self.channel_layer.group_discard(self.group_name, self.channel_name)
    #  Remove chat group connection information from this client 
    ChatConsumer.chats[self.group_name].remove(self)
    await self.close()

chats in ChatComsumer is a dictionary that records the number of connections per group. Each time a client accesses the correct websocket url, the connect () function is called to add the client to the group pointed to in its url and to add the client's information to the chats. When the client disconnects, the disconnect () function is called to remove the client from group and delete its record in chats.

After completing the connection and disconnection processing, let's proceed to the processing of receiving information


 async def receive_json(self, message, **kwargs):
    #  Called when a message is received 
    to_user = message.get('to_user')
    #  Message sending 
    length = len(ChatConsumer.chats[self.group_name])
    if length == 2:
      await self.channel_layer.group_send(
        self.group_name,
        {
          "type": "chat.message",
          "message": message.get('message'),
        },
      )
    else:
      await self.channel_layer.group_send(
        to_user,
        {
          "type": "push.message",
          "event": {'message': message.get('message'), 'group': self.group_name}
        },
      )
  async def chat_message(self, event):
    # Handles the "chat.message" event when it's sent to us.
    await self.send_json({
      "message": event["message"],
    })

In the above function, we can see that after receiving the websocket information from the client, we first judge whether the number of client connections in this chat group is one or two. If the number of connections is 2, it means that both chat parties have been connected to the chat group, so information can be sent directly to the group, so that the other party can receive the information directly; If the number of connections is 1, it means that the message recipient has not entered the chat group, so we push a message to it, including group_name and the message content.

In this way, we completed a peer-to-peer chat system.

3: Message push

The working principle of message push is roughly the same as the principle of chat 1, that is, every user has his own websocket connection. Here, we can use his username as group_name. When some behaviors of other users trigger push conditions, the background will send a message to the group where the user is located, thus completing the message push service.

Again, specifically under 1, channels also provides a single channel sending, that is, every 1 client connection will generate a special channel name. However, all the things we use here are sent by group. One reason is that I am lazy personally. I can complete the corresponding functions by using group. As long as the user authentication is added when the client connects, it can ensure that each user can only connect to his own group. Of course, we just show how to implement a simple message push here, and we don't show any other code.


#  Push consumer
class PushConsumer(AsyncWebsocketConsumer):
  async def connect(self):
    self.group_name = self.scope['url_route']['kwargs']['username']
    await self.channel_layer.group_add(
      self.group_name,
      self.channel_name
    )
    await self.accept()
  async def disconnect(self, close_code):
    await self.channel_layer.group_discard(
      self.group_name,
      self.channel_name
    )
    # print(PushConsumer.chats)
  async def push_message(self, event):
    print(event)
    await self.send(text_data=json.dumps({
      "event": event['event']
    }))

Message push is to push information to the client in the background, so it does not involve processing the operation of accepting information from the client, so we only need to rewrite connect () and disconnect () functions, and then add a processing function push_message () for sending information

Then we write another push () function to call elsewhere in the project, which is why we used redis as the channel back end of channels in Step 1.


from channels.layers import get_channel_layer

def push(username, event):
  channel_layer = get_channel_layer()
  async_to_sync(channel_layer.group_send)(
    username,
    {
      "type": "push.message",
      "event": event
    }
  )

This function is written outside of PushComsumer, because we don't use self. self. channel_layer to get the channel layer when we call it elsewhere in the project, so write it as a separate function and retrieve it using get_channel_layer.

Therefore, where we need to use message push, we only need to call the push () function directly, and pass in the user name of the pushed user and the pushed information, which is OK.

4: routing configuration and other configurations

Similarly, create a new routing. py file under the channel folder, and then add the following content to it, which works like urls. py1 of django, and is the connection path of websocket.


from . import consumers

websocket_urlpatterns = [
  url(r'^ws/chat/(?P<group_name>[^/]+)/$', consumers.ChatConsumer),
  url(r'^push/(?P<username>[0-9a-z]+)/$', consumers.PushConsumer),
]

Then create a new routing. py file in the same directory as settings. py, and add the following code to it


from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import example.routing

application = ProtocolTypeRouter({
  # (http->django views is added by default)
  'websocket': AuthMiddlewareStack(
    URLRouter(
      example.routing.websocket_urlpatterns
    )
  ),
})

In this way, the client can successfully connect to websocket, and the function is simple to realize.

Summarize


Related articles: