Asosiy kontentga o‘tish

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.

database django backend

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#

Terminal
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):

Fayl tuzilishi
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
Python
# 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:

YAML
# docker-compose.yml (services: ichiga, db yoniga)
  redis:
    image: redis:7
    ports:
      - "6379:6379"
Terminal
docker compose up -d
Python
# 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#

Python
# 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:

Python
# 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"])
Python
# 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"]
Python
# 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:

Python
# 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.

Python
# 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):

Terminal
uv run python manage.py makemigrations alerts
uv run python manage.py migrate
uv run python manage.py createsuperuser

Sinov — ikkita terminal:

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:

Python
# 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:

Terminal
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-worker va -k uvicorn_worker.UvicornWorker. Eski uvicorn.workers.UvicornWorker eskirgan deb belgilangan;

  • Nginx: WebSocket ulanishi Upgrade sarlavhalari bilan o'tadi — proxy_http_version 1.1;, proxy_set_header Upgrade $http_upgrade; va proxy_set_header Connection "upgrade";. Birinchi qatorni unutmang: nginx backend'ga standart holatda HTTP/1.0 bilan boradi, unda esa Upgrade yo'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.

15 daqiqa 248 marta o‘qildi