Asosiy kontentga o‘tish

Maqolalar

Maqola · 2026-08-16 · ~12 daqiqa o‘qiladi · murakkab

Django Channels: ilova ochiq turgan foydalanuvchiga xabar bir soniyada

#django #channels #websocket #redis #real-time

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 channels-redis uvicorn

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
├── 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)]},
    }
}
# 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 nuqta/pastki chiziq -> metod nomi
    async def alert_created(self, event):
        await self.send_json(event["payload"])
# 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
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


@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
    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,
            },
        },
    )
# 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

Sinov — ikkita terminal:

# 1-terminal: ASGI server
python manage.py runserver

# 2-terminal: WebSocket mijoz
python -m websockets ws://127.0.0.1:8000/ws/alerts/
# endi 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_asyncawait 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. Nosinxronlik ("push keldi, ilovada ko'rinmadi") sinfan yo'qoladi — 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()
uv add --dev pytest-asyncio
uv run pytest apps/alerts/ -q

Production eslatmalari#

  • ASGI server: uvicorn config.asgi:application --workers 2 (yoki daphne). Gunicorn'ning oddiy worker'i WebSocket'ni tushunmaydi — uvicorn.workers.UvicornWorker bilan ishlatiladi;
  • Nginx: WebSocket ulanishi Upgrade sarlavhalari bilan o'tadi — proxy_set_header Upgrade $http_upgrade; va proxy_set_header Connection "upgrade"; (DevOps seriyasida Nginx'ni to'liq sozlaymiz);
  • Redis majburiy: worker ikkitadan oshdi — InMemory layer yaramaydi, xabar faqat "o'z" worker'idagi mijozlarga borib qoladi. Bu bug juda ayyor: lokalda (1 worker) 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.