介绍
本项目利用服务器发送事件(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 流程
- 客户端向服务器请求一个网页。
- 服务器做出相应响应。
- 服务器将响应发送回客户端。

由于 HTTP 请求是无状态的,即每个请求之后 HTTP 连接都会终止,所以在进行下一个请求以更新相关信息之前,服务器和浏览器完全不知道彼此的状态。此时,不难想到一个简单的解决方案,即浏览器可以定期发送请求来模拟实时效果。这被称为轮询。
轮询流程
- 客户端使用常规 HTTP 向服务器发送连接请求。
- 客户端执行嵌入在网页中的 JavaScript 轮询脚本,定期(例如每 5 秒)向服务器发送请求以获取信息。
- 服务器响应每个请求,并像正常 HTTP 请求一样返回相应信息。

轮询使我们能够近乎实时地获取信息。然而,由于轮询导致浏览器频繁向服务器发送请求,可能会导致性能低下。为了缓解这些问题,人们提出了一种替代方法。服务器不再立即响应客户端请求,而是等待有数据变化(或超时)后再返回响应。这种方法最大限度地提高了连接的有效性,以减少轮询中的请求数量。这种方法被称为长轮询(long polling)。
长轮询流程
- 客户端使用常规 HTTP 向服务器请求一个网页。
- 客户端执行嵌入在网页中的 JavaScript 脚本,向服务器发送数据并请求信息。
- 服务器不立即响应客户端请求,而是等待有效的更新。
- 当信息更新且有效时,服务器将数据推送给客户端。
- 客户端收到服务器的通知后,立即发送新请求以启动下一轮轮询。

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

如果 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 协议下的通信过程。



