关于 chat-room
面向使用 RivetKit 构建实时聊天室后端的开发者技能。示例采用每个房间一个 Actor,以 SQLite 保存消息历史,并通过事件向已连接客户端广播新消息。
主要功能
- 01每个房间使用独立 Actor
- 02SQLite 保存消息历史
- 03按消息 ID 顺序读取历史
- 04广播新消息事件
- 05React 前端示例
适用场景
- 01构建按房间隔离的聊天室
- 02持久化并加载房间消息记录
- 03向所有在线房间客户端广播消息
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rivet-dev/ skills --skill chat-room安装后在 Claude Code 中输入/chat-room确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
BASE_SKILL.mdindex.jsonopenapi.jsonreference/actors/access-control.mdactions.mdactor-runtime-socket.mdappearance.mdauthentication.mdcommunicating-between-actors.mdconnections.mddebugging.mddesign-patterns.mddestroy.mderrors.mdevents.mdfetch-and-websocket-handler.mdhelper-types.mdhttp-api.mdinput.mdinspector-tabs.mdkeys.mdkv.mdlifecycle.mdlimits.mdmetadata.mdqueues.mdreference/actors/quickstart/backend.mdcloudflare.mdeffect.mdnext-js.md详细介绍
聊天室
重要:开始任何操作之前,必须先阅读此技能目录中的 BASE_SKILL.md。其中包含有关调试、错误处理、状态管理、部署和项目设置的重要指导。这些规则和模式适用于所有 RivetKit 工作。以下内容均假设你已阅读并理解这些内容。
可运行示例
如需参考实现,请阅读这些模板中的原始可运行示例代码:
使用 RivetKit 构建聊天室后端的模式:房间级 Actor、持久化消息历史记录,以及通过 WebSocket 连接实时传递消息。
入门代码
从 GitHub 上的可运行示例开始并进行改造。后端是一个 chatRoom Actor;前端是使用 @rivetkit/react 的 React 应用(参见 React 快速入门)。
| 主题 | 摘要 |
|---|---|
| 房间模型 | 每个房间键对应一个 chatRoom Actor。前端默认将键设为 general;输入不同的房间名称会连接到不同的 Actor。 |
| 历史记录 | 在 db({ onMigrate }) 中创建 SQLite messages 表,并使用 ORDER BY id ASC 读取。 |
| 消息传递 | sendMessage 插入数据行,然后向每个已连接的客户端广播类型化的 newMessage 事件。 |
| 身份 | 示例中未设置身份。sender 是普通的操作参数;用于生产环境时,应将身份绑定到连接。 |
每个房间对应一个 Actor 的模型
每个房间都是一个 Rivet Actor 实例,通过 键寻址。客户端调用 useActor({ name: "chatRoom", key: [roomId] }),获取或创建该房间对应的 Actor。这样可以实现:
- 隔离:每个房间的历史记录和连接都完全限定在其键范围内。切换房间输入会为 hook 重新设置键,并连接到拥有独立历史记录的另一个 Actor。
- 单一串行写入者:同一房间的所有
sendMessage调用都由一个 Actor 处理,因此无需锁即可保证消息顺序一致。SQLiteAUTOINCREMENTid 是规范顺序,因此getHistory按id而不是时间戳排序。 - 自然扩展:各个房间可在集群中独立分布。繁忙的房间不会拖慢其他房间。
消息历史记录存储
此示例将历史记录存储在 Actor 的 SQLite 数据库中,而不是 JSON 状态中。请根据历史记录的大小和查询需求进行选择:
| 方案 | 适用场景 | 实现指导 |
|---|---|---|
| SQLite(此示例采用的方案) | 需要排序、上限、分页或搜索的大量或长期历史记录 | 在 db({ onMigrate }) 中创建 messages 表,使用参数化查询插入数据(c.db.execute("INSERT ... VALUES (?, ?, ?)", ...)),并通过 ORDER BY id ASC 读取。历史记录在 Actor 休眠后仍会保留,且可扩展到超出内存存储能力的规模。 |
| JSON 状态 | 少量近期历史记录,例如最近 50 到 100 条消息 | 将消息添加到 Actor 状态中的 messages 数组,并在每次发送时截断至上限。这是最简单的方案,但全部历史记录都存放在内存中,且没有查询层,因此仅适用于近期历史记录数量有限的场景。 |
广播消息投递
新消息通过类型化的 事件 发送给已连接的客户端:
- Actor 声明
events: { newMessage: event() },其中Message为{ sender, text, timestamp }。 sendMessage操作 使用服务端的Date.now()时间戳构建消息,将其插入messages表,然后调用c.broadcast("newMessage", message)并将消息返回给调用方。- 每个客户端通过
useEvent("newMessage", ...)订阅,并将新消息追加到本地列表。发送者也通过与其他人相同的广播路径呈现自己的消息,因此所有客户端都使用同一条代码路径。 - 历史记录加载受连接状态控制:连接就绪后,客户端调用一次
getHistory()来呈现历史记录,之后的所有内容都依赖事件接收。
对于面向整个房间的消息,请使用 c.broadcast(...)。对于私密或面向特定接收者的有效载荷(例如房间内的私信),请改为发送到单个连接;这是对此示例的推荐扩展。
正在输入指示器和在线状态(扩展)
此示例未实现任何类型的正在输入指示器、在线状态或加入/离开处理。代码中没有 createConnState、onConnect 或 onDisconnect。如有需要,请将其作为临时的 连接 行为添加:
- 保持临时性:将用户名和正在输入标志存储在每个连接的状态中,绝不写入 SQLite 或持久化的 Actor 状态。在线状态根据活动连接得出,连接断开时应随之消失。
- 仅在状态变化时广播:用户开始或停止输入时发送输入事件,并在
onConnect/onDisconnect中发送在线状态事件,而不是轮询或定时触发。 - 在客户端设置过期时间:在客户端短暂超时后清除正在输入指示器,避免连接意外断开后留下卡住的“正在输入”提示。
每用户收件箱(扩展)
如需离线消息投递、私信、未读计数或通知扇出,请为每个用户添加一个 userInbox[userId] Actor。此扩展不属于示例内容:
- 房间 Actor 通过 Actor 间调用 将每条消息转发到每位成员的收件箱 Actor,因此即使用户未连接到房间,也能累积消息。
- 收件箱 Actor 管理每位用户的未读状态,并在用户上线时提供这些状态,不受用户所在房间的影响。
- 私信可以视为一种特殊的房间:可以使用以用户 ID 排序后的用户 ID 对作为键的
chatRoom,也可以直接从一个收件箱投递到另一个收件箱(如果不需要共享历史记录语义)。
Actors
- 键:
chatRoom[roomId] - 职责:管理一个聊天室。将聊天室的消息历史记录保存在其 SQLite 数据库中,并向每个已连接的客户端广播每条新消息。
- 操作
sendMessagegetHistory
- 队列
- 无
- 事件
newMessage
- 状态
- SQLite
messages表:id(自增主键)、sender、text、timestamp
生命周期
sequenceDiagram
participant A as Client A
participant B as Client B
participant R as chatRoom
A->>R: connect with key [roomId]
Note over R: every start runs onMigrate (CREATE TABLE IF NOT EXISTS messages)
A->>R: getHistory()
R-->>A: Message[] ordered by id
B->>R: connect with key [roomId]
B->>R: getHistory()
R-->>B: Message[] ordered by id
A->>R: sendMessage(sender, text)
Note over R: INSERT row with server timestamp
R-->>A: newMessage (broadcast)
R-->>B: newMessage (broadcast)
A->>R: disconnect
Note over R: history stays in SQLite for the next connection
安全检查清单
此示例有意保持精简,并跳过了以下所有内容。投入生产前,请添加这些内容:
- 加入前进行身份验证:任何客户端只要知道房间名称,就可以加入任意房间;而且每次调用中的
sender都是任意客户端输入。请在连接身份验证期间验证令牌,将身份绑定到连接状态,并在提供历史记录前检查房间成员资格。切勿信任作为操作参数传入的发送者名称。 - 限制消息长度:此示例接受空消息,且没有长度限制。请在服务端进行修剪、拒绝空文本,并限制最大长度。
- 按连接进行速率限制:对每个连接的
sendMessage进行速率限制,以阻止垃圾消息和广播放大。 - 服务端时间戳和 ID:此示例已正确处理。
timestamp在操作内部通过Date.now()获取,id则来自 SQLiteAUTOINCREMENT。请保持这种方式;切勿接受客户端提供的时间戳或 ID。 - 限制历史记录数量:
getHistory会返回所有行,没有任何限制。请添加LIMIT和分页,并清理或归档旧行,以免长期运行的房间无限增长。 - 参数化查询:此示例已使用
?占位符进行插入。请确保所有用户提供的文本都不会进入 SQL 字符串插值。
参考索引
Actors
- 访问控制
- 操作
- Actor 键
- Actor 运行时 Socket
- Actor 状态
- 身份验证
- Cloudflare Workers 快速入门
- Actor 之间的通信
- 连接
- 自定义检查器标签页
- 调试
- 设计模式
- 销毁 Actor
- Effect.ts 快速入门(Beta)
- 错误
- Fetch 和 WebSocket 处理器
- 辅助类型
- 图标与名称
- 内存状态
- 输入参数
- 生命周期
- 限制
- 底层 HTTP 请求处理器
- 底层 KV 存储
- 底层 WebSocket 处理器
- 元数据
- Next.js 快速入门
- Node.js 与 Bun 快速入门
- 队列与运行循环
- React 快速入门
- 实时功能
- Rust 快速入门(Beta)
- 扩展与并发
- 计划任务与 Cron
- 共享和加入状态
- SQLite
- SQLite + Drizzle
- Supabase Functions 快速入门
- 测试
- 故障排除
- 类型
- 原生 HTTP API
- 版本与升级
- 工作流
Cli
客户端
实用指南
部署
- 容器运行器
- 部署到 Amazon Web Services Lambda
- 部署到 AWS ECS
- 部署到 Cloudflare Workers
- 部署到 Freestyle
- 部署到 Google Cloud Run
- 部署到 Hetzner
- 部署到 Kubernetes
- 部署到 Railway
- 部署到 Rivet Compute
- 部署到 Supabase Functions
- 部署到 Vercel
- 部署到虚拟机和裸金属服务器