※当サイトではアフィリエイト広告を利用しています。

django

Django と React で websocket 通信を行う方法

はじめに

この記事では Django と React で Websocket 通信を行う方法を紹介します。2 つのブラウザを表示して、片方のブラウザに文字を打ち込んだ際にもう片方のブラウザにリアルタイムで反映されるかを確認します。

websocket

この記事でわかる

  • Websocket 通信について理解できる
  • Websocket 通信における Django と React のコーディング方法が理解できる

この記事の対象者

  • Django と React の基礎知識がある方

Websocket 通信とは

WebSocket 通信は、ウェブアプリケーションがサーバーとリアルタイムで双方向通信を行うための技術です。HTTP プロトコルとは異なり、”ws”(非暗号化接続)と”wss”(TLS を使用した暗号化接続)の二つのスキーマを持ちます。

通常 HTTP でのリクエストはクライアントからしか送ることができず、サーバーから通信を始めることはできません。例えば、データベースが更新された場合でも、リクエスト側から確認をしない限り、情報を得ることはできないということです。

Websocket 通信の動きを理解するために、2 つのブラウザを表示し、片方で入力した情報がもう片方のブラウザにリアルタイムで更新されることを確認します。

動作環境

  • OS==Windows11 wsl2 ubuntu22.04
  • python==3.10.12
  • Django==4.2.10
  • channels==4.0.0
  • channels-redis==4.2.0
  • node==v18.16.0
  • npm==9.5.1

バックエンド

はじめに Django 側の実装を行います。python3.10 で仮想環境を作成し、activate します。

mkdir django_react_websocket
cd django_react_websocket/
python -m venv .venv
source .venv/bin/activate

 

ライブラリのインストール

開発に必要なライブラリをインストールします。

各ライブラリについて説明します。(Django は割愛)
  • django-cors-headers
    • 他のオリジンからの Django アプリケーションへのブラウザ内リクエストを可能にする
    • フロントエンドとバックエンドが異なるオリジン(ドメイン、スキーム、ポートが異なる URL)の場合、CORS ポリシーを適切に設定する必要がある
    • React アプリは localhost:3000 で動作し、Django API は localhost:8000 で動作する
    • django-cors-headers を追加すると、他のドメイン上のリソースにアクセスできるようになる
  • channels
    • WebSocket を通じて、クライアントとサーバー間の双方向リアルタイム通信を行う
    • 非同期ビュー、非同期データベースアクセス、非同期ファイル I/O など、非同期処理をサポートする
    • channel_layer を使うことで、例えば、A さんが接続している WebSocket が受け取ったメッセージを、他のすべての関連する WebSocket 通信でつながっている人に対して共有する事ができる
  • channels_redis
    • channel_layer のバックエンドとして Redis を使用するためのライブラリ
    • Redis についてはこの記事が参考になる
  • daphne
    • ASGI(Asynchronous Server Gateway Interface)サーバーの一つで、非同期通信を扱うことができるウェブサーバー
    • WebSocket 通信を行うためには、通常のpython manage.py runserverで起動する WSGI(Web Server Gateway Interface)ではなく ASGI サーバーを使用する必要がある
    • settings.py を修正することでpython manage.py runserverでも ASGI を起動できる(公式ドキュメント
pip install Django==4.2.10
pip install django-cors-headers
pip install channels==4.0.0
pip install channels-redis==4.2.0
pip install daphne

プロジェクトの作成

以下のコマンドで django プロジェクトを立ち上げ、config というプロジェクト名を backend に変更します。(プロジェクト名は自分で管理できる名前であれば何でも OK)

django-admin startproject config
mv config/ backend

実装

まず初めに django プロジェクト内に chat アプリを作成します。

cd config
python manage.py startapp chat

 

次に config 内の settings.py の修正と追加を行います。

settings.py

INSTALLED_APPS = [
    'daphne',  # add
    'django.contrib.admin',
    ...,
    'corsheaders',  # add
    'channels',  # add
    'chat',  # add
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'corsheaders.middleware.CorsMiddleware',  # add
    ...,
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ASGI_APPLICATION = 'config.asgi.application'

# 自身以外のオリジンのHTTPリクエスト内にクッキーを含めることを許可する
CORS_ALLOW_CREDENTIALS = True

CORS_ORIGIN_WHITELIST = [
    'http://localhost:3000',
    'http://localhost:8000'
]

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}

 

簡単に説明しておくと、必要なアプリを追記し CORS 対応および WebSocket 通信を行うための設定を追記しています。

INSTALLED_APPS に daphne、ASGI_APPLICATION = ‘config.asgi.application’を追記することで python manage.py runserver で ASGI サーバーを起動することができます。

また、CHANNEL_LAYERS のバックエンドとして redis を使用します。ただし、現状では redis 自体が起動していないため、redis を起動させる必要があります。

 

docker を使って redis を起動しておきます。

docker run -p 6379:6379 --name redis -d redis

 

models.py

content と timestapm のみを属性に持つ、簡単な Meaage モデルを作成します。

from django.db import models


# Create your models here.
class Message(models.Model):
    content = models.TextField()
    timestamp = models.DateTimeField(auto_now_add=True)

モデルを作成したので、migrate しておきます。

python manage.py makemigrations
python manage.py migrate

 

routing.py

routing.py を作成します。これは startapp で chat アプリを作った際にデフォルトで作成されないため、手動でファイルを作成することになります。

ここでクライアントが WebSocket を通じてサーバーに接続するためのエンドポイント(URL パターン)を指定し、その接続がどのコンシューマ(Consumer)によって処理されるかをマッピングします。そのため、ws/chat/などは任意で指定することになります。

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/', consumers.ChatConsumer.as_asgi()),
]

 

consumers.py

consumers.py を作成します。これも startapp で chat アプリを作った際にデフォルトで作成されないため、手動でファイルを作成することになります。

import json

from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer

from .models import Message


class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_group_name = 'chat'

        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

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

        res = await self.save_message_to_db(message)
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': res['message_data'],
            }
        )

    async def chat_message(self, event):
        message = event['message']

        # WebSocketを介してメッセージを送信
        await self.send(text_data=json.dumps({
            'message': message
        }))

    @database_sync_to_async
    def save_message_to_db(self, message_text):

        message = Message.objects.create(content=message_text)
        return {
            'message_data': {
                'id': message.id,
                'content': message.content,
                'timestamp': message.timestamp.isoformat(),
            }
        }

ChatConsumerクラスは、AsyncWebsocketConsumer を継承し、WebSocket を通じた非同期通信を行います。

receive関数で、フロントエンドから送られてきた text_data から message を取り出します。

そのメッセージを channel_layer を介して chat”グループに属する全てのクライアントに対してブロードキャストします。その際に、データベースにメッセージを保存します。

asgi.py

config 内の asgi.py を修正します。ここで ASGI 対応エントリーポイントを定義します。

import os

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

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

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

)

 

ProtocolTypeRouter はクライアントからの接続タイプ(HTTP、WebSocket など)に基づいて、適切なルーティングを行うためのルーターです。異なるプロトコルに基づいたリクエストを適切なアプリケーションに振り分けることをします。

websocket 通信に対しては、先ほど chat アプリ内に作成した routing.py の websocket_urlpatterns を指定しています。

フロントエンド

はじめに React プロジェクトを立ち上げます。ルートディレクトリに戻って以下のコマンドを実行します。(backend と同じ階層に frontend というフォルダを作成することになる)

npx create-react-app --template typescript frontend

 

実装

App.tsx

App.tsx を以下のように修正します。

import React, { useEffect, useRef, useState } from "react";
import "./App.css";

interface MessagesProps {
  id: string;
  content: string;
  timestamp: string;
}

function App() {
  const [message, setMessage] = useState<string>("");
  const [messages, setMessages] = useState<MessagesProps[]>([]);
  const webSocketRef = useRef<WebSocket | null>(null);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setMessage(event.target.value);
  };

  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault(); // フォームのデフォルト送信を防止

    if (message.trim() === "") return; // 空のメッセージは送信しない

    if (
      webSocketRef.current &&
      webSocketRef.current.readyState === WebSocket.OPEN
    ) {
      webSocketRef.current.send(
        JSON.stringify({
          type: "message",
          message: message,
        })
      );
    }
    setMessage("");
  };

  useEffect(() => {
    // WebSocket接続が既に開かれている場合は、新たに作成しない
    if (
      webSocketRef.current &&
      webSocketRef.current.readyState === WebSocket.OPEN
    ) {
      return;
    }

    webSocketRef.current = new WebSocket(`ws://127.0.0.1:8000/ws/chat/`);

    // 接続が開かれた時の処理
    const onOpen = () => {
      console.log("WebSocket Connected");
    };

    // メッセージ受信時の処理
    const onMessage = (e: MessageEvent) => {
      const data = JSON.parse(e.data);
      setMessages((messages) => [...messages, data.message]);
    };

    // エラー発生時の処理
    const onError = (e: Event) => {
      console.log("WebSocket Error: ", e);
    };

    // 接続が閉じられた時の処理
    const onClose = () => {
      console.log("WebSocket Disconnected");
    };

    // イベントリスナーの設定
    webSocketRef.current.addEventListener("open", onOpen);
    webSocketRef.current.addEventListener("message", onMessage);
    webSocketRef.current.addEventListener("error", onError);
    webSocketRef.current.addEventListener("close", onClose);

    return () => {
      if (webSocketRef.current?.readyState === 1) {
        // イベントリスナーを削除
        webSocketRef.current.removeEventListener("open", onOpen);
        webSocketRef.current.removeEventListener("message", onMessage);
        webSocketRef.current.removeEventListener("error", onError);
        webSocketRef.current.removeEventListener("close", onClose);
        webSocketRef.current?.close();
      }
    };
  }, []);

  return (
    <div className="App">
      <div className="p-2">
        <input
          placeholder="Type something..."
          onChange={handleChange}
          value={message}
        />
        <button
          type="submit"
          onClick={handleSubmit}
        >
          send
        </button>
      </div>
      <div>
        {messages.map((message) => (
          <li key={message.id}>
            {message.content} {message.timestamp}
          </li>
        ))}
      </div>
    </div>
  );
}

export default App;

 

以下のコマンドでサーバーを立ち上げて、2 つのブラウザを起動し、localhost:3000 と入力します。

npm start

 

片方のブラウザに入力した情報がもう片方のブラウザにリアルタイムで反映されることが確認できます。
websocket

まとめ

この記事では、Django と React で WebSocket 通信を行う方法を紹介しました。

WebSocketは、リアルタイムの双方向通信を可能にするプロトコルであり、チャットアプリケーションやリアルタイムデータフィードなど、即時性が求められるウェブアプリケーションの開発に利用されます。