قنوات جانغو خطوة بخطوة

يتعامل Django عادةً مع استجابات HTTP بشكل متزامن. ومع ذلك، تتيح قنوات Django إمكانية تطوير التطبيقات الديناميكية في الوقت الفعلي من خلال توسيع قدرات Django لدعم العمليات غير المتزامنة. يعد هذا مفيدًا بشكل خاص لإنشاء تطبيقات الدردشة، أو معالجة بيانات مستشعر إنترنت الأشياء في الوقت الفعلي، أو دمج WebSockets لأغراض مختلفة داخل Django.

يعمل Django بشكل متزامن، مما يعني أنه يتعامل مع طلبات HTTP بطريقة تسلسلية. عند تلقي الطلب، يقوم بمعالجته بالكامل قبل إرسال الرد.

متزامن

في العمليات المتزامنة، لا يمكن للخادم بدء الاتصال؛ يجب أن يتلقى طلبًا لإرسال الرد.

تتيح العمليات غير المتزامنة تقديم الطلب دون انتظار الرد، مما يتيح للتطبيق الاستمرار في خدمة المستخدم وتنفيذ المهام الأخرى.

WebSockets

WebSockets هو بروتوكول يتيح الاتصال ثنائي الاتجاه بين المتصفح والخادم، على عكس بروتوكول HTTP أحادي الاتجاه. فهو يسمح للعميل بإرسال رسائل إلى الخادم وتلقي التحديثات حول الأحداث ذات الصلة بشكل مستقل، مما يسهل الاتصال المتزامن والمستقل بين الطرفين.

رسم تخطيطي يصف الاتصال باستخدام WebSocket

WebSockets هو بروتوكول ذو حالة، يحافظ على الاتصال المباشر بين العميل والخادم حتى ينهيه أحد الطرفين. بمجرد إغلاق الاتصال من قبل العميل أو الخادم، يتم إنهاؤه من كلا الطرفين.

اتصال Websocket

في البداية، يرسل العميل طلب HTTP إلى الخادم، ويطلب فتح اتصال WebSocket. عند قبول الخادم، يتم إرسال استجابة “101 Switching Protocols” مرة أخرى، لاستكمال المصافحة. يظل اتصال TCP/IP الأساسي مفتوحًا، مما يسمح لكلا الجانبين بتبادل الرسائل. ويستمر هذا الاتصال حتى ينقطع اتصال أحد الأطراف، وهي عملية تُعرف باسم الاتصال ثنائي الاتجاه.

مقارنة HTTP وWebsockets في تطبيق Django

عندما يرسل العميل طلب HTTP، تتم معالجته بواسطة تطبيق Django من خلال WSGI (واجهة بوابة خادم الويب)، ويصل في النهاية إلى موجه URL الخاص بـ Django ويتم توجيهه إلى العرض المناسب.

بالنسبة لاتصالات WebSocket، تتولى ASGI (واجهة بوابة الخادم غير المتزامنة) المسؤولية عن WSGI، حيث توجه الاتصال إلى المستهلك بدلاً من العرض.

مثال 1

في هذا المثال، سنقوم بإنشاء نظام في الوقت الفعلي يقوم بتحديث عداد داخل عنصر div بتنسيق HTML. سيتم تصميم هذا التطبيق للعمل لمستخدم واحد. على الرغم من أنها ليست حالة الاستخدام الأمثل للقنوات، إلا أنها بمثابة مقدمة مباشرة.

مثال 1

يتضمن منطق الأعمال منشئ كلمات عشوائيًا يعرض كلمة عشوائية تم إنشاؤها في الوقت الفعلي على الصفحة دون الحاجة إلى تحديث الصفحة.

#business.py

import json
#pip install random_word
from random_word import RandomWords

class Domain:

    def __init__(self):
        self.R = RandomWords()

    def do(self):
        word = self.R.get_random_word()
        return json.dumps({"message": word})
#urls.py

from django.urls import path
from one.views import one

urlpatterns = [
    path('one/', one),
]
#views.py
from django.shortcuts import render

def one(request):
    return render(request, './templates/one.html', context={'one_text': "ASD"})

لدينا صفحة واضحة مصممة لعرض الكلمة التي تم تمريرها إليها.

{% include 'base.html' %} {% block content%} {% load static %}
<div class="container">
  <p id="one">{{ one_text }}</p>
</div>

<script>
  var socket = new WebSocket("ws://localhost:8000/ws/any_url/");
  socket.onmessage = function (event) {
    var data = JSON.parse(event.data);
    console.log(data);
    document.querySelector("#one").innerText = data.message;
  };
</script>
{% endblock %}

تذكر تضمين القنوات وتطبيقاتك في إعداد “INSTALLED_APPS”.

#settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'channels',
    'one',
]

لاستيعاب WebSockets، يجب علينا تحديث ملف إعداد ASGI. نحدد متغير التطبيق الخاص بنا على أنه ProtocolTypeRouter، الذي يحدد نوع الاتصال والبروتوكول. إذا كان نوع البروتوكول متطابقًا، فسيتم توجيه الاتصال إلى AuthMiddlewareStack للتحقق من مصادقة المستخدم. وأخيرًا، تم دمج URLRouter، الذي يستقبل توجيهات المستهلك، في حزمة البرامج الوسيطة.

#asgi.py

import os

from django.core.asgi import get_asgi_application

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

from one.routing import ws_urlpatterns

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'examplechannels.settings')

application = ProtocolTypeRouter(
    {
        'http': get_asgi_application(),
        'websocket': AuthMiddlewareStack(URLRouter(ws_urlpatterns)),
    }
)

لاستخدام ASGI، قم بتعريفه في ملف settings.py (ما عليك سوى نسخ متغير WSGI_APPLICATION الافتراضي وتحريره).

#settings.py

WSGI_APPLICATION = 'examplechannels.wsgi.application'

ASGI_APPLICATION = 'examplechannels.asgi.application'

لإنشاء مستهلك (مشابه لعرض WebSockets)، نحتاج إلى تجاوز التابع connect للفئة الأصلية WebsocketConsumer.

في هذا المثال، سيقوم المستهلك باسترداد البيانات من كائن المجال مائة مرة، مع التوقف لمدة ثانيتين بين كل عملية استرجاع.

import time
from channels.generic.websocket import WebsocketConsumer
from one.bussiness import Domain

class OneConsumer(WebsocketConsumer):
    
    def connect(self):
        self.accept()
        D = Domain()
        for i in range(100):
            data = D.do()
            self.send(data)
            time.sleep(2)

مثلما نقوم بإنشاء عناوين URL للعرض، نحتاج إلى إنشاء توجيه للمستهلكين.

#routing.py

from django.urls import path
from one.consumers import OneConsumer

ws_urlpatterns = [
    path('ws/any_url/', OneConsumer.as_asgi())
]

لنبدأ الخادم ببساطة عن طريق الأمر: python manager.py runserver

لنذهب إلى صفحة: http://127.0.0.1:8000/one/

دعونا نتحقق من وحدة التحكم في المتصفح:

وحدة التحكم في صفحة تطبيق

يتم إرسال كائن يحتوي على كلمة كل ثانيتين.

مثال 2

هذه المرة، سنقوم بتحديث الرسم البياني في الوقت الفعلي، لمحاكاة آلية إنترنت الأشياء. تم إنشاء تطبيق جديد يسمى “اثنين”.

سنستخدم Chart.js لتصور بيانات إنترنت الأشياء الخاصة بنا، مع دمج علامة canvas في HTML. لاستخدام Chart.js، سنقوم بتضمين CDN الخاص به. بالإضافة إلى ذلك، لدينا ملف جافا سكريبت منفصل يسمى two.js، والذي سيدير عمليات الرسم البياني.

{% include 'base.html' %} {% block content%} {% load static %}
<link rel="stylesheet" href="{% static 'css/one-style.css' %}?{% now 'U' %}" />

<div class="container">
  <div class="chart">
    <canvas id="iot-chart" width="800" height="400"></canvas>
  </div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.4.0/Chart.min.js"></script>
<script src="{% static 'js/two.js' %}"></script>
{% endblock %}

تم تصميم فئة المجال الخاصة بنا لإرجاع عدد صحيح عشوائي.

#business.py

import json
import random

class DomainTwo:

    def do(self):
        value = random.randint(0,100)
        return json.dumps({'data': value})

لدينا توجيه وعناوين URL و عرض مماثل:

#routing.py

from django.urls import path
from two.consumers import TwoConsumer

ws_urlpatterns = [
    path('ws/two_url/', TwoConsumer.as_asgi())
]
#urls.py

from django.urls import path
from two.views import two

urlpatterns = [
    path('two/', two),
]

#views.py

from django.shortcuts import render

def two(request):
    return render(request, './templates/two.html')

هذه المرة، نقوم بإعداد قناة غير متزامنة باستخدام AsyncWebsocketConsumer جنبًا إلى جنب مع بنية البرمجة غير المتزامنة.

#consumers.py

import time
from channels.generic.websocket import AsyncWebsocketConsumer
from asyncio import sleep
from two.business import DomainTwo

class TwoConsumer(AsyncWebsocketConsumer):
    
    async def connect(self):
        await self.accept()
        D = DomainTwo()
        for i in range(100):
            data = D.do()
            await self.send(data)
            await sleep(2)

للتأكد من أن التطبيق يعمل بشكل غير متزامن، يجب أن يتلقى كل عميل مثيلًا جديدًا للمستهلك. يتم تحقيق ذلك باستخدام طبقات القناة، وهي هياكل بيانات FIFO (الوارد أولاً، يخرج أولاً) المصممة لوضع الرسائل المستلمة من العملاء في قائمة الانتظار. لتنفيذ ذلك، نحتاج إلى تثبيت Redis، وهو مخزن بيانات في الذاكرة.

بعد اكتمال التثبيت، اكتب redis-server في موجه الأوامر:

Run redis-server

لكي نتمكن من استخدامه بشكل صحيح، نحتاج أيضًا إلى تثبيت Channels-redis:

pip install channels-redis

لإعداد طبقات القناة، حددها في ملف الإعدادات الخاص بمشروع Django الخاص بك.

#settings.py

CHANNEL_LAYERS = {
    'default' : {
        'BACKEND' : 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts':[('127.0.0.1', 6739)]
        }
    }
}

6739 هو المنفذ الذي يستخدمه خادم Redis.

عندما نقوم بتشغيل الخادم والانتقال إلى عنوان URL للتطبيق الثاني:

System check identified no issues (0 silenced).
November 28, 2022 - 21:07:30
Django version 4.0, using settings 'examplechannels.settings'
Starting ASGI/Channels version 3.0.4 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
HTTP GET /two/ 200 [0.03, 127.0.0.1:51340]
HTTP GET /static/css/one-style.css?1669669655 200 [0.01, 127.0.0.1:51340]
HTTP GET /static/js/two.js 200 [0.02, 127.0.0.1:51341]
WebSocket HANDSHAKING /ws/two_url/ [127.0.0.1:51343]
WebSocket CONNECT /ws/two_url/ [127.0.0.1:51343]

WebSocket قيد التشغيل. تأتي البيانات الجديدة كل ثانيتين.

Chart
Console

مثال 3

هذه المرة دعونا نبني تطبيق دردشة.

صفحة غرف الدردشة

في هذا المثال، سنقوم بإنشاء مجموعات مستخدمين لتنظيم المستخدمين في غرف الدردشة. يوضح هذا الأسلوب المستخدمين الذين سيتلقون الرسائل داخل غرفة محادثة معينة.

نقل الرسالة

لدينا صفحة رئيسية لتطبيق الدردشة وصفحات فردية لكل غرفة دردشة.

#urls.py

from django.urls import path
from threechat.views import chat, room

urlpatterns = [
    path('chat/', chat, name='chat_index'),
    path('chat/<str:room_name>/', room, name="chat_room"),
]
#views.py

from django.shortcuts import render

# Create your views here.
def chat(request):
    return render(request, './templates/threechat.html', context={})

def room(request, room_name):
    return render(request, './templates/threeroom.html', context={'room_name': room_name})

نستخدم w+ في re_path لمطابقة أي تسلسل من الأحرف يتبع “chat/”، مما يضمن التعرف عليه وتمريره إلى المستهلك.

#routing.py

from django.urls import re_path
from threechat.consumers import RoomConsumer

ws_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', RoomConsumer.as_asgi())
]
{% include 'base.html' %} {% load static %} {% block content%}
<link rel="stylesheet" href="{% static 'css/chat-style.css' %}?{% now 'U' %}" />
<div>
  <div class="container">
    <div class="row d-flex justify-content-center">
      <div class="col-6">
        <form>
          <div class="form-group">
            <label for="textarea1" class="h4 pt-5"
              >Chatroom - {{room_name}}</label
            >
            <textarea class="form-control" id="chat-text" rows="10"></textarea>
          </div>
          <div class="form-group">
            <input class="form-control" id="input" type="text" /><br />
          </div>
          <input
            class="btn btn-success btn-lg btn-block"
            id="submit"
            type="button"
            value="Send"
          />
        </form>
      </div>
    </div>
  </div>
</div>
{{room_name|json_script:"room-name"}}
{{request.user.username|json_script:"username"}}
<script>
  const userName = JSON.parse(document.getElementById("username").textContent);
  const roomName = JSON.parse(document.getElementById("room-name").textContent);
  document.querySelector("#submit").onclick = function (e) {
    const msgInput = document.querySelector("#input");
    const message = msgInput.value;
    chatSocket.send(JSON.stringify({ message: message, username: userName }));
    msgInput.value = "";
  };

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

  chatSocket.onmessage = function (event) {
    const data = JSON.parse(event.data);
    document.querySelector("#chat-text").value +=
      data.username + ": " + data.message + "\n";
  };
</script>
<script
  src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
  integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
  crossorigin="anonymous"
></script>
<script
  src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
  integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
  crossorigin="anonymous"
></script>
<script
  src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"
  integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV"
  crossorigin="anonymous"
></script>
{% endblock %}

لنقل البيانات من جانب Django إلى جانب JavaScript، نستخدم الترميز التالي لتمرير Room_name من التوجيه (w+) و username من الطلب:

نقوم بتحليل البيانات الواردة من Django، مثل اسم المستخدم، وإرسالها مع رسالة إدخال المستخدم من خلال WebSocket.

نحدد WebSocket، كما في الأمثلة السابقة. عند تلقي رسالة، يقوم بتحليل البيانات وعرضها على الشاشة.

نحن نحدد المستهلك غير المتزامن.

#consumers.py

import json
from channels.generic.websocket import AsyncWebsocketConsumer

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

        await self.channel_layer.group_add(self.room_group_name, self.channel_name)

        await self.accept()

    async def disconnect(self, code):
        await self.channel_layer.group_discard(self.room_group_name, self.channel_name)

    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        username = text_data_json['username']

        await self.channel_layer.group_send(self.room_group_name, {'type':'chatroom_message','message':message, 'username':username})
    
    async def chatroom_message(self, event):
        message = event['message']
        username = event['username']
        await self.send(text_data=json.dumps({'message':message, 'username':username}))

أولاً نتجاوز طريقة connect.

لملاحظة ما تم تضمينه فيه، دعونا نطبع self.scope.

{'type': 'websocket', 'path': '/ws/chat/asd/', 'raw_path': b'/ws/chat/asd/', 
'headers': [(b'host', b'127.0.0.1:8000'), (b'pragma', b'no-cache'), 
            (b'accept', b'*/*'), (b'sec-websocket-key', 
             (b'DtE1JGLUF0e8X2DLld6l6g=='), (b'sec-websocket-version', b'13'), 
             (b'accept-language', b'en-US,en;q=0.9'), 
(b'sec-websocket-extensions', b'permessage-deflate'), 
(b'cache-control', b'no-cache'), (b'accept-encoding', b'gzip, deflate'), 
(b'origin', b'http://127.0.0.1:8000'), 
(b'user-agent', 
b'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15'), (b'connection', b'Upgrade'), 
(b'upgrade', b'websocket'), (b'cookie', b'csrftoken=qNTynYDGkiIdYsAZEHlqZwuqv98D3EBMZopHw87eOENNOavGczQyX286og7GFCHQ; sessionid=nwxc4zfv1lnq515wdqhxzcqfzln7bev6')], 
'query_string': b'', 
'client': ['127.0.0.1', 49417], 
'server': ['127.0.0.1', 8000], 
'subprotocols': [], 'asgi': {'version': '3.0'}, 
'cookies': {'csrftoken': 'qNTynYDGkiIdYsAZEHlqZwuqv98D3EBMZopHw87eOENNOavGczQyX286og7GFCHQ', 'sessionid': 'nwxc4zfv1lnq515wdqhxzcqfzln7bev6'}, 
'session': <django.utils.functional.LazyObject object at 0x7f9a68617d00>, 
'user': <channels.auth.UserLazyObject object at 0x7f9a483da370>, 
'path_remaining': '', 'url_route': {'args': (), 
'kwargs': {'room_name': 'asd'}}}

في النهاية، نستخدم kwargs للحصول على اسم غرفة الدردشة التي أدخلها المستخدم. من خلال البادئة بـ “chat_”، نقوم بإنشاء مجموعة وإضافة مستخدمين إليها داخل طبقات القناة. يسمح هذا الإعداد بغرف محادثة مميزة حيث تتم مشاركة الرسائل بين المشاركين في كل غرفة.

بعد انضمام المستخدمين إلى غرفهم الخاصة، يتم قبول الطلب واستكمال المصافحة.

بالإضافة إلى ذلك، يمكننا تجاوز التابع disconnect لإزالة مجموعة الدردشة من طبقات القناة عند قطع الاتصال.

إلى جانب الخطوات المذكورة أعلاه، نحتاج أيضًا إلى تجاوز التابع  receive.

يتلقى معلمة باسم text_data.

text_data:  {"message":"adsasd","username":"admin"}

ضع في اعتبارك أن هذه البيانات يتم إرسالها من JavaScript.

chatSocket.send(JSON.stringify({ message: message, username: userName }));

نقوم بتحليل البيانات المستلمة وإرسالها إلى غرفة الدردشة. من خلال تعيين النوع على أنه chatroom_message، فإننا نضمن إنشاء طريقة بالاسم المقابل. تعالج هذه الطريقة تسلسل الرسالة واسم المستخدم قبل الإرسال.

دعونا نحاول ذلك الآن. python manage.py runserver

http://127.0.0.1:8000/chat/room1/

الرسالة الأولى:

سيؤدي فتح علامة تبويب جديدة بنفس العنوان وإدخال رسالة إلى إظهار الاتصال في الوقت الفعلي بين العملاء المتصلين بنفس غرفة الدردشة.

عند العودة إلى علامة التبويب الأولى، ستلاحظ الرسالة المرسلة من علامة التبويب الثانية دون الحاجة إلى تحديث الصفحة.

تتيح قنوات Django إمكانية تطوير التطبيقات غير المتزامنة في الوقت الفعلي، مما يوفر وسيلة لتجاوز قيود Django على طلبات HTTP المتزامنة. طوال هذه المقالة، تم استخدام أمثلة مختلفة، تتراوح من البسيط إلى المعقد، لتوضيح الموضوع.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

اكتشاف المزيد من بايثون العربي

اشترك الآن للاستمرار في القراءة والحصول على حق الوصول إلى الأرشيف الكامل.

Continue reading

Scroll to Top