LoginSignup
1
0

More than 1 year has passed since last update.

DjangoとDocker練習OA15o1o0 Webサーバーとクライアント側のアプリで通信しよう!

Last updated at Posted at 2022-03-19

目標

Webサーバーとクライアント間で双方向の非同期通信をしたい。だからする。
その手法の1つの Webソケット ならできる

情報

この記事は Lesson 1. から順に全部やってこないと ソースが足りず実行できないので注意されたい

What is This is
Lesson 1. 📖 DjangoとDockerでゲーム対局サーバーを作ろう!

この記事のアーキテクチャ:

Key Value
OS Windows10
Container Docker
Program Language Python 3
Auth allauth
Frontend Vuetify
Data format JSON
Others (Socket), Web socket
Editor Visual Studio Code (以下 VSCode と表記)

ディレクトリ構成を抜粋すると 以下のようになっている

    ├── 📂 src1                         # あなたのDjangoサーバー開発用ディレクトリー。任意の名前
    │   ├── 📂 apps1
    │   │   ├── 📂 accounts_vol1o0    # アプリケーション
    │   │   ├── 📂 portal_v1                # アプリケーション
    │   │   └── 📂 practice_vol1o0              # アプリケーション
    │   │       ├── 📂 management
    │   │       ├── 📂 migrations
    │   │       ├── 📂 models
    │   │       ├── 📂 static
    │   │       │   └── 📂 practice_vol1o0      # アプリケーションと同名
    │   │       │       └── 📂 data
    │   │       │           └── 📂 desserts1
    │   │       │               └── 📄 ver1o0.json
    │   │       ├── 📂 templates
    │   │       │   └── 📂 practice_vol1o0      # アプリケーションと同名
    │   │       │       ├── 📂 prefecture
    │   │       │       └── 📂 vuetifies
    │   │       ├── 📂 views
    │   │       │   ├── 📂 prefecture
    │   │       │   └── 📂 vuetifies
    │   │       ├── 📄 __init__.py
    │   │       ├── 📄 admin.py
    │   │       ├── 📄 apps.py
    │   │       └── 📄 tests.py
    │   ├── 📂 data
    │   ├── 📂 project1                  # プロジェクト
    │   │   ├── 📄 __init__.py
    │   │   ├── 📄 asgi.py
    │   │   ├── 📄 settings_secrets_example.txt
    │   │   ├── 📄 settings.py
    │   │   ├── 📄 urls_accounts_vol1o0.py
    │   │   ├── 📄 urls_practice.py
    │   │   ├── 📄 urls.py
    │   │   └── 📄 wsgi.py
    │   ├── 📂 project2                  # プロジェクト
    │   ├── 🐳 docker-compose-project2.yml
    │   ├── 🐳 docker-compose.yml
    │   ├── 🐳 Dockerfile
    │   ├── 📄 manage.py
    │   └── 📄 requirements.txt
    ├── 📂 src1_meta
    │   ├── 📂 data
    │   │   └── 📄 urls.csv
    │   └── 📂 scripts
    │       └── 📂 auto_generators
    │           └── 📄 urls.py
    ├── 📂 src2_local                   # Djangoとは関係ないもの
    │    └── 📂 sockapp1
    │        ├── 📄 client.py
    │        ├── 📄 echo_server.py
    │        └── 📄 main_finally.py
    └── 📄 .gitignore

手順

Step OA15o1o0g1o0 Dockerコンテナの起動

👇 (していなければ) Docker コンテナを起動しておいてほしい

# docker-compose.yml ファイルを置いてあるディレクトリーへ移動してほしい
cd src1

# Docker コンテナ起動
docker-compose up

Step OA15o1o0g2o0 Pythonパッケージ インストール指定 - requirements.txt ファイル

👇 以下のファイルを編集してほしい

    └── 📂 src1                   # あなたの開発用ディレクトリー。任意の名前
👉      └── 📄 requirements.txt
# ...略...


# 以下を追加
# For web socket
channels>=3.0

Step OA15o1o0g3o0 設定の編集 - settings.py ファイル

👇 以下の既存ファイルを編集してほしい

    └── 📂 src1
        ├── 📂 project1                  # プロジェクト
👉      │   └── 📄 settings.py
        └── 📄 requirements.txt
# ...略...


INSTALLED_APPS = [
    # ...略...


    # * 以下を追加
    # OA15o1o0g3o0 For web socket
    'channels',
]


# ...略...


# *
# ├── * 変更前
# │ // WSGI_APPLICATION = 'project1.wsgi.application'
# | // # * O2o1o0g9o1o2o_9o0 プロジェクト名の一般化
# │ // WSGI_APPLICATION = f'{PROJECT_NAME}.wsgi.application'
# │                         -------------------------------
# │                         1
# │ 1. DjangoのWSGI設定の大元となるグローバル変数。
# │    `src1/projectN/wsgi.py` ファイルの中の application 変数を指している
# │          -------------
# │
# └── * OA15o1o0g3o0 WSGI を ASGI にバージョンアップする
ASGI_APPLICATION = f"{PROJECT_NAME}.asgi.application"
#                    -------------------------------
#                    1
# 1. DjangoのASGI設定の大元となるグローバル変数。
#    `src1/projectN/asgi.py` ファイルの中の application 変数を指している
#          -------------

# * 続けて追加
# OA15o1o0g3o0 Webソケット使用
# See also: 📖 [Django Channels and WebSockets](https://blog.logrocket.com/django-channels-and-websockets/)
CHANNEL_LAYERS = {
    'default': {
        # * Method 1: Via redis lab
        # 'BACKEND': 'channels_redis.core.RedisChannelLayer',
        # 'CONFIG': {
        #     "hosts": [
        #       'redis://h:<password>;@<redis Endpoint>:<port>'
        #     ],
        # },

        # * Method 2: Via local Redis
        # 'BACKEND': 'channels_redis.core.RedisChannelLayer',
        # 'CONFIG': {
        #      "hosts": [('127.0.0.1', 6379)],
        # },

        # Method 3: Via In-memory channel layer
        # Using this method.
        "BACKEND": "channels.layers.InMemoryChannelLayer"
    },
}

👆 WSGI から ASGI に乗り換えた。 ASGIWSGI を兼ねるようだ

Step OA15o1o0g4o0 ASGI設定 - asgi.py ファイル<その1>

👇 以下の既存のファイルを編集してほしい

    └── 📂 src1
        ├── 📂 project1                  # プロジェクト
👉      │   ├── 📄 asgi.py
        │   └── 📄 settings.py
        └── 📄 requirements.txt
import os

# * OA15o1o0g4o0 以下をコメントアウト
# from django.core.asgi import get_asgi_application

# vvvv OA15o1o0g4o0 ASGI設定
import django
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter
# ^^^^ OA15o1o0g4o0


# ...略... os.environ.setdefault( ... ) など


# * OA15o1o0g4o0 ASGI設定
django.setup()

# * OA15o1o0g4o0 以下を削除
# application = get_asgi_application()

# * OA15o1o0g4o0 ASGI設定
application = ProtocolTypeRouter({
    "http": AsgiHandler(),
    # * IMPORTANT::Just HTTP for now. (We can add other protocols later.)
})

Step OA15o1o0g5o0 Visual Studio Code のエラー抑制 - pip コマンド

Python の channels パッケージは、 Dockerコンテナにインストールされていればよく、
Dockerコンテナの外側のPCにインストールしている必要はないが、
しかし あなたの Visual Studio Code は
👇 以下のような PROBLEMS (エラーメッセージ)を出しているかもしれない

Import "channels.http" could not be resolved
Import "channels.routing" could not be resolved

その Pythonソースは 外側のPCで実行するわけではないのだから無視して構わないが、気になるということはあるだろう。
Dockerコンテナの外側のPCにも channels をインストールすれば(本末転倒だが)エラーメッセージは解消する。
👇 もし望むなら、以下のコマンドを打鍵してほしい

pip install channels

Step OA15o1o0g6o0 Dockerコンテナの停止~ビルド~起動

👇 以下のコマンドを打鍵してほしい

# Dockerコンテナの停止
docker-compose down
# または Dockerマシンが走っているターミナルで `[Ctrl] + [C]` キーを打鍵

👇 以下のコマンドを打鍵してほしい

# requirements.txtを変更したので、Pythonパッケージのインストールをやり直します
docker-compose build

👇 以下のコマンドを打鍵してほしい

# Dockerコンテナの起動
docker-compose up

👆 これで Dockerコンテナに channels パッケージをインストールした

Step OA15o1o0g7o0 Webソケット設定 - consumer.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        ├── 📂 apps1
        │   └── 📂 practice_vol1o0              # アプリケーション
        │       └── 📂 websocks             # ただのフォルダー
        │           └── 📂 consumer         # ただのフォルダー
👉      │               └── 📄 ver1o0.py
        ├── 📂 project1                     # プロジェクト
        │   ├── 📄 asgi.py
        │   └── 📄 settings.py
        └── 📄 requirements.txt
# See also:
#     📖 [Django Channels and WebSockets](https://blog.logrocket.com/django-channels-and-websockets/)
#     📖 [Channels - Consumers](https://channels.readthedocs.io/en/latest/topics/consumers.html)
#     📖 [Channels - Channel Layers](https://channels.readthedocs.io/en/stable/topics/channel_layers.html)
from channels.generic.websocket import AsyncWebsocketConsumer


class WebsockPractice1V1Consumer(AsyncWebsocketConsumer):
    """OA15o1o0g7o0 非同期のWebソケットのコンシューマー"""

    async def connect(self):
        """接続時"""
        print("Connected")
        await self.accept()

    async def disconnect(self, close_code):
        """切断時"""
        print("Disconnected")

    async def receive(self, text_data):
        """メッセージ受信時
        Receive message from WebSocket.
        """
        print("Received")
        # Send message to WebSocket
        await self.send(text_data=f"Echo: {text_data}")

    async def send_message(self, res):
        """メッセージ送信時
        Receive message from room group
        """
        print("Sent message")
        # Send message to WebSocket
        await self.send(text_data=res)

Step OA15o1o0g8o0 ルート編集 - ws_urls_practice.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        ├── 📂 apps1
        │   └── 📂 practice_vol1o0              # アプリケーション
        │       └── 📂 websocks
        │           └── 📂 consumer
        │               └── 📄 ver1o0.py
        ├── 📂 project1                     # プロジェクト
        │   ├── 📄 asgi.py
        │   ├── 📄 settings.py
👉      │   └── 📄 ws_urls_practice.py
        └── 📄 requirements.txt
# See also: 📖 [Channels - Consumers](https://channels.readthedocs.io/en/latest/topics/consumers.html)
from django.conf.urls import url

# OA15o1o0g8o0 練習1.0巻 Webソケットの練習1 1.0版
from apps1.practice_vol1o0.websocks.consumer.ver1o0 import WebsockPractice1V1Consumer
#          ---------------                   ------        --------------------------
#          11                                12            2
#    ----------------------------------------------
#    10
# 10, 12. ディレクトリー
# 11. アプリケーション
# 2. `12.` に含まれる __init__.py ファイルにさらに含まれるクラス


websocket_urlpatterns = [

    # OA15o1o0g8o0 Webソケットの練習1
    url(r'^websock-practice1/v1/$', WebsockPractice1V1Consumer.as_asgi()),
    #     -----------------------   ------------------------------------
    #     1                         2
    # 1. 例えば `ws://example.com/websock-practice1/v1/` といったURLのパスの部分
    #                            ----------------------
    # 2. WebsockPractice1V1Consumer クラスの as_asgi 静的メソッドの返却値
]

Step OA15o1o0g9o0 ASGI設定 - asgi.py ファイル<その2>

👇以下の既存のファイルを編集してほしい

    └── 📂 src1
        ├── 📂 apps1
        │   └── 📂 practice_vol1o0              # アプリケーション
        │       └── 📂 websocks
        │           └── 📂 consumer
        │               └── 📄 ver1o0.py
        ├── 📂 project1                  # プロジェクト
👉      │   ├── 📄 asgi.py
        │   ├── 📄 settings.py
        │   └── 📄 ws_urls_practice.py
        └── 📄 requirements.txt
import os

# * OA15o1o0g9o0 コメントアウトの解除
from django.core.asgi import get_asgi_application

# * 追加の取消ここから
# import django
# from channels.http import AsgiHandler
# * 追加の取消ここまで

# * 追加ここから
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter

# OA15o1o0g9o0 Webソケット練習1
from . import ws_urls_practice
#    -        ----------------
#    1        2
# 1. 同じディレクトリー
# 2. `src1/projectN/ws_urls_practice.py`
#                   ----------------
# * 追加ここまで


# ...略...
# この辺に os.environ.setdefault(...)


# * 追加の取消
# django.setup()


# ...略...


# * 以下を追加
# OA15o1o0g9o0 複数のアプリケーションの websocket_urlpatterns をマージします
websocket_urlpatterns_merged = []

# OA15o1o0g9o0 Webソケット練習1
websocket_urlpatterns_merged.extend(
    ws_urls_practice.websocket_urlpatterns)


# * 変更前
# | application = ProtocolTypeRouter({
# |     "http": AsgiHandler(),
# |     # * IMPORTANT::Just HTTP for now. (We can add other protocols later.)
# | })
# * OA15o1o0g9o0 変更後
application = ProtocolTypeRouter({
    # * 削除
    # "http": AsgiHandler(),
    # * 追加
    "http": get_asgi_application(),
    # * 追加
    "websocket": AuthMiddlewareStack(
        URLRouter(
            websocket_urlpatterns_merged
        )
    ),
})

Step OA15o1o0gA10o0 ローカルPCにPythonのパッケージ websocket-client をインストール

Step OA15o1o0g1o0~ 9. は Dockerコンテナの中のサーバーサイドだった。
既に Djangoサーバー側では Webソケットで接続されるのを待っている

Step OA15o1o0g9o0 からは Dockerコンテナの外のクライアントサイドを説明する

👇 Dockerコンテナの外側の あなたのPCでコマンドを打鍵してほしい

# docker-compose.yml ファイルを置いてあるディレクトリーへ移動してほしい
# cd src1

pip install websocket-client

Step OA15o1o0gA11o0 複製 - main_finally.py ファイル

👇 以下の記事で掲載した main_finally.py ファイルをコピー&ペーストしてほしい

    ├── 📂 src1
    │   ├── 📂 apps1
    │   │   └── 📂 practice_vol1o0              # アプリケーション
    │   │       └── 📂 websocks
    │   │           └── 📂 consumer
    │   │               └── 📄 ver1o0.py
    │   ├── 📂 project1                  # プロジェクト
    │   │   ├── 📄 asgi.py
    │   │   ├── 📄 settings.py
    │   │   └── 📄 ws_urls_practice.py
    │   └── 📄 requirements.txt
    └── 📂 src2_local
         ├── 📂 sockapp1
         │   ├── 📄 client.py
         │   ├── 📄 echo_server.py
👉       │   └── 📄 main_finally.py  # ここからコピー
         └── 📂 websockapp1
👉           └── 📄 main_finally.py  # ここへペースト

Step OA15o1o0gA12o0 Webソケット クライアント作成 - websock_client.py ファイル

👇 以下のファイルを新規作成してほしい

    ├── 📂 src1
    │   ├── 📂 apps1
    │   │   └── 📂 practice_vol1o0              # アプリケーション
    │   │       └── 📂 websocks
    │   │           └── 📂 consumer
    │   │               └── 📄 ver1o0.py
    │   ├── 📂 project1                  # プロジェクト
    │   │   ├── 📄 asgi.py
    │   │   ├── 📄 settings.py
    │   │   └── 📄 ws_urls_practice.py
    │   └── 📄 requirements.txt
    └── 📂 src2_local
         ├── 📂 sockapp1
         │   ├── 📄 client.py
         │   ├── 📄 echo_server.py
         │   └── 📄 main_finally.py
         └── 📂 websockapp1
             ├── 📄 main_finally.py
👉           └── 📄 websock_client.py
# See also:
#     📖 [Python WebSocket通信の仕方:クライアント編](https://www.raspberrypirulo.net/entry/websocket-client)
#     📖 [websocket-client - Examples](https://websocket-client.readthedocs.io/en/latest/examples.html)
#     📖 [GitHub - websocket-client](https://github.com/websocket-client/websocket-client)
import sys
import traceback
import websocket

try:
    import thread  # 見つからない
except ImportError:
    import _thread as thread  # websocket-client の GitHub ではこっちが使われている

import time
import argparse
from main_finally import MainFinally


class Websocket_Client():
    """OA15o1o0gA12o0 Webソケット クライアント"""

    def __init__(self, url):

        # デバックログの表示/非表示設定
        websocket.enableTrace(True)

        # WebSocketAppクラスを生成
        self.websockApp = websocket.WebSocketApp(url,
                                                 on_open=lambda ws: self.on_open(
                                                     ws),
                                                 on_close=lambda ws, close_status_code, close_msg: self.on_close(
                                                     ws, close_status_code, close_msg),
                                                 on_message=lambda ws, msg: self.on_message(
                                                     ws, msg),
                                                 on_error=lambda ws, msg: self.on_error(ws, msg))

    def on_message(self, ws, message):
        """メッセージ受信に呼ばれる関数"""
        print("receive : {}".format(message))

    def on_error(self, ws, error):
        """エラー時に呼ばれる関数"""
        print(error)

    def on_close(self, ws, close_status_code, close_msg):
        """サーバーから切断時に呼ばれる関数"""
        print("### closed ###")

    def on_open(self, ws):
        """サーバーから接続時に呼ばれる関数"""
        thread.start_new_thread(self.run_worker, ())

    def run_worker(self, *args):
        """サーバーから接続時にスレッドで起動する関数"""
        while True:
            time.sleep(0.1)
            input_data = input("send data:")
            self.websockApp.send(input_data)

    def clean_up(self):
        self.websockApp.close()
        print("thread terminating...")

    def run_forever(self):
        """websocketクライアント起動"""
        self.websockApp.run_forever()


# このファイルを直接実行したときは、以下の関数を呼び出します
if __name__ == "__main__":

    class Main1:
        def __init__(self):
            self._client = None

        def on_main(self):
            parser = argparse.ArgumentParser(
                description='サーバーのアドレスとポートを指定して、テキストを送信します')
            parser.add_argument('--host', default="127.0.0.1",
                                help='サーバーのホスト。規定値:127.0.0.1')
            parser.add_argument('--port', type=int,
                                default=8000, help='サーバーのポート。規定値:8000')
            args = parser.parse_args()

            # FIXME このURLの埋め込みを外に出せないか?
            url = f"ws://{args.host}:{args.port}/websock-practice1/v1/"
            self._client = Websocket_Client(url)
            self._client.run_forever()
            return 0

        def on_except(self, e):
            """ここで例外キャッチ"""
            traceback.print_exc()

        def on_finally(self):
            if self._client:
                self._client.clean_up()

            print("これで終わり")
            return 1

    sys.exit(MainFinally.run(Main1()))

Step OA15o1o0gA13o0 Webソケット通信 - コマンド実行

👇 以下のコマンドを打鍵してほしい

# がんばって `src2_local/websockapp1` へ、カレントディレクトリを移動してほしい
# cd src2_local/websockapp1

python.exe -m websock_client

これで サーバー側とつながったはずだ。
適当な文字列 hello でも打鍵してほしい。
receive : Echo: hello と返ってくれば成功だ

クライアント側は [ctrl] + [C] キーで終了してほしい

サーバー側で [ctrl] + [C] キーを打鍵するとサーバーが落ちるので注意してほしい

次の記事

📖 DjangoのWebサーバーとクライアント側のアプリ間でJSON形式のテキストを通信しよう!

参考にした記事

📖 Python WebSocket通信の仕方:クライアント編
📖 websocket-client - Examples
📖 GitHub - websocket-client
📖 Channels - Consumers
📖 Django Channels and WebSockets
📖 Python で終了時に必ず何か実行したい
📖 Django を WebSocket サーバにする
📖 django-channels を使った websocket を用いたチャットアプリの作成
📖 Django ChannelsでできるリアルタイムWeb

1
0
1

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
1
0