はじめに
オンラインゲームでよく使われる通信プロトコルであるWebSocket
を使えるようになりたいと思い、今回Django Channels
を使って、ボタンを押したらボタンの内容をリアルタイムで送り続けるアプリケーションを作ってみました。
加えて、websocketについて調べてまとめてみようと思います。(間違ってる部分は教えてください)
WebSocketって何?
概要
WebSocket
は、HTTP
と同じように通信プロトコルであり、双方向通信を低コストで行えるのが特徴であり、リアルタイム通信で主に使われます。
HTTPとの違い
HTTP
と特に違うのは、TCPでコネクションを確立した後の通信の仕方が違います。
コネクション確立には、3ウェイハンドシェイク
と呼ばれるコネクション確立を行なっています。
まず、クライアント側からSYNパケット
を送り、それを受け取ったサーバーがSYNパケット
とACKパケット
を同時に送ります(SYN/ACKパケット
とも呼ばれます)。そして、SYNパケット
とACKパケット
を受け取ったクライアントがACKパケット
を返してサーバーが受け取った後、通信ができるようになります。
SYNパケットは、クライアントからサーバーに接続したいときに送られるパケット。TCPヘッダのSYNフィールド(SYNフラグ)が1になっている。
ACKパケットは、何らかの肯定的な返答を返す際に送られるパケット。TCPヘッダのACKフィールド(ACKフラグ)が1になっている。
SYN/ACKパケットでは、SYNフィールドとACKフィールドがどちらも1になっている。
HTTP
とWebSocket
が違うのは、ここからでHTTP
は1リクエストに対して1コネクションを繋ぎ、1リクエストに対する1レスポンスが終わるとコネクションを閉じます。なので、次にリクエストを送るには、もう一度コネクションを繋ぎ直す必要があります。
一方で、WebSocket
は初回リクエストでコネクションを繋いだら、その状態を保持したまま、独自のプロトコルによる通信に切り替わり、双方向通信を行うことができます。
DjangoでWebSocket使うには?
DjangoでWebSocketを使うには、主にchannels
とchannels_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
djangoアプリケーションを立ち上げます。
python3 manage.py startapp websocket
djangowebsocket
と同じ階層にwebsocket
というフォルダができました。
ここからdjangoプロジェクトの設定を行います。
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'#一応追加
#下からの内容を追加
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
asgiref==3.8.1
Django==5.0.6
sqlparse==0.5.0
これを出力したら、venvフォルダは消しても大丈夫です。
これに追記します。
asgiref==3.8.1
Django==5.0.6
sqlparse==0.5.0
#追記
channels
channels_redis
daphne
daphne
DjangoChannelsが推奨しているASGIサーバーです。インストールすることでASGIを使用することが可能になります。
Dockerfile
とdocker-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/
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が使えるように実装していこうと思います。
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の情報を見れるようにします。
from django.shortcuts import render
def index(request):
return render(request, 'websocket/index.html')
urls.py
を作成して、以下のように記述します。
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
routing.py
を作成して、記述します。これは、一般的にHTTP
以外のプロトコルのルーティングを決める際に記述します。
from django.urls import path
from . import consumers
websocket_urlpatterns = [
path("ws/websocket/",consumers.WebsocketConsumer.as_asgi()),
]
consumers.py
を作成して、記述します。ここでは、websocket
を使ってどのような通信を行うかを記述します。
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
を送るようにしてみました。
<<!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 にログインすると以下の画像になっています。
これをpostmanを使って、メッセージを送れるか試してみます。
画質の関係でわかりづらいですがいけました!
postmanはアピテストに使えるツールで、websocketでもテストができるのでぜひ使ってみてください
最後に
今回channelとgroupとconsumerという新たな概念が出てきて、個人的に困惑したので自分なりにまとめてみました。
- channel
websocketに繋がったクライアントをつなげるためのパイプライン。各channelには名前があり、consumerにメッセージを送る。 - group
channelをひとまとめにするもの、groupに所属していると一斉に同じ内容を送れる。 - consumer
channnelから受け取った内容を処理するためのクラス。
今回を機にwebsocketで何か作ってみようと思いました。
リポジトリはこちらです