Dockerでの開発で、Http通信は正常なのに、Socket通信が機能しない。WebSocketは独自にCORS設定が必要。

<h2>注意</h2>
<p>この記事は初心者が書いてるので、鵜呑みにしないでください。</p>
<h2>環境</h2>
<ul>
<li>Windows 10 Home</li>
<li>WSL2</li>
<li>この記事は全てローカルのPCでの話をしています。デプロイはしていません。</li>
</ul>
<h2>背景</h2>
<p>docker-compose upでアプリを起動したら、http通信はうまくいくけど、socketioの通信は機能しない。</p>
<h2>結論</h2>
socketioのためには、下記のように独自にCORS設定をする必要がある。

```python:WSLバージョン
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*") 
```
<h2>CORS設定の理解</h2>
<h3>簡単に説明</h3>
<p>通常、あるアプリにアクセスできる「オリジン」は、そのアプリと同一の「オリジン」のみである。しかし、CORS設定を変えれば、アクセスできる「オリジン」を追加することができる。「オリジン」については、下記のリンクなどが参考になりそう。</p>
<p>https://zenn.dev/syo_yamamoto/articles/445ce152f05b02</p>
<h3>例で確認する</h3>

```python:ディレクトリ構造
- app.py
- get_data.py
- templates/
    - home.html
```
```python:app.py
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)
```
```python:get_data.py
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)
```
```python:home.html
<!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>
```
<p>上記のファイルを作る。「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は表示されないはずである。</p>

<h2>websocketは同一オリジンでも、CORS設定が必要？？？</h2>

```python:ディレクトリ構造
static
    -styles.css
templates
    -home.html
app.py
docker-compose.yml
Dockerfile
nginx.conf
requirements.txt
```
```python:app.py
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)
```
```python:docker-compose.yml
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:
```
```python:Dockerfile
FROM python:3.9

WORKDIR /cors_test_2

ADD requirements.txt .
RUN pip install -r requirements.txt

ADD . .

CMD [ "python", "./app.py" ]
```
```python:nginx.conf
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/;
        }
    }
}
```
```python:requirements.txt
Flask==2.1.1
flask-socketio==5.1.1
```
```html:home.html
<!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>
```
```css:styles.css
.red-text {
    color: red;
}
.blue-text {
    color: blue;
}
.green-text {
    color: green;
}
```
<p>「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ポートだけであることに注意。</p>
<p>ここで、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通信が可能となる。</p>
<p>「cors_allowed_origins="*"」は、あらゆるオリジンのアクセスを許可してしまうので、本番環境では、アクセスを許可したいオリジンのみを指定してください。</p>


<h2>疑問</h2>
<h3>なぜwebsocketだけ特別なCORS設定が必要なのか？</h3>
<p>http通信は、アプリのdockerネットワーク（nginxコンテナ、flaskコンテナ、dbコンテナ(dbを使っている場合)）を一つのオリジンとみなしている。しかし、Socket通信では、flaskコンテナが、nginxコンテナからの通信を受け取れない。つまり、同一オリジンとして扱われていない。感覚的には、Socket通信においても、webコンテナ(flaskコンテナ)へのアクセスは、同一のオリジンだから問題ないということになりそうだけど...。分からないです。</p>


<h2>参考</h2>
<ul>
<li>GPT-4</li>
<li>https://zenn.dev/syo_yamamoto/articles/445ce152f05b02</li>
<li>https://qiita.com/att55/items/2154a8aad8bf1409db2b</li>
</ul>