يتعامل Django عادةً مع استجابات HTTP بشكل متزامن. ومع ذلك، تتيح قنوات Django إمكانية تطوير التطبيقات الديناميكية في الوقت الفعلي من خلال توسيع قدرات Django لدعم العمليات غير المتزامنة. يعد هذا مفيدًا بشكل خاص لإنشاء تطبيقات الدردشة، أو معالجة بيانات مستشعر إنترنت الأشياء في الوقت الفعلي، أو دمج WebSockets لأغراض مختلفة داخل Django.
يعمل Django بشكل متزامن، مما يعني أنه يتعامل مع طلبات HTTP بطريقة تسلسلية. عند تلقي الطلب، يقوم بمعالجته بالكامل قبل إرسال الرد.
في العمليات المتزامنة، لا يمكن للخادم بدء الاتصال؛ يجب أن يتلقى طلبًا لإرسال الرد.
تتيح العمليات غير المتزامنة تقديم الطلب دون انتظار الرد، مما يتيح للتطبيق الاستمرار في خدمة المستخدم وتنفيذ المهام الأخرى.
WebSockets
WebSockets هو بروتوكول يتيح الاتصال ثنائي الاتجاه بين المتصفح والخادم، على عكس بروتوكول HTTP أحادي الاتجاه. فهو يسمح للعميل بإرسال رسائل إلى الخادم وتلقي التحديثات حول الأحداث ذات الصلة بشكل مستقل، مما يسهل الاتصال المتزامن والمستقل بين الطرفين.
WebSockets هو بروتوكول ذو حالة، يحافظ على الاتصال المباشر بين العميل والخادم حتى ينهيه أحد الطرفين. بمجرد إغلاق الاتصال من قبل العميل أو الخادم، يتم إنهاؤه من كلا الطرفين.
في البداية، يرسل العميل طلب HTTP إلى الخادم، ويطلب فتح اتصال WebSocket. عند قبول الخادم، يتم إرسال استجابة “101 Switching Protocols” مرة أخرى، لاستكمال المصافحة. يظل اتصال TCP/IP الأساسي مفتوحًا، مما يسمح لكلا الجانبين بتبادل الرسائل. ويستمر هذا الاتصال حتى ينقطع اتصال أحد الأطراف، وهي عملية تُعرف باسم الاتصال ثنائي الاتجاه.
عندما يرسل العميل طلب HTTP، تتم معالجته بواسطة تطبيق Django من خلال WSGI (واجهة بوابة خادم الويب)، ويصل في النهاية إلى موجه URL الخاص بـ Django ويتم توجيهه إلى العرض المناسب.
بالنسبة لاتصالات WebSocket، تتولى ASGI (واجهة بوابة الخادم غير المتزامنة) المسؤولية عن WSGI، حيث توجه الاتصال إلى المستهلك بدلاً من العرض.
مثال 1
في هذا المثال، سنقوم بإنشاء نظام في الوقت الفعلي يقوم بتحديث عداد داخل عنصر div بتنسيق HTML. سيتم تصميم هذا التطبيق للعمل لمستخدم واحد. على الرغم من أنها ليست حالة الاستخدام الأمثل للقنوات، إلا أنها بمثابة مقدمة مباشرة.
يتضمن منطق الأعمال منشئ كلمات عشوائيًا يعرض كلمة عشوائية تم إنشاؤها في الوقت الفعلي على الصفحة دون الحاجة إلى تحديث الصفحة.
#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
في موجه الأوامر:
لكي نتمكن من استخدامه بشكل صحيح، نحتاج أيضًا إلى تثبيت 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 قيد التشغيل. تأتي البيانات الجديدة كل ثانيتين.
مثال 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 المتزامنة. طوال هذه المقالة، تم استخدام أمثلة مختلفة، تتراوح من البسيط إلى المعقد، لتوضيح الموضوع.
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.