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

PythonPythonBeginner
立即练习

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

本项目利用服务器发送事件(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/[email protected]/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协议下的通信过程。