はじめに
この記事では Django と React で Websocket 通信を行う方法を紹介します。2 つのブラウザを表示して、片方のブラウザに文字を打ち込んだ際にもう片方のブラウザにリアルタイムで反映されるかを確認します。
この記事でわかる
- 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-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
片方のブラウザに入力した情報がもう片方のブラウザにリアルタイムで反映されることが確認できます。
まとめ
この記事では、Django と React で WebSocket 通信を行う方法を紹介しました。
WebSocketは、リアルタイムの双方向通信を可能にするプロトコルであり、チャットアプリケーションやリアルタイムデータフィードなど、即時性が求められるウェブアプリケーションの開発に利用されます。