7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

オンラインゲームでよく使われる通信プロトコルであるWebSocketを使えるようになりたいと思い、今回Django Channelsを使って、ボタンを押したらボタンの内容をリアルタイムで送り続けるアプリケーションを作ってみました。

加えて、websocketについて調べてまとめてみようと思います。(間違ってる部分は教えてください)

WebSocketって何?

概要

WebSocketは、HTTPと同じように通信プロトコルであり、双方向通信を低コストで行えるのが特徴であり、リアルタイム通信で主に使われます。

そもそもHTTPって?

Hyper Text Transfer Protocolの略称であり、主にハイパーテキスト(HTMLや画像や音声等)を通信するために使われる。主にブラウザとサーバーが通信するときに使っているのはこいつ。

HTTPとの違い

HTTPと特に違うのは、TCPでコネクションを確立した後の通信の仕方が違います。

コネクション確立には、3ウェイハンドシェイクと呼ばれるコネクション確立を行なっています。
まず、クライアント側からSYNパケットを送り、それを受け取ったサーバーがSYNパケットACKパケットを同時に送ります(SYN/ACKパケットとも呼ばれます)。そして、SYNパケットACKパケットを受け取ったクライアントがACKパケットを返してサーバーが受け取った後、通信ができるようになります。
image.png

SYNパケットは、クライアントからサーバーに接続したいときに送られるパケット。TCPヘッダのSYNフィールド(SYNフラグ)が1になっている。

ACKパケットは、何らかの肯定的な返答を返す際に送られるパケット。TCPヘッダのACKフィールド(ACKフラグ)が1になっている。

SYN/ACKパケットでは、SYNフィールドとACKフィールドがどちらも1になっている。

HTTPWebSocketが違うのは、ここからでHTTPは1リクエストに対して1コネクションを繋ぎ、1リクエストに対する1レスポンスが終わるとコネクションを閉じます。なので、次にリクエストを送るには、もう一度コネクションを繋ぎ直す必要があります。
一方で、WebSocketは初回リクエストでコネクションを繋いだら、その状態を保持したまま、独自のプロトコルによる通信に切り替わり、双方向通信を行うことができます。

image.png

image.png

DjangoでWebSocket使うには?

DjangoでWebSocketを使うには、主にchannelschannels_redisというライブラリを使用します。channels今回のDjangoChannelsを使うために必要なライブラリで、channels_redisはwebsocketに適しているredisサーバーを使うためのライブラリです。

DjangoChannelsって?

Djangoは標準でWSGIベースとなっており、HTTPのみサポートしているが、他の通信はサポートしていません。なので、HTTPを拡張し、ASGIと呼ばれるPythonの仕様に基づいて構築したパッケージをDjangoChannelsという。

channelはチャネルでもチャンネルでも呼び方はどっちでもいいそうです、一般的にはチャネルが多そうです。

WSGI(ウィスキー)とは
Web Server Gateway Interfaceの略称。名前の通り中継地点にあたるもので、webサーバーとアプリケーションとDBなど繋げる役割を担う。同期通信をベースとしています。

ASGIとは
Asynchronous Server Gateway Interfaceの略称。非同期通信を可能にしたWSGIのことです。

redisサーバーって?

リアルタイム通信でよく使われるデータベースサーバーの1種で、一般的なサーバーは、ディスクからメモリにデータを読み込みそれを通信で送りますが、redisサーバーはメモリにデータを保持し続けるので、高速なデータの取り出しが可能です。

実際にWebSocketを実装する。

今回は、Dockerで環境構築をしようと思ったのですが、問題としてDjangoプロジェクトを作っていない状態でDockerfileの作業ディレクトリを指定して記述するのは難しいと感じたので、まず仮想環境を作ってから行おうと思います。(理由は後ほど後述します)

最終的なディレクトリ構造を示しておくのでわからなくなったら、参考にしてください

.
├── Dockerfile
├── db.sqlite3
├── docker-compose.yml
├── manage.py
├── requirements.txt
├── djangowebsocket
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── websocket
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── consumers.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── routing.py
    ├── templates
    │   └── websocket
    │       └── index.html
    ├── tests.py
    ├── urls.py
    └── views.py

仮想環境での構築

まず、仮想環境を作成します。

python3 -m venv venv

仮想環境に入ります。

source venv/bin/activate

djangoのプロジェクトを作るために、下のコマンドを入力します。

pip3 install django

下のコマンドでDjangoのプロジェクトを作成します。

django-admin startproject djangowebsocket

これが終わるとvenvフォルダがある階層にプロジェクトディレクトリができます。(今回で言うdjangowebsocket)

一応djangoプロジェクトが立ち上がってるかを確認するために、プロジェクトフォルダにあるmanage.pyを使います。
下のコマンドでdjangowebsocketディレクトリに移動します。

cd djangowebsocket/

下のコマンドでサーバーを立ち上げます。

python3 manage.py runserver

下の画像ができたら完了です。
スクリーンショット 2024-06-13 13.27.29.png

djangoアプリケーションを立ち上げます。

python3 manage.py startapp websocket

djangowebsocketと同じ階層にwebsocketというフォルダができました。

ここからdjangoプロジェクトの設定を行います。

djangowebsocket/setting.py
import os

ALLOWED_HOSTS = [
    '*'#追加
]

INSTALLED_APPS = [
    'channels',#追加
    'websocket',#追加
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

#追加
ASGI_APPLICATION = 'djangowebsocket.asgi.application'

#追加
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts': [os.getenv('REDIS_URL')], # import os を忘れないように
        },
    }
}

LANGUAGE_CODE = 'ja'#一応追加

TIME_ZONE = 'Asia/Tokyo'#一応追加
djangowebsocket/asgi.py
#下からの内容を追加
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from websocket.routing import websocket_urlpatterns

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangowebsocket.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(
        URLRouter(
            websocket_urlpatterns
        )
    )
}

)

Dockerに移行

ここからDockerに移行します。
下のコマンドでrequirements.txtを記述します。

pip3 freeze > requirements.txt
requirements.txt
asgiref==3.8.1
Django==5.0.6
sqlparse==0.5.0

これを出力したら、venvフォルダは消しても大丈夫です。
これに追記します。

requirements.txt
asgiref==3.8.1
Django==5.0.6
sqlparse==0.5.0
#追記
channels
channels_redis
daphne

daphne
DjangoChannelsが推奨しているASGIサーバーです。インストールすることでASGIを使用することが可能になります。

Dockerfiledocker-compose.yml記述します.

これらのファイルは、manage.pyのあるディレクトリに入れてください。

FROM python:latest

ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

WORKDIR /code

COPY requirements.txt /code/

RUN pip install -r /code/requirements.txt

COPY . /code/
docker-compose.yml
services:
  web:
    build: .
    command: daphne -b 0.0.0.0 -p 8000 djangowebsocket.asgi:application
    volumes:
      - .:/code
    ports:
     - "8000:8000"
    depends_on:
      - redis

  redis:
    image: redis:latest
    ports:
      - "6379:6379"

djangoのプロジェクトを立ち上げると一つ下のディレクトリにmanage.pyがある状態になってしまい、Dockerfileがファイルを参照できるように記述する必要があるので、仮想環境を先に作りました。
また、daphneを使って実行するときにdjangowebsocketにあるasgi.pyを参照する必要があるので、こちらも複雑に記述がしたくないため、仮想環境で先に構築しました。

以下のコマンドでコンテナを立ち上げます。

docker compose up --build

ターミナルに以下のような表示が出てきたら完了です。

web-1    | 2024-06-13 15:44:17,028 INFO     Starting server at tcp:port=8000:interface=0.0.0.0
web-1    | 2024-06-13 15:44:17,028 INFO     HTTP/2 support not enabled (install the http2 and tls Twisted extras)
web-1    | 2024-06-13 15:44:17,028 INFO     Configuring endpoint tcp:port=8000:interface=0.0.0.0
web-1    | 2024-06-13 15:44:17,029 INFO     Listening on TCP address 0.0.0.0:8000

websocketを使えるように実装する

ここからwebsocketが使えるように実装していこうと思います。

djangowebsocket/urls.py

from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("",include("websocket.urls")),#追加
]

ここからアプリケーションwebsocketの設定します。views.pyでURLの情報を見れるようにします。

websocket/views.py
from django.shortcuts import render

def index(request):
    return render(request, 'websocket/index.html')

urls.pyを作成して、以下のように記述します。

websocket/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

routing.pyを作成して、記述します。これは、一般的にHTTP以外のプロトコルのルーティングを決める際に記述します。

websocket/routing.py
from django.urls import path
from . import consumers

websocket_urlpatterns = [
    path("ws/websocket/",consumers.WebsocketConsumer.as_asgi()),
]

consumers.pyを作成して、記述します。ここでは、websocketを使ってどのような通信を行うかを記述します。

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

class WebsocketConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        #グループ定義しないと動かなかったです
        #現在のwebsocketをsendmessageというグループに追加します
        await self.channel_layer.group_add("sendmessage",self.channel_name)
        print("Websocket connected")
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard("sendmessage",self.channel_name)
        print("Websocket disconnected")

    async def receive(self, text_data=None):
        print("Received websocket message", text_data)

        #受けったデータに内容がなかったら終了
        if not text_data.strip():
            return
        try:
            #jsonをparseする
            data = json.loads(text_data)
        except json.JSONDecodeError as e:
            print(e)
            return

        #辞書を作成
        messages = {
            "message":data.get("message"),
        }

        await self.channel_layer.group_send(

            "sendmessage",{
                "type":"send_message",#websocket通信を受け取ったら下のsend_messageを呼び出す
                "content":messages,
            }
        )
    async def send_message(self, event):
        #contentの中にある辞書を取り出し
        message = event["content"]
        #辞書をjson型にする
        await self.send(text_data=json.dumps(message))

views.pyで指定したwebsocket/index.htmlを表示します。Bodyタグ内にButtonを配置し、押したらHello WebSocketを送るようにしてみました。

websocket/index.html
<<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>test</title>
    <script>
<!--      domを読み込んでから-->
        document.addEventListener("DOMContentLoaded", function() {
            let interval;
            // websocketの接続先
            const url = "ws://" + window.location.host + "/ws/websocket/"
            const ws = new WebSocket(url);
            console.log(url)
            const button = document.getElementById("send");

            // websocketでの接続が確立されたときに動く
            ws.onopen = function() {
                console.log("Connected");
            };
            //websocketでメッセージを受信されたら動く
            ws.onmessage = function(e) {
                const data = JSON.parse(e.data);
                console.log("Received message:", data.message);
            };

            //ボタンを長押ししたら送り続ける
            button.addEventListener("mousedown", function() {
                console.log()
                interval = setInterval(function (){
                    sendMessage("Hello WebSocket");
                },10
                )
            });

            //ボタンを離したら送るのをやめる
            button.addEventListener("mouseup",function(){
                clearInterval(interval)
            })

            function sendMessage(message) {
                ws.send(JSON.stringify({"message": message}));
            }
});

    </script>
</head>
<body>
    <button id="send">Send Hello</button>
</body>
</html>

websocketを使ってみる

以下のコマンドを打ってイメージの作り直しとコンテナを立ち上げます。

docker compose up --build

http://localhost:8000 にログインすると以下の画像になっています。
スクリーンショット 2024-06-13 22.46.21.png

これをpostmanを使って、メッセージを送れるか試してみます。

websockettest

画質の関係でわかりづらいですがいけました!

postmanはアピテストに使えるツールで、websocketでもテストができるのでぜひ使ってみてください

最後に

今回channelとgroupとconsumerという新たな概念が出てきて、個人的に困惑したので自分なりにまとめてみました。

  • channel
    websocketに繋がったクライアントをつなげるためのパイプライン。各channelには名前があり、consumerにメッセージを送る。
  • group
    channelをひとまとめにするもの、groupに所属していると一斉に同じ内容を送れる。
  • consumer
    channnelから受け取った内容を処理するためのクラス。

今回を機にwebsocketで何か作ってみようと思いました。

リポジトリはこちらです

7
8
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
7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?