[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

Django 2.0 + Channels 2.xを使ってWebsocketを扱う(その2)

前回に引き続きChannelsを触っていきます。前回はChannelsの有効化までやっていきましたので、今回は実際にChannelsを使ってWebSocketでのチャットを実装していきます。

room viewの作成

まずはChatで部屋名を入力した後、実際にチャットを行う際の画面を作っていきます。mysite/chat/templates/chat/room.htmlとして以下を作成します。

<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Room</title>
</head>
<body>
    <textarea id="chat-log" cols="100" rows="20"></textarea><br/>
    <input id="chat-message-input" type="text" size="100"/><br/>
    <input id="chat-message-submit" type="button" value="Send"/>
</body>
<script>
    var roomName = {{ room_name_json }};

    var chatSocket = new WebSocket(
        'ws://' + window.location.host +
        '/ws/chat/' + roomName + '/');

    chatSocket.onmessage = function(e) {
        var data = JSON.parse(e.data);
        var message = data['message'];
        document.querySelector('#chat-log').value += (message + '\n');
    };

    chatSocket.onclose = function(e) {
        console.error('Chat socket closed unexpectedly');
    };

    document.querySelector('#chat-message-input').focus();
    document.querySelector('#chat-message-input').onkeyup = function(e) {
        if (e.keyCode === 13) {  // enter, return
            document.querySelector('#chat-message-submit').click();
        }
    };

    document.querySelector('#chat-message-submit').onclick = function(e) {
        var messageInputDom = document.querySelector('#chat-message-input');
        var message = messageInputDom.value;
        chatSocket.send(JSON.stringify({
            'message': message
        }));

        messageInputDom.value = '';
    };
</script>
</html>

つらつらとJSがかいてありますが、基本的にはページを開いた際にws://hostname:port/ws/chat/<部屋名>へのコネクションをはります。その上で、inputに入力した内容をchatSocket.sendでサーバに送信し、サーバからWebSocket経由のメッセージがきた場合はchatSocket.onmessageにあるように、受信した内容をtextareaに反映させています。

この時点でhttp://localhost:3000/chat/lobbyにアクセスすると画面が表示されますが、当然WebSocketのサーバをまだ作成していないのでチャットはできません。以下の様にDevToolでは接続エラーがでています。

f:id:denzow:20180327002144p:plain

サーバ側にもWebSocketに対応するアプリケーションがない旨を示すエラーが出ています。

service_1        | 2018-03-26 15:00:23,529 - ERROR - ws_protocol - Traceback (most recent call last):
service_1        |   File "/usr/local/lib/python3.6/site-packages/daphne/ws_protocol.py", line 76, in onConnect
service_1        |     "subprotocols": subprotocols,
service_1        |   File "/usr/local/lib/python3.6/site-packages/daphne/server.py", line 184, in create_application
service_1        |     application_instance = self.application(scope=scope)
service_1        |   File "/usr/local/lib/python3.6/site-packages/channels/staticfiles.py", line 42, in __call__
service_1        |     return self.application(scope)
service_1        |   File "/usr/local/lib/python3.6/site-packages/channels/routing.py", line 53, in __call__
service_1        |     raise ValueError("No application configured for scope type %r" % scope["type"])
service_1        | ValueError: No application configured for scope type 'websocket'

本作業のコミットログは以下です。

github.com

consumerの作成

続いてWebSocketを扱うバックエンドを作っていきます。そのためにはまずはconsumerを作成します。DjangoではURLConfを作成し、アクセスされるURLとその際に呼び出されるview関数をマッピングします。Channelsはその例でいうところのviewに相当します。WebScoektのアクセスをどのように処理するかを定義するものです。

mysite/chat/consumers.pyとして以下を作成します。

from channels.generic.websocket import WebsocketConsumer
import json


class ChatConsumer(WebsocketConsumer):
    """
    WebSocketでの通信をハンドルする
    """
    def connect(self):
        # とりあえず無条件で受け入れる
        # 接続を拒否する場合はself.close()する
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data=None, bytes_data=None):
        """
        受け取ったメッセージをそのままオウム返しに戻す
        """
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        self.send(text_data=json.dumps({
            'message': message
        }))

すべての接続を受け入れ、受け取ったメッセージをそのまま戻すconsumerです。これをWebSocketからのアクセス時に呼び出せるようにマッピングさせます。

mysite/chat/routing.pyとして以下を作成します。

# chat/routing.py
from django.urls import path

from . import consumers


websocket_urlpatterns = [
    path('ws/chat/<str:room_name>/', consumers.ChatConsumer),
]

/ws/chat/部屋名/というアクセスがあったときに先程つくったconsumers.ChatConsumerを呼び出すという設定です。URLとのマッピングはDjangoのpathを使っているためほとんど通常のURLConfと変わりません。

さらにRootのRoutingを定義するため前回作ったmysite/routing.pyを変更します。

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from chat import routing as chat_routing


application = ProtocolTypeRouter({
    # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chat_routing.websocket_urlpatterns
        )
    ),
})

websocketでのアクセスがあったときにはchat.routing.websocket_urlpatternsのルールに従ってConsumerを呼び出すようになります。

ここまでで一応WebSocketは使えるようになっています。入力した内容がそのまま戻っているだけな上に他のセッションと共有されていない為わかりづらいですが、上の欄に出てくる内容はサーバがWebSocket経由で戻しているものです。

f:id:denzow:20180327002220g:plain

いわゆるエコーサーバ的なものが出来上がりました。

本作業のコミットログは以下です。

github.com

一旦まとめ

とりあえずWebSocketを通じてサーバと通信するところまで完成しました。次回はちゃんとチャットとして機能するようにROOM単位で内容が共有出来るようにしていきます。