Python3.5+uWSGIでWebSocketを実現する
uWSGI 1.9からこちらにあるようにWebSocketがサポートされた。簡単にできそうだったので、実際にサンプルを動作させようとしたところ、何度も失敗に終えて、あれこれ調べて気づけば丸一日以上費やしてしまった。もともと「Python+uWSGIでノンブロッキング」と内容をまとめて掲載しようとしたが、同じような方法では実現できなかったので、備忘録も兼ねて投稿することにした。
環境構築
以下、Python3.5、Dockerがインストールされている環境で実行する。
なお、Mac(macOS Sierra)の環境で構築した。
ディレクトリ構成
ディレクトリ構成は以下。
nginx-python/conf
配下のnginx.conf
とnginx.repo
は「DockerでPython+uWSGI+Nginxの環境を作成」と同一のため省略。docker-compose
やdocker
コマンドの基本的な利用方法についても、そちらに記載。
SSL証明書のCRTとKEYを作成
opensslのインストール
Mac環境で作成した。まずは、brew
コマンドでopenssl
をインストールする。
$ brew install openssl
brew
でインストールした最新のopenssl
コマンドを利用する場合、下記を.bashrc
の最後の行に追加。
export PATH=$(brew --prefix openssl)/bin:$PATH
追加したら、下記を実行。
$ source ~/.bashrc
証明書の作成
csr
を作成するときにいくつか質問に回答する必要があるが、すべてデフォルト(enterキー押下のみ)とした。
$ openssl genrsa -out server.key 2048
$ openssl req -new -key server.key -out server.csr
$ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
作成したCRT、KEYファイルをapp
ディレクトリ配下に配置(この例ではCSRもあるが本来不要)。
サンプルコード
サーバ側
クライアントとのハンドシェイクが完了した後で、データの受信待ちをし、受信したデータをそのままクライアントに送信する。
import uwsgi
def application(env, start_response):
uwsgi.websocket_handshake(env['HTTP_SEC_WEBSOCKET_KEY'], env.get('HTTP_ORIGIN', ''))
msg = uwsgi.websocket_recv()
print("receive: %s" % msg)
uwsgi.websocket_send(msg)
print("end")
クライアント側
ボタンを押下すると、サーバに接続し、データを送信し、応答を待つサンプル。
<!DOCTYPE HTML>
<html>
<head>
<script type="text/javascript">
function WebSocketTest()
{
if ("WebSocket" in window) {
alert("WebSocket is supported by your Browser!");
var ws = new WebSocket("wss://127.0.0.1:8443/ws/");
ws.onopen = function() {
ws.send("Hello from client");
alert("Message is sent...");
};
ws.onmessage = function (evt) {
var received_msg = evt.data;
alert("Message is received...");
};
ws.onclose = function() {
alert("Connection is closed...");
};
} else {
alert("WebSocket NOT supported by your Browser!");
}
}
</script>
</head>
<body>
<input type="button" onClick="WebSocketTest();" value="WebSocket Test">
</body>
</html>
各種設定ファイル
docker-compose.yml
ポート番号80を8180に、443を8443にフォーワードする。
version: "2"
services:
# nginx
nginx-python:
build: ./nginx-python
ports:
- "8180:80"
- "8443:443"
volumes:
- ./app/:/var/www/html/app/
environment:
TZ: "Asia/Tokyo"
Dockerfile
uWSGI
については、git clone
し、ビルドしている。そうしないとSSL接続ができなかった。また、ここではasyncio
を利用し、ノンブロッキングモードも対応することを想定しているが、ノンブロッキングモードに対応しなくてもWebSocket
は利用できる。その場合、CFLAGS="-I/usr/include/python3.5m" UWSGI_PROFILE="asyncio"
は削除しても問題ない。
FROM centos:6.8
ADD ./conf/nginx.repo /etc/yum.repos.d/
# nginx & python
RUN yum localinstall -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
RUN yum install -y https://centos6.iuscommunity.org/ius-release.rpm
RUN yum install -y nginx-1.10.1
RUN yum install -y make gcc
RUN yum install -y libxml2-devel
RUN yum install -y python35u python35u-libs python35u-devel python35u-pip
RUN yum clean all
RUN yum install -y git
RUN yum install -y openssl-devel
RUN ln -s /usr/bin/python3.5 /usr/bin/python3 && \
unlink /usr/bin/python && \
ln -s /usr/bin/python3 /usr/bin/python && \
ln -s /usr/bin/pip3.5 /usr/bin/pip && \
sed -i -e 's/python/python2.6/' /usr/bin/yum
RUN pip install greenlet && \
cd /root && \
git clone https://github.com/unbit/uwsgi.git && \
cd uwsgi && \
CFLAGS="-I/usr/include/python3.5m" UWSGI_PROFILE="asyncio" python uwsgiconfig.py --build
# setting nginx
COPY conf/nginx.conf /etc/nginx/nginx.conf
ADD conf/default.conf /etc/nginx/conf.d/default.conf
RUN usermod -u 1000 nginx
EXPOSE 80
EXPOSE 443
ADD ./conf/start.sh /tmp/start.sh
CMD /bin/sh /tmp/start.sh
途中/usr/bin/yum
の内容をsed
コマンドで置換しているが、python
コマンドでPython3.5にリンクすると、yum
が利用できなくなるため。
nginxの設定ファイル
フレームワークなしで、ws
プロトコルで利用する方法がどうしても見つからなかった。wss
プロトコルのため、https
でアクセスするようにリバースプロキシの設定を施した。
upstream websocket {
server localhost:9090;
}
server {
listen 443;
server_name _;
ssl on;
ssl_certificate /var/www/html/app/server.crt;
ssl_certificate_key /var/www/html/app/server.key;
index index.html index.htm;
charset utf-8;
root /var/www/html/app/client;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location /ws/ {
proxy_pass https://websocket/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location = /favicon.ico {
empty_gif;
}
}
起動バッチ
ノンブロッキングモードを利用しないようにuWSGI
をビルドした場合、--asyncio 10 --greenlet
オプションは不要(オプションが指定されていた場合、エラーとなる)。
また、HTTPS接続でアクセスすることを前提としている。
#!/bin/sh
/etc/init.d/nginx start
cd /var/www/html/app
chmod -R 777 .
/root/uwsgi/uwsgi --asyncio 10 --greenlet --logto uwsgi.log --https :9090,server.crt,server.key --http-websockets --wsgi-file websocket.py
デモ
docker-compose up --build
でビルドして起動する。正常に起動した後、https://127.0.0.1:8443
にアクセスする。
ボタンをクリックすると、以下のようにアラートが"OK"を押下するごとに次々と表示されていく。
また、サーバ側のログ(ここでは、/var/www/html/app/uwsgi.log
)に以下のように出力される。
receive: b'Hello from client'
end
その他
ソケットの受信を別タスクでハンドリングするなど、ノンブロッキングで本来実施したかったことについては、まだ実現できていないので、asyncio
だけでなく、gevent
やgreenlet
についても調査していきたい。
uWSGI
のノンブロッキングモードやWebSocketについての情報があまりないのが、痛いところ。