ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Huly 平台实时白板服务 Hulypulse 实战指南:Key 模型、REST/WebSocket API 与源码级原理剖析

Huly 平台实时白板服务 Hulypulse 实战指南:Key 模型、REST/WebSocket API 与源码级原理剖析 Huly 平台实时白板服务 Hulypulse 实战指南Key 模型、REST/WebSocket API 与源码级原理剖析【免费下载链接】platformHuly — All-in-One Project Management Platform (alternative to Linear, Jira, Slack, Notion, Motion)项目地址: https://gitcode.com/GitHub_Trending/platform80/platformHulypulse 是 Huly 全栈协作平台foundations/hulypulse中一个用 Rust 实现的轻量级共享白板服务多个客户端连接到同一个命名空间后彼此写入的键值数据可被实时同步与订阅服务同时暴露 REST 与 WebSocket 两种 API。读完本文你将掌握 Hulypulse 的 Key 命名模型与私有段语义、完整的 HTTP/WS 协议交互含 TTL 与条件写等并发控制、内存/Redis 双后端切换、JWT 认证与构建运行方式并能依据源码理解其事件广播与心跳保活机制。Hulypulse 是什么为在线协作状态而生的共享白板Hulypulse 的核心抽象是白板whiteboard连接到同一白板的客户端能够看到其他客户端写入该白板的数据。服务本身并不关心数据的业务含义它只负责按 Key 存取 JSON、按前缀订阅、实时广播变更三件事。官方列举的典型使用场景包括文档中的用户在线状态presence用户的正在输入typing事件编辑器或绘图板中的光标位置cursor position服务端推送的进程状态process status从源码结构看Hulypulse 由main.rsactix-web 服务入口、handlers_http.rs、handlers_ws.rs协议层、db.rs存储抽象层、redis.rs/memory.rs两个后端实现、hub_service.rs连接会话与订阅广播中枢组成并在 Cargo.toml 中声明版本0.4.2edition 2024。Key 模型命名空间、层级前缀与私有段Key 的格式与约束Key是由一个或多个段segment通过/拼接而成的字符串例如foo/bar/baz。规则如下Key不能以/结尾单 Key 语义段内不得包含特殊字符*、?、[、]、\、\x00..\x1F控制字符、\x7F、、段不能为空段可以是私有段以$前缀标记例如/a/b/$c/$d。这些限制在 db.rs 中有直接实现deprecated_symbol()逐一比对上述字符集合deprecated_symbol_error()在命中时返回412 Deprecated symbol in key读写路径redis_save、redis_read、memory_save等都会在操作前调用该校验。Query单 Key 与前缀查询Query用于查询/订阅同样不能包含上述特殊字符但支持前缀查询前缀以段分隔符/结尾。GET/SUBSCRIBE ... a/b—— 精确匹配单个 Keya/bGET/SUBSCRIBE ... a/b/c/—— 匹配所有以a/b/c/开头的 Key。前缀多匹配的私有段规则选取所有以该前缀起始的 Key跳过前缀右侧包含私有段$的 Key。前缀匹配示例官方文档原例假设存在四个 Key/a/b/$c/$d/a/b/c/a/b/$c/a/b/$c/$d/e查询前缀与结果的对应关系前缀结果/[2]/a/b/[2]/a/b/$c/[3]/a/b/$c/$d/[4]/a/b/$c/$d[1]可以看到私有段$c、$d本身可以被精确查询或作为前缀继续匹配但当它们作为中间段出现时公开前缀如/a/b/不会泄漏其下的私有内容。源码中对应实现有两处Redis 后端的 redis.rsredis_list用SCAN MATCH {prefix}*遍历后通过k.strip_prefix(key).is_some_and(|s| s.contains($))过滤掉前缀右侧含$的 Key订阅匹配的 hub_service.rssubscription_matches()对以/结尾的订阅校验key.starts_with(sub_key)且剩余部分rest不含$。Data任意 JSON 文档Data是任意 JSON 文档大小被限制在一个合理的范围。这个上限在存储层有两处落地redis_save/memory_save都会读取CONFIG.max_size当值超过max_size时返回400提示 Value in memory mode must be less than {max_size} bytes内存后端对值有 UTF-8 校验非 UTF-8 数据返回400。max_size在 config/default.toml 中以注释形式存在# max_size 100即默认不限制0表示关闭校验按需开启即可见后文配置章节。HTTP APIHulypulse 的所有 HTTP 路由挂在 actix-web 的/api/{workspace}/{key...}之下详见 main.rs其中{key:./}以/结尾路由到 list 处理器{key:.}路由到 get/put/delete 处理器。工作区workspace在带认证编译时还会经过check_workspace中间件校验。GET /status—— 服务器状态与 WebSocket 连接数GET /status应答示例{status:OK,websockets:2}从 hub_service.rs 的info_json()看实际返回字段比文档示例更丰富包括status、backendredis/memory、websockets、subscriptions、heartbeats、serverping、loops、loglevel、memory_info、version。PUT /{workspace}/{key}—— 保存 Key输入BodydataJSONContent-Type: application/jsonContent-Length可选TTL 头二选一HULY-TTLN 秒后自动删除HULY-EXPIRE-AT在指定 UnixTime 自动删除默认max_ttl 3600config/default.toml条件头Conditional HeadersIf-Match: *—— 仅当 Key 存在时更新If-Match: md5—— 仅当当前值的 MD5 匹配时更新If-None-Match: *—— 仅当 Key 不存在时插入输出README 文档约定201以If-None-Match: *插入成功204普通插入或更新成功412条件不满足400请求头非法BodyDONE实现层面的实际行为当前仓库的 handlers_http.rs 中put处理器把HULY-TTL/HULY-EXPIRE-AT解析为Ttl::Sec/Ttl::At同时指定两者返回400 Multiple ttl specified把If-Match/If-None-Match组合映射为四种SaveModeIf-MatchIf-None-MatchSaveMode语义空空Upsert覆盖写入默认*空Update仅更新已存在 Keymd5空Equal(md5)仅当 MD5 匹配时更新空*Insert仅插入不存在的 Key保存成功后处理器实际返回的是200与正文DONE而非文档中的 201/204这一行为与 tests/rest_api.rs 中assert!(r.code 200); assert!(r.text DONE)的断言一致——使用时请以实测行为为准文档中的 201/204 属于协议设计约定。在 Redis 后端SaveMode::Insert/Update会翻译为SET key value EX sec NX/XXSaveMode::Equal则通过WATCHGETMULTI/EXEC实现带 MD5 校验的乐观锁CAS并在冲突时循环重试上限MAX_LOOP_COUNT 1000见 redis.rs。DELETE /{workspace}/{key}—— 删除 Key成功204 No Content无正文Key 不存在404 Not Found可选的If-Match条件If-Match: md5仅当 MD5 匹配时删除If-Match: *在 Key 不存在时返回错误Redis 后端实现为SaveMode::Update见 handlers_ws.rs 与 redis.rs。GET /{workspace}/{key}—— 读取单个 Key状态200Content-type: application/json响应头Etag: md5Bodyworkspace输入回显、key输入回显、data输入回显、expiresAt/TTL可选、etag md5实现层面的实际响应存储层统一返回DbArray { key, data, ttl, etag }见 db.rs其中ttl为剩余秒数、etag为 data 的 MD5handlers_http.rs 的get处理器将etag放入ETag响应头并以 JSON 序列化DbArray实际字段为key/data/ttl/etag。Key 不存在时返回404与正文empty。GET /{workspace}/{key}/—— 读取 Key 数组前缀列表状态200Content-type: application/jsonBody数组[{key,data,ttl,etag}, ...]注意 URL 必须以/结尾否则会被路由到单 Key 读取若前缀未以/结尾存储层返回412 Key must end with slash。WebSocket APIWebSocket 入口为/ws与/ws/{client_name}后者配合lopt特性启用命名会话握手后客户端发送 JSON 命令帧服务端以 JSON 应答。每条命令可携带可选的correlation id用于配对请求与响应。此外连接还支持应用层ping/pong文本帧做心跳见 handlers_ws.rs。Client → Server 命令PUT{ type: put, correlation: abc123, key: workspace/foo/bar, // 共享 Key data: hello, TTL: 60, // 可选N 秒后自动删除 expiresAt: 1700000000, // 可选UnixTime 自动删除 ifMatch: *, // 可选仅当存在时更新或传 md5 ifNoneMatch: * // 可选仅当不存在时插入 }Key 写法workspace/foo/bar共享 Key或workspace/foo/bar/$/secret私有 KeyTTL 默认max_ttl 3600应答{action:put,correlation:abc123,result:OK}GET{ type: get, correlation: abc123, key: workspace/foo/bar }应答{action:get,result:{data:hello,etag:5d41402abc4b2a76b9719d911017c592,ttl:3599,key:00000000-0000-0000-0000-000000000001/foo/bar}}LIST{ type: list, correlation: abc123, key: workspace/foo/bar/ }Key 写法workspace/foo/bar/公共空间前缀或workspace/foo/bar/$/secret/私有空间前缀应答{action:list,result:[{data:hello 1,etag:df0649bc4f1be901c85b6183091c1d83,ttl:3570,key:00000000-0000-0000-0000-000000000001/foo/bar1},{data:hello 2,etag:bb21ec8394b75795622f61613a777a8b,ttl:3555,key:00000000-0000-0000-0000-000000000001/foo/bar2}]}DELETE{ type: delete, correlation: abc123, key: workspace/foo/bar }可选条件ifMatch: md5仅当 MD5 匹配时删除、ifMatch: *Key 不存在时返回错误应答{action:delete,result:OK}不存在时返回 error not foundSUBSCRIBE{ type: sub, correlation: abc123, key: workspace/foo/bar }Key 写法与 LIST 相同的私有段规则workspace/foo/bar—— 订阅单个共享 Keyworkspace/foo/bar/—— 订阅所有以该前缀起始的 Keyworkspace/foo/bar/$/my_secret—— 订阅单个私有 Keyworkspace/foo/bar/$/my_secret/—— 订阅所有以该私有前缀起始的 Key应答{action:sub,result:OK}UNSUBSCRIBE{ type: unsub, correlation: abc123, key: workspace/foo/bar }Key 写法workspace/foo/bar退订指定 Key或*退订全部应答{action:unsub,result:OK}SUBLIST我的订阅列表{ type: sublist, correlation: abc123 }应答{action:list,result:[00000000-0000-0000-0000-000000000001/foo/bar1,00000000-0000-0000-0000-000000000001/foo/bar2]}INFO{ type: info, correlation: abc123 }应答{db_mode:memory,memory_info:1231 keys, 80345 bytes,status:OK,websockets:164}WsCommand枚举与上述命令一一对应定义于 handlers_ws.rs其中correlation缺省时默认为1。命令处理统一走handle_command()先做 workspace/Rego 权限校验auth 特性下再按语义调用db.save/read/list/delete或hub_state.subscribe/unsubscribe最后回写应答帧。lopt特性还额外提供personal/answer两类点对点消息命令。Server → Client 订阅事件一旦某个 Key 发生变更所有订阅了该 Key或匹配其前缀的会话都会收到广播事件写入{message:Set,key:00000000-0000-0000-0000-000000000001/foo/bar,value:hello}过期{message:Expired,key:00000000-0000-0000-0000-000000000001/foo/bar}删除{message:Del,key:00000000-0000-0000-0000-000000000001/foo/bar}事件广播链路在 hub_service.rs 的broadcast_event()先根据事件 Key 计算出所有匹配订阅的接收方recipients_for_key复用subscription_matches的私有段过滤逻辑再逐会话发送 JSON 文本帧。Redis 后端的事件来源是 Redis 的keyspace 通知见 redis.rs服务启动后通过CONFIG SET notify-keyspace-events E$gx开启并PSUBSCRIBE四个模式__keyevent*__:set/del/unlink/expired收到RedisEvent后对 Set 事件回查GET取当前值再广播断线会以指数退避1s 起、上限 60s自动重连。内存后端则由每秒一次的 ticker 扫描过期 Key 并广播Expired见 memory.rs。配置详解config/default.tomlconfig/default.toml 是仓库内的默认配置文件bind_port 8099 bind_host 0.0.0.0 token_secret secret backend redis redis_urls redis://huly.local:6379 redis_password invalid redis_mode direct redis_service mymaster max_ttl 3600 heartbeat_timeout 90 ping_timeout 30 loglevel INFO # optional settings # max_size 100 # permit_file /home/user/hulipulse/permit.rego加载顺序config.rs内嵌默认 TOML → 可选的etc/config.toml→ 以HULY为前缀的环境变量后者逐层覆盖前者配置解析失败会打印错误并退出进程。环境变量README 中列出的环境变量如下环境变量说明默认值HULY_BIND_HOST服务绑定主机0.0.0.0HULY_BIND_PORT服务绑定端口8094README 文档值仓库内 config/default.toml 实际为8099Docker 示例则映射8095请以部署配置为准HULY_TOKEN_SECRET用于签发/校验 JWT 的密钥secretHULY_BACKEND存储后端redis或memoryredisHULY_REDIS_URLSRedis 连接串逗号分隔支持多个用于 Sentinelredis://huly.local:6379HULY_REDIS_PASSWORDRedis 密码invalidHULY_REDIS_MODERedis 模式direct或sentineldirectHULY_REDIS_SERVICERedis Sentinel 服务名mymasterHULY_MAX_TTL最大存储时长秒3600HULY_PAYLOAD_SIZE_LIMIT最大载荷大小TODO尚未实现2Mb计划值此外源码 config.rs 与 default.toml 还支持max_size单值最大字节数0 表示不限制、heartbeat_timeout心跳超时默认 90s、ping_timeout服务端 ping 超时默认 30s、loglevelTRACE/DEBUG/INFO/WARN/ERROR映射见 main.rs以及 auth 特性下的policy_fileRego 策略文件路径。两个后端的语义差异TTL 上限两者都强制TTL 0且TTL max_ttl但内存后端内部用u8tick 计数表示过期时刻因此单次 TTL 被进一步限制在 255 秒以内见 memory.rs 的compute_ttl_u8超限返回412 TTL exceeds MAX_TTL or 255 secRedis 后端无此限制。绝对值过期Ttl::At(timestamp)在两端都会先换算为相对秒数若时间戳已过期 now返回400。信息上报info中memory_info字段在内存后端格式为1231 keys, 80345 bytesHashMap 长度与数据字节总和见 memory.rs在 Redis 后端则解析INFO输出的db0: keys与used_memory:见 redis.rs。构建与运行Cargo 特性构建Hulypulse 通过 Cargo features 控制认证能力Cargo.toml默认特性包含auth使用 huly-authorization即hulyrsJWT 与 Rego 策略禁用认证cargo build --no-default-features显式启用认证cargo build --no-default-features --features authlopt为可选特性开启后额外支持命名会话与personal/answer点对点消息。Docker 运行预构建镜像位于hardcoreeng/service_hulypulse:{tag}本地运行docker run -p 8095:8095 -it --rm hardcoreeng/service_hulypulse:{tag}构建流程参见 Dockerfile基于rust:1.88多阶段交叉编译linux/amd64与linux/arm64产物拷贝进debian:12-slim运行镜像。从源码运行使用 Redis 后端HULY_REDIS_URLSredis://huly.local:6379 cargo run使用内存后端无需 RedisHULY_BACKENDmemory cargo run注意内存后端下HULY_MAX_TTL若大于 255 仍受单值 255 秒限制且数据不持久化、重启即失仅适合开发与演示。加入本地 Huly 开发环境若要以本地 Huly 开发环境的一员运行与其余服务共享网络、对接本地开发 Redisexport HULY_REDIS_URLSredis://huly.local:6379 docker run --rm -it --network dev_default -p 8095:8095 hardcoreeng/service_hulypulse:{tag}随后即可通过http://localhost:8095访问服务/status、/api、/ws。另可参考目录下的测试脚本如 scripts/TEST_HTTP_API.sh、scripts/TEST_WS_API.sh做端到端冒烟验证。认证与授权Hulypulse 使用Bearer JWT认证auth 特性默认开启。当前实现接受任何由HULY_TOKEN_SECRET环境变量默认secret签名的 Token。HTTP 与 WebSocket 均可通过Authorization: Bearer token头携带 Token也可在查询串传?tokentoken见 main.rs 的extract_claims中间件。带认证编译时HTTP 请求还会经过check_workspace中间件claims.workspace必须与 URL 中的 workspace 一致claims.is_system()系统级 Token 除外否则返回401 Unauthorized。WebSocket 命令层另有 workspace 校验check_workspace_core与Rego 策略校验test_rego_http/test_rego_claims依据动作Put/Get/List/Delete/Sub...与 Key 执行策略判定未通过则拒绝HTTP 返回403 ForbiddenWS 返回 Unauthorized: Rego policy。策略文件通过policy_file配置项指定仓库内附带了示例策略 policy.repo。内部架构从请求到事件广播的完整链路入口main.rs 启动时初始化HubState会话/订阅中枢、心跳检查协程与存储后端backend redis时还会拉起redis::receiver协程消费 keyspace 事件。actix-web 服务配置全开放 CORS挂载/status、/api/{workspace}/...、/ws、/ws/{client_name}。存储抽象db.rs 的Db枚举封装Redis(ConnectionManager)与Memory(HashMap Hub)两个后端对上层HTTP/WS 处理器暴露统一接口save/read/list/delete/info。Redis 后端SET ... EX sec [NX|XX]写入GET/TTL读取TTL 为-1/-2时视为错误SCAN MATCH列表WATCH/MULTI/EXEC实现 MD5 CASSentinel 模式通过SentinelClientBuilder连接RESP3、db 11direct 模式直连RESP3、db 0默认端口分别为6379sentinel/6380direct见 redis.rs。内存后端HashMapString, Entry{data, ttl_tick}加每秒 ticker 过期扫描与Expired广播所有变更save/delete同步向 Hub 广播Set/Del事件。会话中枢hub_service.rs 维护sessions、subs、heartbeatscheck_heartbeat每 2 秒巡检超heartbeat_timeout90s未活动的会话被强制关闭超ping_timeout30s未活动的会话先发ping探活。客户端参考实现client/off/client.ts 提供了HulypulseClient30 秒应用层ping、5 分钟无响应判定断线、1 秒自动重连、连接类错误broken pipe / connection reset 等触发重连可作为接入端协议模板。测试与验证仓库提供了多层测试用于验证协议正确性tests/rest_api.rs覆盖PUT/GET/DELETE全流程包括If-Match: *、If-Match: md5匹配/不匹配412 md5 mismatch、If-None-Match、HULY-TTL过期写入 7 秒与 1 秒后等待 1.05 秒再读应 404、/status后端字段断言等db.rs内存后端单元测试验证save → read → list → delete的 CRUD 闭环tests/ws.rsWebSocket 协议测试scripts 下还有TEST_HTTP_API.sh、TEST_WS_API.sh、typing-test.sh、pulse_lib.sh等可执行的集成测试脚本与TEST.html浏览器调试页。已知限制与路线图README 列出的待办事项按原文档顺序可选的值加密Optional value encryptionOpenTelemetry 支持数据库迁移的并发控制多个 Hulypulse 实例同时升级时TLS 支持Liveness/readiness 探针端点此外README 中标记为 TODO 的HULY_PAYLOAD_SIZE_LIMIT尚未在源码中实现目前载荷大小仅由max_size控制。许可证与贡献Hulypulse 以EPL-2.0Eclipse Public License 2.0开源见 LICENSE接受 issue 与 pull request 形式的社区贡献。若要在自己的服务中集成在线状态/光标/输入中/状态推送这类实时协作能力Hulypulse 这套Key 前缀 私有段 条件写 订阅广播的协议模型可直接复用其事件广播与心跳机制均可对照上述源码路径进一步深入研读。【免费下载链接】platformHuly — All-in-One Project Management Platform (alternative to Linear, Jira, Slack, Notion, Motion)项目地址: https://gitcode.com/GitHub_Trending/platform80/platform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表