使用 FastAPI 和 WebSockets 实现聊天室功能

在本教程中,我们将使用 FastAPI 和 websockets 实现一个简易的聊天室应用程序。FastAPI 是一个高性能框架,主要用于构建 API,但您也可以使用它构建快速应用程序。

我们正在构建的应用程序只有一个页面,聊天页面又名 index.html,用户将在这里互相聊天。

设置项目

https://fastapi.tiangolo.com/,使用此链接设置 FastAPI 项目。您将需要 FastAPI 软件包。

pip install fastapi
pip install uvicorn

设置main.py

from fastapi import *
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import uvicorn

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory = "templates")

@app.get('/')
def home(request: Request):
  return templates.TemplateResponse("index.html", {"request": request})

if __name__ == "__main__":
  return uvicorn.run("main:app")

为了设置文件系统,使“/”返回index.html,需创建一个“static”文件夹和“templates”文件夹。文件系统应遵循如下图所示的层次结构:

使用 FastAPI 和 WebSockets 实现聊天室功能

设置index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket - client</title>
    <style>
    #messages .clientID {
        font-weight: bold;
    }
    </style>
</head>
<body >
    <main>
        <div>
            <h1>Chat App</h1>
            <form id="room-form" class="mb-2">
                <label for="room_id" class="text-slate-8000">Room id:</label>
                <input type="text" id='room-id' class="border p-1" required>
                <button type="submit" id="room-submit">Enter</button>
            </form>
            <div id="message-box">
                <!-- Messages will be added here -->
            </div>
            <form id="message-form">
                <div>
                    <input type="text" id="message" placeholder="Enter your message">
                    <button type="submit" text-slate-300 border-0 w-1/12 outline-0">Send</button>
                </div>
            </form>
        </div>
    </main>

我们唯一的样式是将 clientID 设为粗体,以便它看起来与实际消息不同。该文件的主要部分是两个 form 元素和 div 元素。

id 为“room-form”的<form>元素允许用户输入聊天室 ID。它由“房间 id”输入字段的<label>元素组成,该元素附带一个<input>“文本”类型的字段(id 为“room-id”),供用户输入房间 ID。提供“提交”按钮(ID为“room-submit”)来提交表单。

在此表单下方,有一个<div>id 为“message-box”的内容,其中将显示实际的聊天消息;在使用过程中,这将通过我们的 JS 代码填充消息。

再往下,还有另一个<form>id 为“message-form”的元素,它允许用户输入和发送聊天消息。它包含一个<input>“文本”类型的字段(ID 为“消息”),用户可以在其中键入消息。输入字段的右侧是“发送”按钮,用户可以单击该按钮来发送消息。

设置 main.js

在我的代码中,我只是在 index.html 中 body 元素末尾的 script 标签中加入了它,因为它的 Javascript 代码并不多。

const roomIDInput = document.getElementById("room-id");
const roomForm = document.getElementById('room-form')
const messageForm = document.getElementById("message-form");
const messageInput = document.getElementById("message");
const messageBox = document.getElementById("message-box");

const clientID = Date.now();
const ws;

前五行 JavaScript 代码通过 document.getElementById 获取带有相应 ID 的 HTML 元素。如果两个用户同时访问页面,就有可能分配到相同的const clientID。这就是为什么这段代码最适合在本地主机上使用,而且只需学习 websockets 的逻辑即可。此外,我们还初始化了 ws,稍后将为其分配一个 websocket。

function processMessage(event) {
    const d = JSON.parse(event.data);
    const messageEl = document.createElement("div");
    messageEl.className = "w-full flex justify-start";
    messageEl.innerHTML = `
        <div>
            <p>${d.userID}: ${d.msg}</p>
        </div>`;
    messageBox.appendChild(messageEl);
    messageBox.scrollTop = messageBox.scrollHeight;
}

processMessage(event) 函数通过处理传入的消息在聊天应用程序中发挥着重要作用。首先,它会解析事件中的 JSON 数据,提取用户 ID 和消息内容。随后,它会创建一个新的 HTML div 元素来代表消息,并设置其类别以便格式化。在该元素中,动态插入用户 ID 和信息文本。构建好的消息 div 会附加到聊天界面的 messageBox 中,使其可见。

roomForm.addEventListener("submit", (e) => {
    console.log("here")
    e.preventDefault()
    if (ws) {
        ws.onmessage = processMessage
    }
    else{
        roomID = roomIDInput.value;
        ws = new WebSocket(`ws://localhost:8000/ws/${roomID}/${clientID}`)
        console.log(`ws://localhost:8000/ws/${roomID}/${clientID}`)
        ws.onmessage = processMessage
    }
});

现在,我们为 roomForm 元素添加了一个事件监听器,用于捕捉表单提交。提交表单后,代码会检查 WebSocket 连接 ws 是否已存在。如果存在,就会分配 processMessage 函数来处理传入的聊天信息。但如果不存在 WebSocket 连接,它就会从输入字段中提取房间 ID,建立与特定房间的新 WebSocket 连接,并将其与唯一的客户端 ID 关联。此外,它还会记录 WebSocket 连接的详细信息,以便调试。在这两种情况下,代码都会确保阻止默认的表单提交行为,以便在聊天应用程序中进行房间输入和消息接收的客户端处理。

messageForm.addEventListener("submit", (event) => {
    event.preventDefault();
    const message = messageInput.value;

    ws.send(JSON.stringify({ msg: message, userID: clientID }));
    messageInput.value = "";
});

window.addEventListener("beforeunload", () => {
    if (ws) {
        ws.close();
    }
});

最后,这段 JavaScript 代码为 messageForm 元素和窗口的 “beforeunload “事件添加了事件侦听器。当 messageForm 提交时(当用户发送聊天信息时),它首先会阻止默认的表单提交行为,然后从输入框中获取信息,连同 JSON 格式的客户端唯一 ID 一起发送到 WebSocket 服务器,并清除输入框。卸载前 “事件监听器可确保当用户离开或刷新页面时,如果存在 WebSocket 连接 (ws),该连接将被关闭以清理资源,从而帮助维护聊天系统的完整性并优雅地处理用户断开连接的情况。

完成服务器端(main.py)

class Room:
  def __init__(self, room_id):
    self.room = room_id
    self.connections = []

  async def broadcast(self, message, sender):
    for connection in self.connections:
      print(message)
      await connection.send_text(message)

room_dict = {}

这里我们定义一个Room类,用于管理聊天室中的房间。每个Room实例初始化时都使用一个唯一的room_id,它标识特定的聊天室,并调用一个空列表connections来存储房间中客户端的 WebSocket 连接。Room类中的broadcast方法遍历连接列表,并将给所有连接的客户端发送message,包括sender. 此方法负责向聊天室内的所有用户广播消息。我们还初始化一个字典,当应用程序运行时,该字典将填充各个房间对象。

@app.websocket('/ws/{room_id}/{client_id}')
async def websocket_endpoint(websocket:WebSocket, room_id: str, client_id: str):
    try:
        await websocket.accept()
        if room_id not in room_dict:
            room_dict[room_id] = Room(room_id)

        room  = room_dict[room_id]
        room.connections.append(websocket)

        print(f"connection established for {client_id} in room {room_id}")

        while True:
            data = await websocket.receive_text()
            await room.broadcast(data, websocket)
    except WebSocketDisconnect:
        if room_id not in room_dict:
            room = room_dict[room_id]
            room.connections.remove(websocket)
            if len(room.connections) == 0:
                del room_dict[room_id]

这里我们定义了一个 WebSocket 端点,用于在网络应用程序中管理聊天室功能。当客户端连接到该端点时,代码首先会接受 WebSocket 连接。它会检查 room_dict 中是否存在指定的聊天室,room_dict 是一个用于管理聊天室的字典。如果聊天室不存在,它就会创建一个新的聊天室类实例并将其添加到字典中。然后通过将 WebSocket 连接添加到相应房间实例的连接列表中,将其与聊天室关联起来。客户发送的信息会被连续接收,并通过房间实例的广播方法广播给同一聊天室中所有已连接的客户,确保实时通信。如果 WebSocket 断开连接,则会从连接列表中删除。如果断开连接后聊天室为空,则会从字典中删除,以保持聊天室管理系统的整洁和高效。

if __name__ == "__main__" : 
    uvicorn.run( "main:app" )

最后,该代码块检查脚本是否作为主程序运行,如果是,则使用 uvicorn 启动网络服务器,为变量名为 “app “的 “main “模块中定义的 FastAPI 应用程序提供服务。

结论

该程序制作了一个简单的聊天室应用程序,用户可以连接到不同的房间,并向特定房间中的所有用户发送消息。

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/34523.html

(0)

相关推荐

发表回复

登录后才能评论