Maqolalar 15 daqiqa 248 ko‘rish
Django Channels: ilova ochiq turgan foydalanuvchiga xabar bir soniyada
Metro liniyasida uzilish bo'ldi — buni ilovasi ochiq turgan minglab foydalanuvchiga qanday darhol yetkazamiz? Django Channels, WebSocket, Redis channel layer va group broadcast — production'da sinalgan to'liq yo'l.
Mundarija
Birinchi qismda stansiyalarni xaritaga qo'ydik. Endi jiddiyroq talab: liniyada uzilish bo'lsa, operator ogohlantirish kiritadi — va bu xabar ilovasi ochiq turgan har bir foydalanuvchiga bir soniya ichida yetib borishi kerak. HTTP bunga qodir emas: u so'rov-javob protokoli, server o'zi birinchi bo'lib gapira olmaydi.
HTTP'ning chegarasi va WebSocket#
Uch klassik yechim bor:
| Usul | Qanday ishlaydi | Kamchilik |
|---|---|---|
| Polling | Mijoz har 10 soniyada "yangilik bormi?" deb so'raydi | 10 soniyagacha kechikish, behuda so'rovlar oqimi |
| Long polling | So'rov javob kelguncha ochiq turadi | Ulanishlar band, murakkab timeout logikasi |
| WebSocket | Bitta doimiy ikki tomonlama ulanish | Server tomonda maxsus infratuzilma kerak |
Django'ning klassik qismi WSGI ustida ishlaydi — bitta so'rov, bitta javob, ulanish yopildi. WebSocket'ka esa doimiy ochiq ulanish kerak. Shu yerda Django Channels kiradi: u Django'ni ASGI (asinxron interfeys) ustiga o'tkazib, HTTP bilan yonma-yon WebSocket'ni ham boshqarishga imkon beradi.
ASGI nima
WSGI — "bitta so'rov keldi → bitta javob qaytdi" sinxron shartnoma. ASGI — uning avlodi: ulanish hodisalar oqimi sifatida ko'riladi (ulandi, xabar keldi, uzildi...), shuning uchun WebSocket kabi uzun umrli protokollarga mos. Django 3.0+ ASGI'ni tabiiy qo'llaydi.
1-qadam: o'rnatish va ASGI konfiguratsiya#
uv add "channels[daphne]" channels-redis "uvicorn[standard]"
Uchta paketning har biri o'z ishi uchun: channels[daphne] — Channels'ning
o'zi va lokal runserverni ASGI rejimga o'tkazadigan daphne (pastda
INSTALLED_APPSga qo'shamiz; usiz No module named 'daphne' chiqadi),
channels-redis — Redis channel layer, uvicorn[standard] — production
ASGI server. [standard] qo'shimchasini tashlab ketmang: usiz uvicorn'da
WebSocket kutubxonasi bo'lmaydi va har bir ulanishga 404 qaytaradi.
Fayl tuzilishi (1-qismdagi skeletga alerts moduli qo'shildi):
config/
├── settings.py
├── urls.py
└── asgi.py ← WebSocket marshrutlash shu yerda
apps/alerts/
├── __init__.py
├── models.py ← Alert modeli
├── admin.py ← operator ogohlantirishni shu yerdan kiritadi
├── consumers.py ← WebSocket "view"lari
├── routing.py ← WebSocket URL'lari
├── signals.py ← saqlanganda broadcast
├── apps.py
└── tests.py
# config/settings.py
INSTALLED_APPS = [
"daphne", # runserver'ni ASGI rejimga o'tkazadi — ro'yxat BOSHIDA tursin
# ...
"channels",
"apps.alerts",
]
ASGI_APPLICATION = "config.asgi.application"
# Channel layer — jarayonlar orasidagi "pochta": worker'lar ko'p bo'lsa ham
# xabar hammaga yetadi. Production'da doim Redis, testda InMemory.
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {"hosts": [("127.0.0.1", 6379)]},
}
}
Lokal ishlab chiqishda ham Redis kerak. 1-qismdagi
docker-compose.yml ga ikkinchi servis qo'shamiz:
# docker-compose.yml (services: ichiga, db yoniga)
redis:
image: redis:7
ports:
- "6379:6379"
docker compose up -d
# config/asgi.py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
# MUHIM: get_asgi_application() import'lardan OLDIN chaqiriladi —
# aks holda "Apps aren't loaded yet" xatosi chiqadi
django_asgi_app = get_asgi_application()
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
from apps.alerts.routing import websocket_urlpatterns # noqa: E402
application = ProtocolTypeRouter(
{
"http": django_asgi_app, # oddiy Django o'z yo'lida davom etadi
"websocket": URLRouter(websocket_urlpatterns),
}
)
Import tartibi — jim sinadigan joy
get_asgi_application() chaqiruvi consumer import'laridan oldin turishi
shart: u Django app registry'ni yuklaydi. Tartibni buzsangiz xato ba'zan
faqat production ASGI serverda ko'rinadi — lokalda esa hammasi "ishlab
turganday" bo'ladi.
2-qadam: model va consumer#
# apps/alerts/models.py
from django.db import models
class Alert(models.Model):
class Level(models.TextChoices):
INFO = "info", "Ma'lumot"
WARNING = "warning", "Ogohlantirish"
CRITICAL = "critical", "Jiddiy"
title = models.CharField(max_length=200)
body = models.TextField(blank=True)
level = models.CharField(max_length=8, choices=Level.choices, default=Level.INFO)
line = models.ForeignKey(
"metro.Line",
null=True,
blank=True,
on_delete=models.CASCADE,
help_text="Bo'sh qolsa — butun tarmoq bo'yicha ogohlantirish",
)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
Consumer — WebSocket dunyosidagi "view". Bizniki juda sodda: ulangan har kim
alerts guruhiga qo'shiladi va guruhga kelgan har xabarni oladi:
# apps/alerts/consumers.py
from channels.generic.websocket import AsyncJsonWebsocketConsumer
ALERTS_GROUP = "alerts"
class AlertsConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
# har ulanishning o'z kanali (self.channel_name) bor;
# uni umumiy guruhga qo'shamiz — broadcast shu guruh orqali
await self.channel_layer.group_add(ALERTS_GROUP, self.channel_name)
await self.accept()
async def disconnect(self, code):
await self.channel_layer.group_discard(ALERTS_GROUP, self.channel_name)
# group_send'dagi {"type": "alert_created"} shu metodga keladi;
# type'dagi nuqtalar pastki chiziqqa aylanadi ("alert.created" ham shu yerga)
async def alert_created(self, event):
await self.send_json(event["payload"])
# apps/alerts/admin.py
from django.contrib import admin
from .models import Alert
@admin.register(Alert)
class AlertAdmin(admin.ModelAdmin):
list_display = ["title", "level", "line", "is_active", "created_at"]
list_filter = ["level", "is_active"]
# apps/alerts/routing.py
from django.urls import re_path
from .consumers import AlertsConsumer
websocket_urlpatterns = [
re_path(r"^ws/alerts/$", AlertsConsumer.as_asgi()),
]
3-qadam: yozilgan ogohlantirishni tarqatish#
Operator admin'da Alert saqlaydi — signal uni guruhga uzatadi:
# apps/alerts/signals.py
import logging
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.db.models.signals import post_save
from django.dispatch import receiver
from .consumers import ALERTS_GROUP
from .models import Alert
logger = logging.getLogger(__name__)
@receiver(post_save, sender=Alert)
def broadcast_alert(sender, instance: Alert, created: bool, **kwargs):
if not created or not instance.is_active:
return
channel_layer = get_channel_layer()
# signal sinxron kontekstda ishlaydi, channel layer esa async —
# ko'prik: async_to_sync
try:
async_to_sync(channel_layer.group_send)(
ALERTS_GROUP,
{
"type": "alert_created", # -> AlertsConsumer.alert_created
"payload": {
"id": instance.id,
"title": instance.title,
"body": instance.body,
"level": instance.level,
"line_id": instance.line_id,
},
},
)
except Exception:
# Redis yotgan bo'lsa ham Alert saqlanishi va boshqa kanallar ishlashi kerak
logger.exception("Alert #%s WebSocket orqali tarqatilmadi", instance.id)
try/except bu yerda ehtiyotkorlik emas, zarurat. Signal Alert bazaga
yozilgandan keyin ishlaydi. Redis javob bermasa, group_send xato otadi:
yozuv saqlangan bo'ladi, lekin operator admin'da 500 xato ko'radi, "saqlanmadi"
deb qayta bosadi va ikkinchi nusxa paydo bo'ladi. Bundan tashqari Django
signal'ning keyingi receiver'larini ham chaqirmay qo'yadi — seriyaning
keyingi qismida shu signalga push-bildirishnoma ulaymiz, va Redis yotgan
kuni u ham jim qolardi.
# apps/alerts/apps.py
from django.apps import AppConfig
class AlertsConfig(AppConfig):
name = "apps.alerts"
def ready(self):
from . import signals # noqa: F401 — signal ulanishi uchun import shart
Yangi model uchun migratsiya va admin'ga kirish uchun foydalanuvchi
(1-qismda yaratgan bo'lsangiz, createsuperuser shart emas):
uv run python manage.py makemigrations alerts
uv run python manage.py migrate
uv run python manage.py createsuperuser
Sinov — ikkita terminal:
# 1-terminal: ASGI server
uv run python manage.py runserver
# "Starting ASGI/Daphne ... development server" chiqishi kerak —
# bu daphne ishlayotganini bildiradi
# 2-terminal: WebSocket mijoz (websockets kutubxonasi uvicorn[standard] bilan keldi)
uv run python -m websockets ws://127.0.0.1:8000/ws/alerts/
# endi http://127.0.0.1:8000/admin/ dan Alert qo'shing — JSON shu yerga bir zumda tushadi
Async consumer ichida ORM
AsyncJsonWebsocketConsumer ichida to'g'ridan-to'g'ri
Alert.objects.filter(...) chaqirish — SynchronousOnlyOperation
xatosi (yoki undan yomoni: event loop'ni bloklab, HAMMA ulanishlarni
to'xtatib qo'yish). ORM kerak bo'lsa o'rang:
from channels.db import database_sync_to_async →
await database_sync_to_async(list)(Alert.objects.filter(is_active=True)).
Nega "bir manba, ikki kanal"#
Metro'da ogohlantirish ikki auditoriyaga boradi: ilovasi ochiq turganlarga
WebSocket bilan (shu maqola) va ilovasi yopiq turganlarga push-bildirishnoma
bilan (seriyaning keyingi qismi). Ikkalasini alohida yozish o'rniga manba bitta:
Alert saqlanadi → signal ikkala kanalga birdan tarqatadi. Shu tufayli
"push keldi, lekin ilovada ko'rinmadi" degan nomuvofiqlik umuman kelib
chiqmaydi — chunki ikkala kanal ham bitta yozuvdan o'qiydi.
Test#
Channels test uchun maxsus WebsocketCommunicator beradi — haqiqiy server
ko'tarmasdan consumer bilan gaplashamiz:
# apps/alerts/tests.py
import pytest
from channels.layers import get_channel_layer
from channels.testing import WebsocketCommunicator
from apps.alerts.consumers import ALERTS_GROUP, AlertsConsumer
@pytest.mark.asyncio
async def test_connected_client_receives_group_broadcast(settings):
# testda Redis shart emas — xotiradagi layer yetadi
settings.CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
communicator = WebsocketCommunicator(AlertsConsumer.as_asgi(), "/ws/alerts/")
connected, _ = await communicator.connect()
assert connected
channel_layer = get_channel_layer()
await channel_layer.group_send(
ALERTS_GROUP,
{"type": "alert_created", "payload": {"title": "Chilonzor: uzilish"}},
)
message = await communicator.receive_json_from()
assert message["title"] == "Chilonzor: uzilish"
await communicator.disconnect()
pytest va pytest-django 1-qismda sozlangan; async test uchun bitta plagin qo'shamiz:
uv add --dev pytest-asyncio
uv run pytest apps/alerts/ -q
Test Redis'siz ishlaydi — ishga tushirishdan oldin docker compose stop redis
qilib ko'rsangiz ham o'tadi.
Production eslatmalari#
-
ASGI server:
uvicorn config.asgi:application --workers 2(yoki daphne). Gunicorn'ning oddiy worker'i WebSocket'ni tushunmaydi. Gunicorn bilan ishlatmoqchi bo'lsangiz,uv add gunicorn uvicorn-workerva-k uvicorn_worker.UvicornWorker. Eskiuvicorn.workers.UvicornWorkereskirgan deb belgilangan; -
Nginx: WebSocket ulanishi
Upgradesarlavhalari bilan o'tadi —proxy_http_version 1.1;,proxy_set_header Upgrade $http_upgrade;vaproxy_set_header Connection "upgrade";. Birinchi qatorni unutmang: nginx backend'ga standart holatda HTTP/1.0 bilan boradi, unda esaUpgradeyo'q. To'liq konfiguratsiya — DevOps seriyasining nginx qismida; -
Redis majburiy: worker bittadan ko'p bo'lsa, InMemory layer yaramaydi — xabar faqat "o'z" worker'idagi mijozlarga borib qoladi. Bu xatoni topish qiyin, chunki lokalda bitta worker bilan ishlaganda u hech qachon ko'rinmaydi.
Xulosa#
- Server o'zi gapirishi kerak bo'lgan joyda HTTP tugaydi — WebSocket boshlanadi;
-
Channels arxitekturasi uch tushunchada: consumer (ulanish egasi), channel layer (jarayonlararo pochta), group (broadcast manzili);
-
Sinxron Django kodi bilan async dunyo orasida ko'prik —
async_to_sync/database_sync_to_async; ularni chetlab o'tish jim sinadigan xatolar beradi; -
Ogohlantirish bitta yoziladi, kanallarga signal tarqatadi — "bir manba, ikki kanal" printsipi.
Keyingi qismda ikkinchi kanalni quramiz: FCM push-bildirishnomalar — ilovasi yopiq foydalanuvchiga xabar yetkazish, qurilma tokenlari va mehmon-foydalanuvchilar muammosi bilan.