Python+uWSGIでWebSocket

#Python3.5+uWSGIでWebSocketを実現する
uWSGI 1.9から[こちら](http://uwsgi-docs.readthedocs.io/en/latest/WebSockets.html)にあるようにWebSocketがサポートされた。簡単にできそうだったので、実際にサンプルを動作させようとしたところ、何度も失敗に終えて、あれこれ調べて気づけば丸一日以上費やしてしまった。もともと[「Python+uWSGIでノンブロッキング」](http://qiita.com/hiroykam/items/440d2763d824997b9700)と内容をまとめて掲載しようとしたが、同じような方法では実現できなかったので、備忘録も兼ねて投稿することにした。

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

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

<img width="196" alt="スクリーンショット 2017-07-04 0.54.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/06398498-3dcf-9542-6f0c-e6e9799e4124.png">

`nginx-python/conf`配下の`nginx.conf`と`nginx.repo`は[「DockerでPython+uWSGI+Nginxの環境を作成」](http://qiita.com/hiroykam/items/748c3fab31c616994db9)と同一のため省略。`docker-compose`や`docker`コマンドの基本的な利用方法についても、そちらに記載。

###SSL証明書のCRTとKEYを作成
####opensslのインストール
Mac環境で作成した。まずは、`brew`コマンドで`openssl`をインストールする。

```shell-session
$ brew install openssl
```

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

```text:~/.bashrc
export PATH=$(brew --prefix openssl)/bin:$PATH
```
追加したら、下記を実行。

```shell-session
$ source ~/.bashrc
```

####証明書の作成
`csr`を作成するときにいくつか質問に回答する必要があるが、すべてデフォルト（enterキー押下のみ）とした。

```shell-session
$ 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もあるが本来不要）。

###サンプルコード

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

```py: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")
```

####クライアント側
ボタンを押下すると、サーバに接続し、データを送信し、応答を待つサンプル。

```html: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にフォーワードする。

```yaml: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"`は削除しても問題ない。

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

```text: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接続でアクセスすることを前提としている。

```shell-session: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`にアクセスする。

<img width="448" alt="スクリーンショット 2017-07-04 1.22.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/231aa6a1-d261-4a82-e38d-0529f6c304b0.png">

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

<img width="526" alt="スクリーンショット 2017-07-04 1.23.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/2c371bad-0ad7-c270-fd45-41491b7d14d6.png">
<img width="522" alt="スクリーンショット 2017-07-04 1.23.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/bace691c-d8c7-85a9-4c4e-5acd6e912ade.png">
<img width="524" alt="スクリーンショット 2017-07-04 1.23.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/f8f1c8fd-dc95-086d-1a8b-e2b07f852bd0.png">
<img width="516" alt="スクリーンショット 2017-07-04 1.23.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/b0c044e0-ec60-be46-a6aa-a5e832ff0050.png">

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

```text
receive: b'Hello from client'
end
```

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