使用 Flask 和 Redis 构建实时聊天室

PythonBeginner
立即练习

介绍

本项目利用服务器发送事件(SSE,Server-Sent Events)和 Redis 实现一个在线聊天室。你需要具备 Python 和 JavaScript 语法的先验知识,以及对 Flask 和 Redis 用法的基本理解。

在本实验的这一部分,我们将学习和实践以下概念:

  • 网络实时通信
  • SSE 的工作原理
  • Redis 的使用

👀 预览

在线聊天室界面预览

🎯 任务

在本项目中,你将学习:

  • 如何使用 Flask 和 SSE 创建一个简单的在线聊天室
  • 如何实现用户登录功能
  • 如何使用 Redis 进行消息存储和检索

🏆 成果

完成本项目后,你将能够:

  • 在 Web 应用程序中设置 SSE 进行实时通信
  • 使用 Redis 在聊天室应用程序中存储和检索消息
  • 在 Flask 中实现用户登录功能

网页实时通信

网络实时通信(Web Real-Time Communication,WebRTC)是一种机制,它使我们能够在无需用户刷新页面的情况下,即时通知网页上的用户某个事件。WebRTC 有许多用途,比如实时聊天和即时通讯。

Web 客户端和服务器之间有几种通信方式。

常规 HTTP 流程

  1. 客户端向服务器请求一个网页。
  2. 服务器做出相应响应。
  3. 服务器将响应发送回客户端。
HTTP 请求响应流程

由于 HTTP 请求是无状态的,即每个请求之后 HTTP 连接都会终止,所以在进行下一个请求以更新相关信息之前,服务器和浏览器完全不知道彼此的状态。此时,不难想到一个简单的解决方案,即浏览器可以定期发送请求来模拟实时效果。这被称为轮询。

轮询流程

  1. 客户端使用常规 HTTP 向服务器发送连接请求。
  2. 客户端执行嵌入在网页中的 JavaScript 轮询脚本,定期(例如每 5 秒)向服务器发送请求以获取信息。
  3. 服务器响应每个请求,并像正常 HTTP 请求一样返回相应信息。
轮询通信图

轮询使我们能够近乎实时地获取信息。然而,由于轮询导致浏览器频繁向服务器发送请求,可能会导致性能低下。为了缓解这些问题,人们提出了一种替代方法。服务器不再立即响应客户端请求,而是等待有数据变化(或超时)后再返回响应。这种方法最大限度地提高了连接的有效性,以减少轮询中的请求数量。这种方法被称为长轮询(long polling)。

长轮询流程

  1. 客户端使用常规 HTTP 向服务器请求一个网页。
  2. 客户端执行嵌入在网页中的 JavaScript 脚本,向服务器发送数据并请求信息。
  3. 服务器不立即响应客户端请求,而是等待有效的更新。
  4. 当信息更新且有效时,服务器将数据推送给客户端。
  5. 客户端收到服务器的通知后,立即发送新请求以启动下一轮轮询。
长轮询流程图

上述方法通常用于实现网络实时通信。然而,在 HTML5 引入之后,我们有了更好的选择。在 HTML5 中,我们可以使用服务器发送事件(Server-Sent Events,SSE)或 WebSocket。SSE 专为服务器向客户端推送数据而设计,对于诸如广播比赛信息或股票价格变化等场景通常就足够了。

服务器发送事件流程

  1. 客户端使用常规 HTTP 向服务器请求一个网页。
  2. 客户端使用嵌入在网页中的 JavaScript 与服务器建立连接。
  3. 当服务器有更新时,它向客户端发送一个事件。
SSE 通信流程图

如果 SSE 不能满足我们的需求,我们可以改用 WebSocket。通过 WebSocket,在浏览器和服务器之间建立了一个全双工通信通道,允许实时双向消息交换,类似于使用 TCP 套接字。

SSE 与 WebSocket 的简单比较

  • WebSocket 是一个全双工通信通道,支持双向通信且具有更高级的功能。SSE 是一个单向通道,服务器只能向浏览器发送数据。
  • WebSocket 是一个新协议,需要服务器端支持,而 SSE 部署在 HTTP 协议之上,由现有服务器软件支持。
  • SSE 是一个轻量级协议,相对简单,而 WebSocket 是一个重量级协议,相对复杂。

在了解了这些实现网络实时通信的机制之后,我们现在将使用 SSE 来实现一个简单的在线聊天室。

✨ 查看解决方案并练习

基于服务器发送事件(SSE)实现在线聊天室

在在线聊天室中推送消息有多种方式,在本课程中,我们将使用服务器发送事件(Server-Sent Events,SSE)来实现这一点。为了便于接收消息,我们将利用 Redis 的发布/订阅(pub/sub)功能来收发消息。在 Web 服务器端,我们将使用 Flask 来实现。

SSE 的工作原理

在之前的课程中,我们了解到 SSE 是基于 HTTP 的。那么浏览器如何知道这是一个服务器发送的事件流呢?其实很简单——只需将 HTTP 请求的 Content-Type 头设置为 text/event-stream 即可。SSE 本质上是浏览器向服务器发送一个 HTTP 请求,然后服务器以单向方式持续向浏览器推送信息。这些信息的格式也非常简单,前缀为“data:”,后面跟着消息内容,最后以“\n\n”结尾。

Redis 发布/订阅功能

Redis 是一个流行的内存数据库,可用于缓存、队列等服务。在本课程中,我们将使用 Redis 的发布/订阅功能。简单来说,订阅功能允许我们订阅各种频道,每当有新消息发布到这些频道时,我们会自动收到它们。当服务器通过 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>在线聊天登录</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">
    用户名:<input name="user" />
    <input type="submit" value="登录" />
  </form>
</body>

login.html 文件中,我们利用了 Flask 的模板功能,并使用 {{ user }} 来表示从 flask.session 中获取的用户名。

✨ 查看解决方案并练习

实现 index.html 模板

接下来,让我们在 templates 目录中创建一个 index.html 文件来实现聊天室页面。将以下代码写入其中:

<!doctype html>
<title>在线聊天</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>嗨,{{ user }}!</b></p>
<p>消息:<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,输入一个随机用户名,进入聊天室。

你也可以在私密模式下打开另一个浏览器窗口进入聊天室,然后你就可以在两个窗口中聊天了。

聊天室界面截图
✨ 查看解决方案并练习

总结

本项目利用服务器发送事件(SSE)来实现实时网络通信功能,并借助 Flask 和 Redis 创建了一个在线聊天室。目的是让大家通过这部分实验了解 HTTP 协议下的通信过程。