注意
この記事は初心者が書いてるので、鵜呑みにしないでください。
環境
- Windows 10 Home
- WSL2
- この記事は全てローカルのPCでの話をしています。デプロイはしていません。
背景
docker-compose upでアプリを起動したら、http通信はうまくいくけど、socketioの通信は機能しない。
結論
socketioのためには、下記のように独自にCORS設定をする必要がある。app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
CORS設定の理解
簡単に説明
通常、あるアプリにアクセスできる「オリジン」は、そのアプリと同一の「オリジン」のみである。しかし、CORS設定を変えれば、アクセスできる「オリジン」を追加することができる。「オリジン」については、下記のリンクなどが参考になりそう。
https://zenn.dev/syo_yamamoto/articles/445ce152f05b02
例で確認する
- app.py
- get_data.py
- templates/
- home.html
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('home.html')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app) #ここが大事
@app.route("/")
def helloWorld():
return "Hello, cross-origin-world!"
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8080)
<!DOCTYPE html>
<html>
<head>
<title>CORS Test</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<h2>CORS Test</h2>
<button id="testButton">Test CORS</button>
<script>
$(document).ready(function(){
$("#testButton").click(function(){
$.get("http://localhost:8080", function(data, status){
alert("Data: " + data + "\nStatus: " + status);
});
});
});
</script>
</body>
</html>
上記のファイルを作る。「py get_data.py」でget_data.pyを動かしつつ、「py app.py」でapp.pyを動かす。http://127.0.0.1:5000/にアクセスすると、app.pyが表示させてるhome.htmlを見ることができる。そこにあるボタンを押すと、http://127.0.0.1:8080/(get_data.py)にアクセスして、情報を取得してalertに表示させることができる。これができるのは、get_data.pyのCORS(app) の部分のおかげである。CORS(app)がなければ、異なるオリジンであるので(http://127.0.0.1:5000/、http://127.0.0.1:8080/)、http://127.0.0.1:8080/(get_data.py)にアクセスすることはできない。CORS(app)がなければ、ボタンを押してもalertは表示されないはずである。
websocketは同一オリジンでも、CORS設定が必要???
static
-styles.css
templates
-home.html
app.py
docker-compose.yml
Dockerfile
nginx.conf
requirements.txt
from flask import Flask, render_template
from flask_socketio import SocketIO, emit
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*") #ここが大事
@app.route('/')
def index():
return render_template('home.html')
@socketio.on('message from js')
def handle_message(message):
emit('message from py', message, broadcast=True)
if __name__ == '__main__':
socketio.run(app, debug=True, host='0.0.0.0', port=5000)
version: '3'
services:
db:
image: mysql:8.0
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_DATABASE: aaa
MYSQL_USER: bbb
MYSQL_PASSWORD: ccc
MYSQL_ROOT_PASSWORD: ddd
volumes:
- db_data:/var/lib/mysql
web:
build: .
command: python app.py
volumes:
- .:/cors_test_2
#ports:
#- "5000:5000"
depends_on:
- db
nginx:
image: nginx:latest
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
#- ./static:/static
- .:/cors_test_2
ports:
- "8010:7010"
depends_on:
- web
volumes:
db_data:
FROM python:3.9
WORKDIR /cors_test_2
ADD requirements.txt .
RUN pip install -r requirements.txt
ADD . .
CMD [ "python", "./app.py" ]
user nginx;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
client_max_body_size 20M;
server {
listen 7010;
location / {
proxy_pass http://web:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocketを使用するための設定
proxy_set_header Connection "upgrade"; # WebSocketを使用するための設定
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Origin $http_origin; # オリジンを伝えるのに必要?
}
location /static/ {
alias /cors_test_2/static/;
}
}
}
Flask==2.1.1
flask-socketio==5.1.1
<!DOCTYPE html>
<html>
<head>
<title>cors_test_2</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<h1 class="red-text">cors_test_2</h1>
<button id="sendButton">Send Message</button>
<h4 id="message_show" class="green-text"></h4>
<script type="text/javascript">
var socket = io();
$(document).ready(function(){
$("#sendButton").click(function(){
socket.emit('message from js', {'msg':'Hello from the client!'});
});
});
socket.on('message from py', (data) => {
document.getElementById('message_show').textContent = data['msg'];
});
</script>
</body>
</html>
.red-text {
color: red;
}
.blue-text {
color: blue;
}
.green-text {
color: green;
}
「docker-compopse build」と「docker-compose up」を実行することで、アプリが起動する。ホストマシンの8010ポート、すなわちhttp://127.0.0.1:8010/にアクセスする。ページが表示されて、ボタンを押して、文字が表示されれば、Socket通信が成功している。通信の流れとしては、ホストマシンの8010ポート、nginxコンテナの7010ポート、webコンテナ(flaskコンテナ)の5000ポートの順番である。nginxコンテナの7010ポート、webコンテナ(flaskコンテナ)の5000ポートは、ホストマシンのポートではなく、そのコンテナのポートを意味しており、このアプリ全体で占有しているホストマシンのポートは8010ポートだけであることに注意。
ここで、CORS設定の考え方で重要になる「オリジン」についてだが、このアプリを構築しているdockerネットワークは、ホストマシンの8010ポート(http://127.0.0.1:8010/)と、同一「オリジン」に属しているらしい。なので、http通信がwebコンテナ(flaskコンテナ)の5000ポートにアクセスするために、追加のCORS設定は必要ない。つまり、CORS(app)という記述は必要ない。しかし、ここで自分は引っかかった。Socket通信は独自にCORS設定をしないと、webコンテナ(flaskコンテナ)の5000ポートにアクセスできないらしい。具体的には、「cors_allowed_origins="*"」という記述が必要である。これによって、Socket通信が可能となる。
「cors_allowed_origins="*"」は、あらゆるオリジンのアクセスを許可してしまうので、本番環境では、アクセスを許可したいオリジンのみを指定してください。
疑問
なぜwebsocketだけ特別なCORS設定が必要なのか?
http通信は、アプリのdockerネットワーク(nginxコンテナ、flaskコンテナ、dbコンテナ(dbを使っている場合))を一つのオリジンとみなしている。しかし、Socket通信では、flaskコンテナが、nginxコンテナからの通信を受け取れない。つまり、同一オリジンとして扱われていない。感覚的には、Socket通信においても、webコンテナ(flaskコンテナ)へのアクセスは、同一のオリジンだから問題ないということになりそうだけど...。分からないです。
参考
- GPT-4
- https://zenn.dev/syo_yamamoto/articles/445ce152f05b02
- https://qiita.com/att55/items/2154a8aad8bf1409db2b