LoginSignup
14
11

More than 5 years have passed since last update.

Python+uWSGIでWebSocket

Posted at

Python3.5+uWSGIでWebSocketを実現する

uWSGI 1.9からこちらにあるようにWebSocketがサポートされた。簡単にできそうだったので、実際にサンプルを動作させようとしたところ、何度も失敗に終えて、あれこれ調べて気づけば丸一日以上費やしてしまった。もともと「Python+uWSGIでノンブロッキング」と内容をまとめて掲載しようとしたが、同じような方法では実現できなかったので、備忘録も兼ねて投稿することにした。

環境構築

以下、Python3.5、Dockerがインストールされている環境で実行する。
なお、Mac(macOS Sierra)の環境で構築した。

ディレクトリ構成

ディレクトリ構成は以下。

スクリーンショット 2017-07-04 0.54.59.png

nginx-python/conf配下のnginx.confnginx.repo「DockerでPython+uWSGI+Nginxの環境を作成」と同一のため省略。docker-composedockerコマンドの基本的な利用方法についても、そちらに記載。

SSL証明書のCRTとKEYを作成

opensslのインストール

Mac環境で作成した。まずは、brewコマンドでopensslをインストールする。

$ brew install openssl

brewでインストールした最新のopensslコマンドを利用する場合、下記を.bashrcの最後の行に追加。

~/.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もあるが本来不要)。

サンプルコード

サーバ側

クライアントとのハンドシェイクが完了した後で、データの受信待ちをし、受信したデータをそのままクライアントに送信する。

app/websocket.py
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")

クライアント側

ボタンを押下すると、サーバに接続し、データを送信し、応答を待つサンプル。

app/client/index.htm
<!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にフォーワードする。

docker-compose.yml
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"は削除しても問題ない。

nginx-python/Dockerfile
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でアクセスするようにリバースプロキシの設定を施した。

nginx-python/conf/default.conf
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接続でアクセスすることを前提としている。

nginx-python/conf/start.sh
#!/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にアクセスする。

スクリーンショット 2017-07-04 1.22.48.png

ボタンをクリックすると、以下のようにアラートが"OK"を押下するごとに次々と表示されていく。

スクリーンショット 2017-07-04 1.23.04.png
スクリーンショット 2017-07-04 1.23.14.png
スクリーンショット 2017-07-04 1.23.27.png
スクリーンショット 2017-07-04 1.23.35.png

また、サーバ側のログ(ここでは、/var/www/html/app/uwsgi.log)に以下のように出力される。

receive: b'Hello from client'
end

その他

ソケットの受信を別タスクでハンドリングするなど、ノンブロッキングで本来実施したかったことについては、まだ実現できていないので、asyncioだけでなく、geventgreenletについても調査していきたい。
uWSGIのノンブロッキングモードやWebSocketについての情報があまりないのが、痛いところ。

14
11
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
14
11