Flask と Redis を使ったリアルタイムチャットルーム

PythonBeginner
オンラインで実践に進む

はじめに

このプロジェクトでは、SSE(Server-Sent Events)と Redis を利用してオンラインチャットルームを実装します。Python と JavaScript の構文の事前知識、および Flask と Redis の使い方の基本的な理解が必要です。

この実験のこのセクションでは、以下の概念を学び、練習します。

  • Web リアルタイム通信
  • SSE の機能
  • Redis の利用

👀 プレビュー

オンラインチャットルームインターフェイスのプレビュー

🎯 タスク

このプロジェクトでは、以下を学びます。

  • Flask と SSE を使って簡単なオンラインチャットルームを作成する方法
  • ユーザーログイン機能を実装する方法
  • Redis を使ってメッセージの保存と取得を行う方法

🏆 成果

このプロジェクトを完了すると、以下のことができるようになります。

  • Web アプリケーションにおけるリアルタイム通信のために SSE をセットアップする
  • チャットルームアプリケーションで Redis を使ってメッセージを保存し、取得する
  • Flask においてユーザーログイン機能を実装する

ウェブリアルタイム通信

Web リアルタイム通信(WebRTC)は、ユーザーがページを更新する必要なく、Web ページ上のイベントを即座に通知することができる仕組みを指します。WebRTC には、リアルタイムチャットや即時メッセージングなど、多くの用途があります。

Web クライアントとサーバー間の通信方法はいくつかあります。

通常の HTTP フロー

  1. クライアントがサーバーから Web ページを要求します。
  2. サーバーが適切に応答します。
  3. サーバーが応答をクライアントに返します。

HTTP 要求応答フロー

HTTP 要求はステートレスであり、つまり各要求後に HTTP 接続が終了するため、次の要求が行われて関連情報が更新されるまで、サーバーとブラウザはお互いをまったく認識しません。この時点で、ブラウザが定期的な要求を行ってリアルタイムの効果をシミュレートするという簡単な解決策を考えるのは難しくありません。これをポーリングと呼びます。

ポーリングフロー

  1. クライアントが通常の HTTP を使ってサーバーに接続要求を送信します。
  2. クライアントは Web ページに埋め込まれた JavaScript ポーリングスクリプトを実行して、定期的に(たとえば 5 秒ごとに)サーバーに要求を送信して情報を取得します。
  3. サーバーは各要求に応答し、通常の HTTP 要求と同じように対応する情報を返します。

ポーリング通信図

ポーリングにより、我々はほぼリアルタイムで情報を取得することができます。ただし、ポーリングの結果、ブラウザからサーバーに頻繁に要求が行われると、パフォーマンスの効率が悪くなる可能性があります。これらの問題を軽減するために、代替方法が提案されました。サーバーは、クライアント要求に即座に応答するのではなく、データ変更(またはタイムアウト)があるまで待ってから応答を返します。このアプローチは、接続の有効性を最大化してポーリングにおける要求回数を減らします。この方法をロングポーリングまたは Long-Polling と呼びます。

ロングポーリングフロー

  1. クライアントが通常の HTTP を使ってサーバーから Web ページを要求します。
  2. クライアントは Web ページに埋め込まれた JavaScript スクリプトを実行して、データを送信してサーバーに情報を要求します。
  3. サーバーはクライアントの要求に即座に応答するのではなく、有効な更新があるまで待ちます。
  4. 情報が更新され有効になると、サーバーはデータをクライアントにプッシュします。
  5. サーバーの通知を受け取ると、クライアントは直ちに新しい要求を送信して次のポーリングラウンドを開始します。

ロングポーリングフロー図

上記の方法は、通常、Web リアルタイム通信を実装するために使用されます。ただし、HTML5 の導入後、もっと良いオプションがあります。HTML5 では、Server-Sent Events(SSE)または WebSocket を使用できます。SSE は、特に試合情報や株価変動の放送などのシナリオに十分な、サーバーからクライアントへのデータプッシュに特化しています。

Server-Sent Events フロー

  1. クライアントが通常の HTTP を使ってサーバーから Web ページを要求します。
  2. クライアントは Web ページに埋め込まれた JavaScript を使ってサーバーと接続を確立します。
  3. サーバーに更新があると、それをクライアントにイベントとして送信します。

SSE 通信フロー図

SSE が我々のニーズを満たさない場合、代わりに WebSocket を使用できます。WebSocket を使うと、ブラウザとサーバー間にフルデュプレックス通信チャネルが確立され、TCP ソケットを使うのと同じように、双方向のメッセージ交換が可能になります。

SSE と WebSocket の単純な比較

  • WebSocket は双方向通信をサポートするフルデュプレックス通信チャネルで、より高度な機能を備えています。SSE は、サーバーがブラウザにのみデータを送信できる片方向チャネルです。
  • WebSocket は新しいプロトコルであり、サーバーサイドのサポートが必要ですが、SSE は HTTP プロトコルの上に展開され、既存のサーバーソフトウェアによってサポートされます。
  • SSE は軽量なプロトコルで比較的単純ですが、WebSocket は重たいプロトコルで比較的に複雑です。

これらの Web リアルタイム通信の実装メカニズムを理解したので、次に SSE を使って簡単なオンラインチャットルームを実装します。

SSE を基にしたオンラインチャットルームの実装

オンラインチャットルームでメッセージをプッシュする方法は様々ですが、このコースでは Server-Sent Events(SSE)を使ってこれを実現します。メッセージの受信を容易にするために、Redis の pub/sub(publish/subscribe)機能を利用してメッセージの受信と送信を行います。Web サーバー側では、Flask を使って実装します。

SSE の仕組み

前のレッスンで学んだように、SSE は HTTP に基づいています。では、ブラウザはどのようにこれがサーバーから送信されるイベントストリームであることを知るのでしょうか?実はとても簡単で、HTTP 要求の Content-Type ヘッダを text/event-stream に設定するだけです。SSE は基本的に、ブラウザがサーバーに HTTP 要求を送信し、その後サーバーがブラウザに対して片方向で情報を継続的にプッシュするものです。これらの情報の形式も非常に単純で、接頭辞 "data:" の後にメッセージの内容が続き、"\n\n" で終わります。

Redis の pub/sub 機能

Redis は、キャッシング、キューイング、その他のサービスに使用できる人気のあるインメモリデータベースです。このコースでは、Redis の publish/subscribe 機能を使用します。簡単に言えば、購読機能を使うと、さまざまなチャネルに購読することができ、これらのチャネルに新しいメッセージが投稿されるたびに、自動的に受信することができます。サーバーが POST 要求を介してブラウザから送信されたメッセージを受け取ると、これらのメッセージを特定のチャネルに投稿します。その後、これらのチャネルに購読しているクライアントは自動的にこれらのメッセージを受信し、それが SSE を介してクライアントにプッシュされます。

機能の実装

上記の分析の後、チャットルームの全体の流れは既に明確になっています。では、このチャットルームの機能を実装し始めましょう。

~/project ディレクトリに app.py という名前のファイルを作成し、次のソースコードを入力します。

import datetime
import flask
import redis

app = flask.Flask("labex-sse-chat")
app.secret_key = "labex"
app.config["DEBUG"] = True
r = redis.StrictRedis()


## ホームルート関数
@app.route("/")
def home():
    ## ユーザーがログインしていない場合、ログインページにリダイレクトする
    if "user" not in flask.session:
        return flask.redirect("/login")
    user = flask.session["user"]
    return flask.render_template("index.html", user=user)


## メッセージ生成器
def event_stream():
    ## 購読配信システムを作成する
    pubsub = r.pubsub()
    ## 購読配信システムの subscribe メソッドを使ってチャネルを購読する
    pubsub.subscribe("chat")
    for message in pubsub.listen():
        data = message["data"]
        if type(data) == bytes:
            yield "data: {}\n\n".format(data.decode())


## ログイン関数、初回訪問時にはログインが必要
@app.route("/login", methods=["GET", "POST"])
def login():
    if flask.request.method == "POST":
        ## セッション辞書にユーザー名を格納し、その後ホームページにリダイレクトする
        flask.session["user"] = flask.request.form["user"]
        return flask.redirect("/")
    return flask.render_template("login.html")


## POST メソッドを使って JavaScript から送信されたデータを受け取る
@app.route("/post", methods=["POST"])
def post():
    message = flask.request.form["message"]
    user = flask.session.get("user", "anonymous")
    now = datetime.datetime.now().replace(microsecond=0).time()
    r.publish("chat", "[{}] {}: {}\n".format(now.isoformat(), user, message))
    return flask.Response(status=204)


## イベントストリームインターフェイス
@app.route("/stream")
def stream():
    ## このルート関数の返却オブジェクトは必ず text/event-stream 型でなければならない
    return flask.Response(event_stream(), mimetype="text/event-stream")


## Flask アプリケーションを実行する
app.run()

上記のコードでは、Flask のセッション機能を使ってユーザーのログイン情報を保存し、Redis の購読配信機能を使ってメッセージの受信と送信を行い、SSE を使ってメッセージのプッシュを実装しています。

ここで:

  • event_stream 関数は、Redis からメッセージを継続的に取得してクライアントにプッシュするメッセージ生成器です。
  • stream 関数は、text/event-stream 型のオブジェクトを返すイベントストリームインターフェイスであり、SSE イベントストリームです。
  • post 関数は、POST メソッドを使って JavaScript から送信されたデータを受け取るインターフェイスです。受け取ったデータを Redis の chat チャネルに投稿します。
  • login 関数は、初回訪問時に必要なログイン関数です。ログイン成功後、ユーザー名をセッション辞書に保存し、その後ホームページにリダイレクトします。
  • home 関数は、ホームページのルート関数です。ユーザーがログインしていない場合、ログインページにリダイレクトします。ユーザーがログインしている場合、index.html テンプレートをレンダリングします。

login.html テンプレートの実装

まず、~/project の下に templates ディレクトリを作成して、必要な HTML ファイルを保存します。templates ディレクトリの中に login.html ファイルを作成し、次のコードを書き込みます。

<!doctype html>
<title>Online Chat Login</title>
<style>
  body {
    max-width: 500px;
    margin: auto;
    padding: 1em;
    background: black;
    color: #fff;
    font:
      16px/1.6 menlo,
      monospace;
  }
</style>

<body>
  <form action="" method="post">
    User Name: <input name="user" />
    <input type="submit" value="login" />
  </form>
</body>

login.html ファイルでは、Flask のテンプレート機能を利用して、{{ user }} を使ってユーザー名を表しており、これは flask.session から取得されます。

index.html テンプレートの実装

次に、チャットルームページを実装するために、templates ディレクトリに index.html ファイルを作成しましょう。その中に次のコードを書き込みます。

<!doctype html>
<title>Online Chat</title>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<style>
  body {
    max-width: 500px;
    margin: auto;
    padding: 1em;
    background: black;
    color: #fff;
    font:
      16px/1.6 menlo,
      monospace;
  }
</style>
<p><b>Hi, {{ user }}!</b></p>
<p>Message: <input id="in" /></p>
<pre id="out"></pre>
<script>
  function sse() {
    // サーバーのイベントストリームに接続する
    var source = new EventSource("/stream");
    var out = document.getElementById("out");
    source.onmessage = function (e) {
      out.innerHTML = e.data + "\n" + out.innerHTML;
    };
  }
  // メッセージをサーバーに POST する
  $("#in").keyup(function (e) {
    if (e.keyCode == 13) {
      $.post("/post", { message: $(this).val() });
      $(this).val("");
    }
  });
  sse();
</script>

index.html ファイルでは、jQuery の $.post メソッドを使ってメッセージをサーバーに送信し、EventSource を使ってサーバーからのプッシュメッセージを受信しています。

実行とテスト

Redis を使用しているため、環境内で Redis サービスを起動し、Python を Redis サーバーに接続するために必要な redis モジュールをダウンロードする必要があります。

pip install redis
sudo service redis-server start

次に、チャットルームを実行できます。

cd ~/project
python app.py

その後、ブラウザで http://localhost:5000 にアクセスし、適当なユーザー名を入力してチャットルームに入ることができます。

また、プライベートモードで別のブラウザウィンドウを開いてチャットルームに入り、両方のウィンドウでチャットすることもできます。

Chatroom interface screenshot

まとめ

このプロジェクトでは、SSE を利用してリアルタイムのウェブ通信機能を実装し、Flask と Redis に依存してオンラインチャットルームを作成しています。目的は、このセクションの実験を通じて、HTTP プロトコル下での通信プロセスを皆さんに理解してもらうことです。

✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習