LoginSignup
4
1

More than 3 years have passed since last update.

ロングポーリングからの解放!DjangoのChannelsをしばく

Posted at

挨拶

書いてある内容は公式チュートリアルと同じです。公式ドキュメント以外にDjangoのChannelsを1から10まで分かりやすく説明している親切な記事がなく、どう学習したらいいのか困ったので後の走者のために書き記しておきます。

「公式ドキュメント読んでるよね?」みたいな記事しかないのもきついですし。
公式ドキュメントが前提ならそっち読むわ!みたいなね

あと記事が古くてうまく動かない、っていうのもあると思うので。

一番いい勉強法はChannel公式ドキュメントのチュートリアルから始めることですが、まぁこの記事を見つけたなら始めなくて良いです。

想定読者は「Djangoちょっと使えるけどChannelsに手が出せない」みたいな方です。

あと、先に言っておくと僕自身Djangoに詳しくないです。
とりあえず動くWebsocketアプリが作れるまでを紹介します。

Channelsの概念

Channelsは今までのDjangoにWebsocket通信を追加します。
HTTP通信はそのままに、websocket通信が来たときのみにviewsではなくconsumerを呼び出します。

次にchannelとgroup、layerという概念があります(ドキュメントとの整合性のため英語表記します)。
channel: クライアントのメールボックスみたいなもの。これに対してメッセージを送れば結果的にクライアントに届くことになる。
group: channelの集まり。チャットの部屋みたいなもの。channelsはgroupで変化があったらgroup全体に通知する、みたいな仕組みを基本として想定しているんだと思う。
layer: groupのさらなる上位概念。何のためにあるかいまいちわからない。基本的に1個で足りるらしい。

これらを前提知識として実装していきます。別に覚えなくてもニュアンスで分かりますし、分からなくても問題ないです。

準備

chanellsをインストールしましょう
python -m pip install -U channels

つぎにプロジェクトを作ります。プロジェクト名を変えた場合は読み替えてください
django-admin startproject mysite

つぎにプロジェクト内に移動して
python3 manage.py startapp chat
これもアプリ名を変えた場合は読み替えてください

まぁここまではおまじないです。最初に形だけ作っとく、みたいなもんだと思ってください。
次にchatの中身をふっとばします。以下の形になるようにファイルを消したり作ったりしてください。

chat/
    __init__.py
    consumers.py
    routing.py
    urls.py
    views.py

ファイルを作るのはマジでいつもの感じで新しいファイルを作れば大丈夫です。

結果的にプロジェクト全体はこんな感じになると思います。

chat/
    migrations
    __init__.py
    consumers.py
    routing.py
    urls.py
    views.py
mysite/
    __init__.py
    asgi.py
    settings.py
    urls.py
    wsgi.py
db.sqlite3
manage.py

これで形だけは完成です。
asgi.pyがない場合はdjangoのバージョンが間違っている場合があります。Channelsが使えるバージョンを用意してください。

次に各ファイルをいじっていきます。

mystite/settings.py

mysite/settings.py
INSTALLED_APPS = [
    'channels',#追加
    'chat',#追加
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

#一番下に追加
ASGI_APPLICATION = 'mysite.asgi.application'

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer"#インメモリを使う場合
    },
}

INSTALLED_APPS内に'channels'と'アプリ名'を追加します。まぁこれはDjangoの仕様です。
一番下に追加したのは変更の分かりやすさのためです。
レイヤーとはグループが集団のことで、公式ドキュメントによると1アプリ1レイヤーが普通らしいです。
"default"はレイヤーの名前です。

現在はインメモリを使うことにしていますがこれだと同じプロセス内でしか動作しないらしいです。
なのでRedisというキャッシュサーバーを使うことで解消できます。Redisとは共用メモリみたいなもんだと思います。

#推奨 redisというキャッシュサーバーを用いた方が良い,
CHANNEL_LAYERS = {
    'default': {
        'BACKEND':'channels_redis.core.RedisChannelLayer', 
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

この場合はポート番号6379にあるRedisサーバーをキャッシュサーバーとして使っています。

mysite/urls.py

以下のようにします。

mysite/urls.py
from django.conf.urls import include
from django.urls import path
from django.contrib import admin

urlpatterns = [
    path('chat/', include('chat.urls')),
    path('admin/', admin.site.urls),
]

includeは別のURL書いてあるファイルを使うよ~ってことです。

chat/urls.py

chat/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('<str:room_name>/', views.room, name='room'),
]

これに関してはDjangoと全く一緒です。
urlになんもない場合はviews.pyのindexを、なんかある場合はview.pyのroomを呼び出しています。その際にroom_nameとして取り出していて、views.py内ではそれを使っています。

chat/routing.py

chat/routing.py
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]

次にrouting.pyの内容です。routing.pyはwebsocketの接続について書きます。
re_pathはURLRouterの制限のためと書いてありましたがよくわかりません。まぁこう書けばいいんでしょう。
as_asgi()は後に用意するcomsumerクラスを使うための呼び出し方です。comsumerクラスを用意してこの呼び出し方をすれば後は勝手にやってくれるっぽいです。comsumerクラスの書き方は後で紹介します。

mysite/asgi.py

mysite/asgi.py
import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")

application = ProtocolTypeRouter({
  "http": get_asgi_application(),#http://ならこっち
  "websocket": AuthMiddlewareStack(#ws:// wss://ならこっち
        URLRouter(
            chat.routing.websocket_urlpatterns#chat/routing.py
        )
    ),
})

これはHTTP接続かWebsocket通信かを判断して振り分けます。HTTPならchat/urlsに飛んでそこではviewsなどを使ってHTTPで返します。
Websocketならばrouting.pyに飛んで処理をします。

chat/consumers.py

これがwebsocketの処理になります。とりあえずまずはコピーして、下の解説を見ながら読んでください。

chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer

#asyncをつけることでパフォーマンスが上がる
#websocketConsumerからAsyncWebsocketConsumerになる

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']#scopeは接続の情報を持つ、
        self.room_group_name = 'chat_%s' % self.room_name#roomのstrを作ってる

        # Join room group
        await self.channel_layer.group_add(
        #グループ参加処理、まぁこういう書き方するんだよ、ぐらい
        #ChatComsumerは同期だがchannel_layerは非同期
            self.room_group_name,
            self.channel_name
        )

        await self.accept()#websocketをacceptする,acceptしない場合rejectされる

    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(#退出処理
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to room group
        await self.channel_layer.group_send(#グループにメッセージを送る
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message
            }
        )

    # Receive message from room group
    async def chat_message(self, event):
        message = event['message']

        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            'message': message
        }))

まぁちょっと長いです。

もともとasyncではないのですが、asyncで実装することでスレッド呼び出しがなくなってパフォーマンスがあがる(?)らしいです。従ってawaitやasyncがついています。

connect、disconnect、receiveはchat/routing.pyにてas_asgi()として呼び出すための関数です。このように書いておけば後は勝手にやってくれるっぽいですね。connectはurlを入力して接続したとき、disconnectはcloseしたとき、receiveはsendしたときに実行されるっぽいです。

sendのtypeに特定の関数を設定しておくことで誰かがsendした際にその関数が実行され、それが結果的にサーバーからクライアントにメッセージを送ることになります。今回はchat_messageがそれに該当して結果的にグループに所属する全クライアントにsendを行っていますね。

動かす

動かす際にはここにあるindex.htmlやここにあるroom.htmlを参考にchat/Templates/chat/内にHTMLを書いておいてください。

py manage.py runserver

をしてサーバーを動かしたのちにhttp://127.0.0.1:8000/chat/lobby/ に2つのブラウザ(ウィンドウ)でアクセスします。

するとなんということでしょう!相手側にも反映されています!!!!

すごい!!!!!!!

はい。

まぁ使う場合は「JavaScript websocket」や「python websocket」、「Unity websocket」で調べてください。多分サーバーからsendされた場合はonReceiveみたいなイベント関数が用意されていると思います。以上。

参考

公式ドキュメント・チュートリアル
https://channels.readthedocs.io/en/stable/tutorial/part_2.html

stack overflow「channels without channel layer or any other free hosting」
https://stackoverflow.com/questions/53271407/channels-without-channel-layer-or-any-other-free-hosting

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1