WebSockets

Websockets 是一种用于客户端和服务器之间进行双向、持久通信的协议。这与 HTTP 不同,HTTP 使用请求/响应模型,即客户端发送请求,服务器进行响应。而使用 websockets,任何一方都可以随时发送消息,另一方可以做出响应。

这使得构建不同类型的应用程序成为可能,包括聊天应用、实时更新的仪表盘和实时协作工具等。如果使用 HTTP,这些应用需要不断地轮询服务器以获取更新。

在 FastHTML 中,你可以使用 @app.ws 装饰器来创建一个 websocket 路由。这个装饰器接受一个路由路径,以及可选的 conndisconn 参数,它们分别代表 websockets 中的 on_connecton_disconnect 回调函数。被 @app.ws 装饰的函数是接收到消息时调用的主函数。

这是一个基本的 websocket 路由示例:

@app.ws('/ws', conn=on_conn, disconn=on_disconn)
async def on_message(msg:str, send):
    await send(Div('Hello ' + msg, id='notifications'))
    await send(Div('Goodbye ' + msg, id='notifications'))

on_message 函数是接收到消息时调用的主函数,你可以随意命名。与标准路由类似,on_message 的参数会自动从 websocket 负载中为你解析,因此你无需手动解析消息内容。但是,某些参数名称被保留用于特殊目的。以下是最重要的几个:

例如,我们可以像这样向刚刚连接的客户端发送一条消息:

async def on_conn(send):
    await send(Div('Hello, world!'))

或者,如果我们从客户端收到一条消息,我们可以向他们回发一条消息:

@app.ws('/ws', conn=on_conn, disconn=on_disconn)
async def on_message(msg:str, send):
    await send(Div('You said: ' + msg, id='notifications'))
    # or...
    return Div('You said: ' + msg, id='notifications')

在客户端,我们可以使用 HTMX 的 websocket 扩展来打开一个 websocket 连接并发送/接收消息。例如:

from fasthtml.common import *

app = FastHTML(exts='ws')

@app.get('/')
def home():
    cts = Div(
        Div(id='notifications'),
        Form(Input(id='msg'), id='form', ws_send=True),
        hx_ext='ws', ws_connect='/ws')
    return Titled('Websocket Test', cts)

这将在 /ws 路由上创建一个到服务器的 websocket 连接,并通过 websocket 将任何表单提交发送到服务器。然后,服务器会通过向客户端回发一条消息来响应。客户端将使用带外交换(Out of Band Swaps)技术,用来自服务器的消息更新消息 div,这意味着内容会被具有相同 id 的元素替换,而无需重新加载页面。

注意

如果你想使用 websockets,请确保在创建 FastHTML 对象时设置 exts='ws',以便加载该扩展。

综上所述,客户端和服务器的代码应如下所示:

from fasthtml.common import *

app = FastHTML(exts='ws')
rt = app.route

@rt('/')
def get():
    cts = Div(
        Div(id='notifications'),
        Form(Input(id='msg'), id='form', ws_send=True),
        hx_ext='ws', ws_connect='/ws')
    return Titled('Websocket Test', cts)

@app.ws('/ws')
async def ws(msg:str, send):
    await send(Div('Hello ' + msg, id='notifications'))

serve()

这是一个相当简单的例子,用标准的 HTTP 请求也能轻松实现,但它展示了 websockets 的基本工作原理。接下来,让我们看一个更复杂的例子。

Websocket 中的会话数据

会话数据在标准 HTTP 路由和 Websockets 之间是共享的。这意味着你可以在 websocket 处理程序中访问例如已登录用户的 ID。

from fasthtml.common import *

app = FastHTML(exts='ws')
rt = app.route

@rt('/login')
def get(session):
    session["person"] = "Bob"
    return "ok"

@app.ws('/ws')
async def ws(msg:str, send, session):
    await send(Div(f'Hello {session.get("person")}' + msg, id='notifications'))

serve()

实时聊天应用

让我们利用新学的 websocket 知识来构建一个简单的聊天应用。我们将创建一个应用,让多个用户可以实时发送和接收消息。

让我们从定义应用和主页开始:

from fasthtml.common import *

app = FastHTML(exts='ws')
rt = app.route

msgs = []
@rt('/')
def home(): return Div(
    Div(Ul(*[Li(m) for m in msgs], id='msg-list')),
    Form(Input(id='msg'), id='form', ws_send=True),
    hx_ext='ws', ws_connect='/ws')

现在,让我们来处理 websocket 连接。我们将为此添加一个新的路由,并附带一个 on_connon_disconn 函数来跟踪当前连接到 websocket 的用户。最后,我们将处理向所有已连接用户发送消息的逻辑。

users = {}
def on_conn(ws, send): users[str(id(ws))] = send
def on_disconn(ws): users.pop(str(id(ws)), None)

@app.ws('/ws', conn=on_conn, disconn=on_disconn)
async def ws(msg:str):
    msgs.append(msg)
    # Use associated `send` function to send message to each user
    for u in users.values(): await u(Ul(*[Li(m) for m in msgs], id='msg-list'))

serve()

我们现在可以用 python chat_ws.py 运行这个应用,并打开多个浏览器标签页访问 https://:5001。你应该能在一个标签页中发送消息,并在其他标签页中看到它们出现。

仍在开发中

这个页面(以及 FastHTML 中的 Websocket 支持)仍在开发中。欢迎提出问题、PR 和反馈!