چت در جنگو با channels جنگو
03 Khordad 1402
0 نظر
77 : 23
چت در جنگو با channels

بعد از ایجاد پروژه اپ جدیدی بسازید برای من اسمش هست chats

 

موارد مورد نیاز

اقدام به نصب پکیج های زیر کنید.

channels

channels-redis

بعد از نصب لازمه که channels رو به لیست INSTALLED_APPS در فایل setting اضافه کنید.

 

پیاده سازی

به فایل urls رفته و کد های زیر رو اضافه کنید.

from django.urls import path
from . import views

app_name = "chats"

urlpatterns = [

    path('', views.chats, name='chats.base'),
    path('<str:room_name>/', views.room, name='room'),


]

در اینجا با اولین url کار داریم که میشه views.chats

خب حالا به views میریم تا متد های مربوطه رو بنویسیم.

def chats(request):
    return render(request, 'chat/chat.html')

این متد یک صفحه html رو برمیگردونه که توش میتونیم نام گروهی که میخوام داخلش شروع به گفتگو کنیم رو وارد کنیم.

   <section class="d-flex bg-danger flex-column" style="margin-top: 20rem; height: 15rem">
    <input id="room-name-input" type="text" size="100"><br>
    <input id="room-name-submit" type="button" value="Enter">

    <script>
        document.querySelector('#room-name-input').focus();
        document.querySelector('#room-name-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#room-name-submit').click();
            }
        };

        document.querySelector('#room-name-submit').onclick = function(e) {
            var roomName = document.querySelector('#room-name-input').value;
            window.location.pathname = '/chat/' + roomName + '/';
        };
    </script>

   </section>

به اضافه کمی کد جاوا اسکریپت (تو این مقاله از جی کوئری استفاده ای نکردم و فقط جاوا اسکریپته خامه (وانیلا جی اس)

کد جاوا اسکریپت ما کارش اینه که اگر شما رو دکمه enter تو صفحه html زدید یا دکمه enter کیبوردتون رو زدید بره به لینک زیر

'localhost'+ '/chat/' + roomName + '/'

وقتی کاربر اسم گروهی که میخواد داخلش مشغول چت کردن بشه رو وارد کرد ما به url بعدی میریم 

path('<str:room_name>/', views.room, name='room')

متد این ویو هم برابره با :

from django.shortcuts import render
from django.utils.safestring import mark_safe
import json


def room(request, room_name):
    return render(request, 'chat/room.html', {
        'room_name': room_name,
        'username': mark_safe(json.dumps(request.user.username))
    })

در این متد ما اسم گروه و نام کاربری کاربر رو صفحه html خودمون میفرستیم. 

صفحه html.room به شکل زیره:

{% extends 'layouts/master.html' %}
{% load static %}
{% load render_partial %}

{% block title %}
    ابرا
{% endblock %}

{% block content %}

    <ul id="chat-log" style="
    height: 15rem;
     background: #b3b1b3;
    margin-top: 15rem;
     max-height: 15rem;
     overflow: scroll;
    overflow-x: hidden;
">

    </ul>
    <br>
    <input id="chat-message-input" class="" type="text" size="100"><br>
    <input id="chat-message-submit" type="button" value="Send">
    <input id="inp" type='file'>
    {{ room_name|json_script:"room-name" }}


    <script>
        const roomName = JSON.parse(document.getElementById('room-name').textContent);
        let username = {{ username }};


        let chat_log = document.querySelector('#chat-log')

        const chatSocket = new WebSocket(
            'ws://'
            + window.location.host
            + '/ws/chat/'
            + roomName
            + '/'
        );

        chatSocket.onopen = (e) => {
            chatSocket.send(JSON.stringify({
                'command': "old_msg"
            }));

        }

        chatSocket.onmessage = function (e) {

            const data = JSON.parse(e.data);
            console.log(data)


            if (data['command'] === "old_msg") {

                for (let i = data['message'].length - 1; i >= 0; i--) {
                    create(data['message'][i]);
                }
            } else {

                create(data['message'])
            }

        };

        chatSocket.onclose = function (e) {
            console.error('Chat socket closed unexpectedly');
        };

        document.querySelector('#chat-message-input').focus();
        document.querySelector('#chat-message-input').onkeyup = function (e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#chat-message-submit').click();
            }
        };

        document.querySelector('#chat-message-submit').onclick = function (e) {
            const messageInputDom = document.querySelector('#chat-message-input');
            const message = messageInputDom.value;
            chatSocket.send(JSON.stringify({
                'message': message,
                'command': "new_msg",
                'username': {{ username }}
            }));
            messageInputDom.value = '';
        };


        function create(data) {
            let msgListTag = document.createElement('li');
            msgListTag.classList.add('list-group-item','m-3')

            if (data['command'] === "img") {
                let imgTag = document.createElement('img')
                imgTag.style.cssText = "height: 200px; width: 200px;"
                imgTag.src = data['message']
                msgListTag.appendChild(imgTag)
            } else {
                let pTag = document.createElement('p')
                pTag.textContent = data.text
                msgListTag.appendChild(pTag)
            }
            chat_log.appendChild(msgListTag);
           


        }

        function readFile() {
            if (this.files && this.files[0]) {
                let FR = new FileReader()

                FR.addEventListener("load", function (e) {

                    chatSocket.send(JSON.stringify({
                        'message': e.target.result,
                        'command': "img",
                        'username': {{ username }}
                    }));

                });
                FR.readAsDataURL(this.files[0]);

            }
        }

        document.getElementById("inp").addEventListener("change", readFile)
    </script>

{% endblock %}

 

window.location.pathname میاد و همون ادرس سایت رو میگیره که اینجا چون پروژه روی local هست برابر میشه با localhost

به اسلش '/'   اخر مسیر دهی توجه کنید چون در پروتوکل http و https اگر شما اسلش اخر رو نزارید مهم نیست و خودش تشخیص میده ولی در اینجا ما قرارهر به پروتکل دیگه ای بریم ws که نمیتونه این مورد رو خودش تشخیص بده و حتما لازمه که ما بک اسلش اخر رو قرار بدیم.

برای اینکه بتونید به صفحه مورد نظر خودتون در پروتکل http برید لازمه در فایل url مسیرتون رو بنویسید و برای اینکه بتونید مسیر چت سوکت خودتون رو بنویسید لازمه روت خودتون رو بنویسید پس در مسیر اپی که ایجاد کردید (برای من chats) یک فایل با نام routing.py ایجاد کنید و به شکل زیر اقدام به نوشتم مسیر خودتون کنید.

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]

در مسیر دهی سوکت باید از regix استفاده کنیم پس بجای  path باید از re_path استفاده کنید.

در این ادرس ما نام room_name که همون نام گروهی هست که کاربر وارد میکنه رو میگیریم.

نکته : در مسیر دهی پروتکل http میتونید هم از فانکشن FBV هم از کلاس CBV استفاده کنید ولی در سوکت فقط باید از نوع کلاسی استفاده کنید و لازمه بعد از نوشتن نام کلاس خود از عبارت as_asgi() استفاده کنید.

حالا به یکی از قسمت های مهم برنامه میریم و بعد از اون میریم سراغ نوشتن کلاس ChatConsumer 

این بخش به ما این امکان رو میده که برای مسیر دهی مون URL ها هم از پروتکل http استفاده ببریم و هم از وب سوکت.

تغیراتی که ما در فایل wsgi انجام میدیم باعث میشه که اگر درخواست از طریق http اومد بره سراغ فایل urls.py و اگر درخواست از طرف وب سوکت بود بره سراغ فایل routing.py و که در اپ chats نوشتیم و مسیر هایی که در ارایه websocket_urlpatterns دادیم رو اجرا کنه.

 

دیگه وقتشه بریم سراغ اصلی تری بخش چت ریل تایممون که  همون کلاس ChatConsumer هست که برای پیاده سازی چتمون از کلاس WebsocketConsumer ارث بری میکنه تا بتونه از متد هاش استفاده و نسبت به نیازمون Overrid کنیم.

import json

from account.models import AccountModel
from .models import MessageModel
from .serializers import MessageSerializer
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
from rest_framework.renderers import JSONRenderer


class ChatConsumer(WebsocketConsumer):

    def new_msg(self, data):
        # print("+"*90)
        message = data.get('message', None)
        username = data['username']
        user = AccountModel.objects.filter(username=username).first()
        msg_model = MessageModel.objects.create(user=user, text=message)
        self.send_to_chat_msg(eval(self.msg_serializer(msg_model)))

    def old_msg(self, data):
        qs = MessageModel.last_message(self)
        msg_json = self.msg_serializer(qs)

        content = {
            'message': eval(msg_json),
            'command': 'old_msg'
        }

        self.chat_message(content)

    def msg_serializer(self, qs):
        # x = lambda a : a + 10
        ss = lambda query: True if(query.__class__.__name__ == "QuerySet") else False
        # print(ss(qs))
        serialized = MessageSerializer(qs, many=ss(qs))
        return JSONRenderer().render(serialized.data)

    def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = 'chat_%s' % self.room_name

        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    commands = {
        "new_msg": new_msg,
        "old_msg": old_msg
    }

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    def receive(self, text_data=None, bytes_data=None):
        text_data_json = json.loads(text_data)
        command = text_data_json['command']
        self.commands[command](self, text_data_json)

    # Send message to room group
    def send_to_chat_msg(self, msg):
        command = msg.get("command", None)
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                    'type': 'chat_message',
                    'message': msg,
                    'command': (lambda cd: "img" if(cd == 'img') else 'new_msg')(command),
            }
        )

    # Receive message from room group
    def chat_message(self, event):
        self.send(text_data=json.dumps(event))

 

خب متد های کلاس  WebsocketConsumer که ما Overrid کردیم شامل connect disconnect receive میشه (سایر متد ها رو خودمون نشوتیم و مربوط به این کلاس نمیشه) که به ترتیب توضیح میدم داخلشون چیکار می کنیم.

قبل از شروع توضیح این متد ها لازمه بگم کلاس WebsocketConsumer یک کلاس sync هست و متد های (group_add و group_discard) از نوع async  پس برای اینکه تداخلی نداشته باشن لازمه متد زیر رو ایمپورت کنیم و باهاش هر دو متد رو از نوع async به نوع sync که پشتیبانی میکنه تغییر بدیم.

from asgiref.sync import async_to_sync

متد connect

همونطور که از اسمش پیداست برای اتصال استفاده میشه و کاری که ما باهاش انجام میدیم اینکه یک گروه جدید بسازیم و کاربر رو توش عضو کنیم.

self.room_name = self.scope['url_route']['kwargs']['room_name']

با کد بالا میتونیم room_name که در url وجود داره رو بگیریم.

نکته وقتی متغیر خود را به شکل self.room_name مینویسیم این امکان رو به ما میده که بتونیم  room_name رو در سایر متد های کلاسمون براحتی دریافت کنیم.

self.room_group_name = 'chat_%s' % self.room_name

سپس برای room_group_name و با استفاده از f string مقدار chat_ رو به اول نام room_name خودمون اضافه می کنیم.

async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )
خب این کد بالا هم که کار اصلی رو انجام میده که یکی یکی بهتون توضیح میدم
async_to_sync همون متدی که async رو به synce تغیر میداد.
self.channel_layer همون لایه کانالمونه
group_add متدی که میتونیم باهاش به لایه کانالمون با استفاده از نام کانال و نام کاربر ، گروه رو اضافه کنیم.
self.room_group_name همون متغیری که باهاش عبارت chat_ رو به نام room_name که کاربر برامون ارسال کرده بود اضافه کردیم.
self.channel_name این تو یک شناسه یونیک برای کاربرمون وجود داره. وقتی کاربر وارد چت میشه اتوماتیک یک شناسه یک تا واسش ساخته میشه که در channel_name ذخیره میشه.

کد بالا میاد 

چند قدم در چند راه

ابتدا وارد شوید یا ثبت نام کنید!

برای ثبت نظر، نقد ابتدا باید وارد شوید یا ثبت نام کنید.
سبد خرید