DjangoとVueでカンバンアプリケーションを作る(6)

※はじめからは[こちら](https://qiita.com/denzow/items/046f3c8b9bd8d3378eb4#_reference-7b3e4a58510793f8a9c7)

今回からいよいよ、Django Channelsを使ってWebsocket部分を実装していきます。今回はサーバサイドを大まかに作っていきます。

### Django Channels

Django Channelsは標準のDjangoでは扱いづらいWebsocketなどのプロトコルへDjangoを対応させるためのライブラリです。

https://channels.readthedocs.io/en/latest/

Django開発グループが作成しているので安心感があります。今後DjangoでWebsocketを使う必要がある場合はChannels一択になるかと思います。

#### channelsの有効化

今回利用しているDockerの環境にはChannelsはインストール済ですので不要ですが、実際にインストールする場合は以下を実行します。

```bash:今回は実施済なので不要
$ pip install channels channels-redis
```

さて、インストールされているchannelsを有効化ていきます。

まずは、`channels`のエントリーポイント(最初にアクセスされるファイル)を用意します。

```python:application/views/routing.py
from channels.routing import ProtocolTypeRouter


application = ProtocolTypeRouter({})

```

`ProtocolTypeRouter`が最初にアクセスを受け入れる入り口になります。その名前の通りアクセスしてきたプロトコルごとに処理を振り分けます。今はどのプロトコルに対しても振り分けのルールを書いていませんが、デフォルトで`HTTP`用のルールが自動設定されているので、まずはこのままで大丈夫です。

続いてsettingsを変更していきます。

```diff:application/settings/base.py

index 264d64a..23dc6b3 100644
--- a/application/settings/base.py
+++ b/application/settings/base.py
@@ -39,6 +39,9 @@ INSTALLED_APPS = [
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.staticfiles',
+
+    'channels',
+
     'modules.kanban',
 ]

@@ -133,3 +136,6 @@ ROOT_URLCONF = 'application.views.urls'
 LOGIN_REDIRECT_URL = '/'
 LOGIN_URL = '/accounts/login/'
 LOGOUT_REDIRECT_URL = '/'
+
+# ASGIの起点を指定
+ASGI_APPLICATION = 'views.routing.application'

```

`INSTALLED_APPS`に`channels`を`'django.contrib.staticfiles'`と`'modules.kanban'`の間に追加します。そして、`ASGI_APPLICATION`という変数に、先程追加した`routing.py`の`application`を指定します。

場合によっては編集中にdjangoが再起動して、中途半端な状態でファイルを読み込み停止してしまう可能性もあるので動きがおかしいと思った場合は以下でDockerを再起動します。

```bash
$ docker-compose restart
```

```bash:実行例
$ docker-compose restart
WARNING: The DJANGO_ENV variable is not set. Defaulting to a blank string.
Restarting workrepo-for-tutorial_service_nginx_1 ... done
Restarting workrepo-for-tutorial_service_1       ... done
Restarting workrepo-for-tutorial_vuejs_1         ... done
Restarting workrepo-for-tutorial_redis_1         ... done
Restarting workrepo-for-tutorial_db_1            ... done
```

再起動後、 http://localhost:3000/ に再度アクセスして画面がしっかりと表示されてば設定は完了です。見た目上は特に変わっていませんが、Channels上(ASGI上)でアプリケーションが動作するようになっています。

### websocketの処理追加

Channels上でWebSocketを使えるようにしていきます。このセクションでは`ws://localhost:3000/ws/board/1`へWebsocketでアクセスできるようにします。

#### Consumerの作成

いままではAPIごとにViewを作ってきましたが、WebSocketでも同じ様に処理を受け付けるコンポーネントは`Consumer`と呼ばれます。まずは簡単なConsumerを作っておきます。

`views`配下に新たに`ws`パッケージを作ります。

```bash
$ mkdir application/views/ws
$ touch application/views/ws/__init__.py
```

続いて、以下のファイルを作成します。

```python:application/views/ws/kanban_consumer.py
from channels.generic.websocket import JsonWebsocketConsumer


class KanbanConsumer(JsonWebsocketConsumer):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 自身に一意なIDを付与する
        self.consumer_id = id(self)
        # 後で使う
        self.user = None

    def connect(self):
        # 認証チェック
        if not self.scope['user'].is_authenticated:
            self.close()
            return
        
        self.user = self.scope['user']
        # 接続を受け入れる
        self.accept()

```

自身のConsumerを定義する場合は`channels`が提供しているベースクラスを継承して作成します。JSON以外を扱う前提の基底クラスもありますが、JSONをクライアントとやり取りするのであれば`JsonWebsocketConsumer`を継承しておくのが最も楽です。

`__init__` はオーバーライドする必要は必ずしもないですが、Consumerに独自の属性(`consumer_id`, `user`)をもたせたいので今回はオーバライドしています。

websocketの場合、最初にアクセスしてきた際にこの`KanbanConsumer`がインスタンス化され、自身の`connect`メソッドが呼び出されます。今回は、そのタイミングでログイン済であるかをチェックし、未ログインであれば`self.close()`を呼び出して切断するようにしています。


#### routingの追加

先程作成した`routing.py`を以下のように編集します。

```diff:application/views/routing.py
@@ -1,4 +1,13 @@
-from channels.routing import ProtocolTypeRouter
+from channels.auth import AuthMiddlewareStack
+from channels.routing import ProtocolTypeRouter, URLRouter
+from django.urls import path

+from .ws.kanban_consumer import KanbanConsumer

-application = ProtocolTypeRouter({})
+application = ProtocolTypeRouter({
+    'websocket': AuthMiddlewareStack(
+        URLRouter([
+            path('ws/boards/<int:board_id>', KanbanConsumer),
+        ])
+    ),
+})
```


`websocket`というキーにマッピングを追加しましたので、WebSocketでのアクセスはここに流れてきます。まずは、`AuthMiddlewareStack `が呼び出されて、Django自体が提供しているセッション内のログインユーザ情報にChannelsでもアクセスできるようになります。その後、リクエストされたURLが`'ws/boards/<int:board_id>'`にマッチすれば、先程作成された`KanbanConsumer`が呼び出されるようになります。

#### 動作確認

WebSocketの場合、ブラウザから開いて動作確認というのは難しいです(ブラウザから開く場合はHTTPになるので)。`wscat`などを使うことが多いようですが、今回はちゃんとログインしたセッションでないと処理できないので、`wsrequests`というPythonライブラリを使って確認します。

https://github.com/denzow/wsrequests

これはカンバンアプリケーションの動作に必要なライブラリではないですが、動作確認のためにインストールします。(Docker外で大丈夫です)

```bash
$ pip install wsrequests
```

そして、`wstest.py`として以下のファイルを作成します。もしログイン情報を`test/test`以外にしている場合は書き換えてください。

```python:wstest.py
from wsrequests import WsRequests

wsr = WsRequests()

# login django
wsr.get('http://localhost:3000/accounts/login/')
wsr.post(
    'http://localhost:3000/accounts/login/',
    data={
        'username': 'test',  # Djangoのユーザ名とパスワード
        'password': 'test',
        'csrfmiddlewaretoken': wsr.cookies['csrftoken'],
        'next': '/',
    }
)
wsr.connect('ws://localhost:3000/ws/boards/1')
wsr.disconnect()

```

そして以下のように実行します。

```bash
$ python wstest.py
```

```bash:実行例
$ python wstest.py
2018-09-22 17:00:13,528 - DEBUG - MainThread - connect websocket [ws://localhost:3000/ws/boards/1]
2018-09-22 17:00:13,545 - DEBUG - MainThread - disconnect websocket [ws://localhost:3000/ws/boards/1]
```

実行例のように、エラーなく`connect`と`disconnect`が出れば正常です。ここまででChannelsをつかったConsumerの基本的な部分が用意できましたので、次回からは実際の処理を組み込んでいきます。

### 次回

https://qiita.com/denzow/items/a61f5faf3161451559e7