Flask Full-stack + Flask-SocketIO + Nginx + Gunicorn[2/2]


前述の内容では、正しく実装されていない場合に発生する可能性のある問題から、WebSocketとSocketIOの理解とFlask-SocketIOの初期化方法について理解した.今回の記事では、FlashでFlask、Nginx、Guncornとソケット通信を行う簡単な例のソースコード、クライアント構成方法、およびFlaskサンプルコードについて説明します.

4. Nginx + Gunicorn + Flask-SocketIO


4.1 Nginx config


Nginx config of Flask-SocketIO Docs


Flask SocketIOの場合、Docsは次のNginx設定を提供します.
server {
    listen 80;
    server_name _;

    location / {
        include proxy_params;
        proxy_pass http://127.0.0.1:5000;
    }

    location /static {
        alias <path-to-your-application>/static;
        expires 30d;
    }

    location /socket.io {
        include proxy_params;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://127.0.0.1:5000/socket.io;
    }
}
設定では、proxy passを使用して内部駆動フラスコを接続し、location /socket.ioを使用してsocketを接続します.入力ioのパケットヘッダをアップグレードして転送する内容が書かれています.また、上記の設定はhttpでのみ記述されるため、httpを構成するために追加の設定が必要です.

Nginx config + SSL

# Nginx configuration

# HTTP -  Listen 80 
server {
    listen          80;
    # listen          [::]:80;
    server_name     example.com;

    access_log      /var/www/some/logs/http/access.log;
    error_log       /var/www/some/logs/http/error.log;
    
    # include         /etc/nginx/snippets/letsencrypt.conf;
    location ~ /\.well-known/acme-challenge/ {
        allow all;
        root  /var/www/letsencrypt;
    }

    location / {
        return 301  https://$server_name$request_uri;
        # expires epoch;
    }
    
}

# HTTPS - Listen 443
server {
    listen          443 ssl;
    # listen          [::]:443;
    server_name     example.com;

    access_log      /var/www/some/logs/https/access.log;
    error_log       /var/www/some/logs/https/error.log;

    # letsencrypt should add new group.
    # And also chgrp /etc/letsencrypt/live, /etc/letsencrypt/archive
    # /etc/nginx.conf ==> user [added user name]
    ssl_certificate     /etc/letsencrypt/live/$server_name/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/$server_name/privkey.pem;

    ssl_protocols       TLSv1.2 TLSv1.3 SSLv3;
    ssl_ciphers         ALL:!ADH:!EXPORT56:!RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;

    ssl_prefer_server_ciphers on;

    location / {
        include             proxy_params;
        # proxy_pass          http://unix:/var/www/some/some.sock;
        proxy_pass          http://127.0.0.1:8080;
    }

    location /socket.io {
        include             proxy_params;
        proxy_http_version  1.1;
        proxy_buffering     off;
        proxy_redirect      off;
        proxy_set_header    Upgrade $http_upgrade;
        proxy_set_header    Connection "Upgrade";
        proxy_pass          http://127.0.0.1:8080/socket.io;
    }
}
ここでlocation/socket.ioセクションでは、proxy setヘッダのUpgrade、Connectionがコアに必要なセクションです.ソケット通信を行うには、http、httpsではなくws(またはwss)を使用して通信する必要があります.そうでない場合、ソケット通信は非常に不安定であり、各接続のクライアントは新しい接続を要求し続けるため、101アップグレードパッケージを使用してWebソケット接続を実行する必要があります.

4.2 Gunicorn


Guncorn位置の確認


サービスfile内のコンテンツを作成するには、まずgunicornインストール場所を見つける必要があります.
$ which -a gunicorn

/usr/local/bin/gunicorn
/usr/bin/gunicorn
/bin/gunicorn

サービスファイルの作成


/etc/systemd/systemm/ディレクトリ内/etc/systemd/systemm/[servicename].サービスの作成
sudo vim /etc/systemd/system/myproject.service
確認したGuncorn位置を使用して、ExecStartコマンドを使用して実行位置を作成します.
# /etc/systemd/system/~~~.service
[Unit]
Description=Gunicorn instance to serve myflask
After=network.target

[Service]
# You can change your own account
User=flaskuser
# You can change your own group
Group=flaskcert
# /home/userme/myproject
WorkingDirectory=/var/www/some
# "PATH=/home/userme/myproject/myproject-env/bin"
Environment="PATH=/usr/bin" 
# You must chown working directory
ExecStart=/usr/bin/gunicorn --worker-class eventlet -w 1 -b 127.0.0.1:8080 wsgi:app

[Install]
WantedBy=multi-user.target

エラーと解決方法


gunicorn-worker-class eventlettを使用するには、適切なeventlettバージョンをインストールする必要があります.eventlettのバージョンが適切でない場合、次のエラーが発生する可能性があります.
$ gunicorn --worker-class eventlet -w 1 -b 127.0.0.1:8080 wsgi:app

...
ImportError: cannot import name 'ALREADY_HANDLED' from 'eventlet.wsgi'
...
これらのエラーが発生した場合は、解決策を添付します.
$ pip3 install eventlet==0.30.2

4.3 Flask-SocketIO


sockets/init.pyの作成

# file : app/sockets/__init__.py
from flask_socketio import SocketIO

socketio = SocketIO()

init.pyの作成

# file : app/__init__.py
from flask import Flask
from app.sockets import socketio # app/sockets/__init__.py에 선언된 socketio 변수

def create_app():
    app = Flask(__name__)

    # Your logics...

    # Set Config
    app.config.from_object(config)

    with app.app_context():
        # Socket.IO
        socketio.init_app(  app,
                            async_mode="eventlet",
                            cors_allowed_origins="*",
                            logger=True,
                            engineio_logger=True)
        # Your logics...
        return app

5.クライアント構成(Bootstrap+Javascript)


クライアント構成は、完全なスタックとしてFlashを使用し、Bootstrap+socketです.ioライブラリなどの方法を紹介したいと思います.

5.1 Socket.iojavascriptサンプルコード

// important!! - socket.io version
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script>

var namespace   = "/socket/main";
var socket_main = io.connect(namespace, { transports :  ['websocket',],
                                          upgrade:      true,    // 삭제해도 무관
                                          secure:       true }); // 삭제해도 무관
socket_main.on('announce', function(msg) {
  console.log(msg);
}); // socket_main.on end
上のようにcdnからioライブラリをインポートし、コードを記述できます.ただし、Flask-StocketIOでは、socketは特定バージョンのsocketではありません.ioライブラリを使用すると、バージョンが一致しないエラーが発生する可能性があります.なお、ここでupgrade、secure部分はhttp、httpsなどの環境によって異なるので削除しても構わない.(Defaultに設定されているため)
例示コードでは、socket_mainは任意の変数であり、on('~~',function(msg){})関数内の通知も任意の文字列値である.これは、emit()関数を使用して特定の値を渡すときに指標として使用されます.

6.Flashサンプルソースコード


6.1 Flaskでのsocket通信


Flask内でソケット通信を実現するのは、論理を実現する方法によって異なる場合があります.ここでは、ソースコードの例を簡単に説明します.

app/sockets/init.py

from flask_socketio import SocketIO

socketio = SocketIO()

test.py

# Your logics...

from app.sockets    import socketio

@app.route("/test", methods=['GET'])
def test():
    socketio.emit(  "announce",
                    {"test" : "test success"},
                    namespace='/socket/main',
                    broadcast=True)
    return "Some text"
ここでテストpyソースでsocketio.emit()関数で適切なパラメータを設定し/test pathに入ると、適切なフィードバックが得られます.

終了時..。


研究の過程で多くの曲折を経験した.特にFlask-StocketIOを構成するコンテンツでは、socketio.init_app()関数を使用して構成されたコンテンツが見つかりません.そのため、ほとんどが実験結果に基づいて文章を書いている.特に、Flask-SocketIOを使用してテスト経験を記述したブログの大部分を見ても、適切なフィードバックが得られなかった.

私を死なせる方法。


成功の数日前まで101 Upgradeパケット要求は失敗した.この時点でFlask+Flask-StocketIO+Guncorn+NginxとSSLの応用が完了した.しかし,Web上でWeb Socketを要求すると,400 Badリクエストが応答として常に存在する.このときのサーバドライバ環境.

Guncornコマンド

$ gunicorn -w 3 -b 127.0.0.1:8080 wsgi:app
このときgevent,eventlett,geventtwebsocket.gunicorn.workers.GeventWebSocketWorkerなどのコマンドは使用されていません.これまでGuniornの問題ではなく、nginxの問題だったと思います.

Pythonドライバ

# file : app/__init__.py
from flask import Flask
from app.sockets import socketio # app/sockets/__init__.py에 선언된 socketio 변수

def create_app():
    app = Flask(__name__)

    # Your logics...

    # Set Config
    app.config.from_object(config)

    with app.app_context():
        # Socket.IO
        socketio.init_app(  app,
                            async_mode="gevent",
                            cors_allowed_origins="*")
        # Your logics...
        return app

Nginx設定


Nginxの設定は、前述のアプリケーションSSLのプロファイルと同じです.
実験を続けていくうちに、できる限りのテストが行われても400 Badのリクエストは同じなのでGuncornで問題点を探します.最初にテストするコマンドは次のとおりです.
$ pip3 install gevent

$ gunicorn -w 3 -b 127.0.0.1:8080 wsgi:app
このとき最も重要な変化101 Upgradeパケットは正常に伝送され、応答も正常に回復する.図に示すように、101交換プロトコルは正常に戻ります.この時点で進めない研究が進んでいくのは安心だ.

通信だけが正常ではない.次のエラーが発生し、スロットが不安定でサービスが提供されないためです.しかしこのとき,NginxではなくGunicornの設定を変更すると,答えの直感が得られるので,種々の実験を行った.
[2021-08-01 18:52:03 +0000] [62584] [ERROR] Socket error processing request.
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/gunicorn/workers/sync.py", line 135, in handle
    self.handle_request(listener, req, client, addr)
  File "/usr/local/lib/python3.8/dist-packages/gunicorn/workers/sync.py", line 191, in handle_request
    six.reraise(*sys.exc_info())
  File "/usr/local/lib/python3.8/dist-packages/gunicorn/six.py", line 625, in reraise
    raise value
  File "/usr/local/lib/python3.8/dist-packages/gunicorn/workers/sync.py", line 183, in handle_request
    resp.close()
  File "/usr/local/lib/python3.8/dist-packages/gunicorn/http/wsgi.py", line 409, in close
    self.send_headers()
  File "/usr/local/lib/python3.8/dist-packages/gunicorn/http/wsgi.py", line 329, in send_headers
    util.write(self.sock, util.to_bytestring(header_str, "ascii"))
  File "/usr/local/lib/python3.8/dist-packages/gunicorn/util.py", line 304, in write
    sock.sendall(data)
OSError: [Errno 9] Bad file descriptor

Guncorn指令実験


まずGuncorn命令を実験するために種々の実験を行った.
# Failed - 502 Bad Gateway
gunicorn -k gevent -w 1 -b 127.0.0.1:8080 wsgi:app

# Failed - 502 Bad Gateway
$ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 wsgi:app

# Failed - ImportError: cannot import name 'ALREADY_HANDLED' from 'eventlet.wsgi'
$ gunicorn --worker-class eventlet -w 1 -b 127.0.0.1:8080 wsgi:app

# Success - pip3 install eventlet==0.30.2
$ gunicorn --worker-class eventlet -w 1 -b 127.0.0.1:8080 wsgi:app
上記の過程でeventlettは満足のいく結果を得た.ただし、eventlettをインストールする場合は、バージョンをダウングレードする必要があります.

ポスト


上記の実験過程による研究では,参考になる資料の欠如に加えて,ウェブサイト,Nginx,Guncornの理解が欠けている.しかし、見慣れない環境構成やライブラリの使用については、1週間の研究時間が思ったより短く、満足のいく結果が得られたことをうれしく思います.研究時間が足りなかったので仮眠して行ったのですが…たとえ困難にぶつかっても、たまに成功した結果が出ても、眠れず、徹夜で研究します.今回の研究では、Python Flashフレームワークを活用してより多くの機能を提供する方法について理解しました.