Skip to content

Repository files navigation

OT 协同编辑

基于 Operational Transformation 的多人实时文档协同编辑系统:多个客户端可同时编辑同一篇文档,编辑操作在服务端定序、变换后广播,各端最终收敛到一致文本,并实时同步彼此的光标。

特性

  • 经典 OT 内核:纯文本 insert/delete 的 Apply / Transform(TP1 收敛)/ Compose / TransformPosition,Go 与 JS 双实现、语义对齐(UTF-16 计数)。
  • 客户端三态状态机:ShareDB 式 pending/buffer 状态机(Synchronized / AwaitingConfirm / AwaitingWithBuffer),保证本地编辑与远端 ack 的正确合流。
  • 多实例水平扩展:后端多实例用 etcd 协调,文档按需惰性认领属主(带 fence token 防脑裂)。单文档在其 owner 实例内以单 goroutine + channel 串行处理,天然满足 OT 全局有序。实例故障时属主锁经 etcd 租约释放,其他实例自动接管。
  • 持久化与恢复:op 日志(append-only,真相源)+ 定期快照落 MySQL,实例重启或文档重新打开时自动恢复内存状态。
  • 远端光标同步:实时广播协作者光标位置,并随文档编辑自动漂移。
  • 用户与授权:注册/登录(bcrypt + Redis session),文档可按 single/subtree 粒度授权给其他用户协同编辑。

Demo

image

架构

后端为单 Go module otdemo,拆成三个可独立部署的进程,各自按 transport / service / infra 分层;跨服务共享收敛到极瘦的顶层 contracts/

                          浏览器(多标签页)
             web/ · ot.js / client.js / cursors.js
                    │                        │
              HTTP /api/*              WebSocket /ws
                    │                        │
                    ▼                        ▼
        ┌───────────────────────────────────────────────┐
        │        edge-gateway  :8081  无状态接入网关       │
        │  静态托管 /   ·   /api/* 反代   ·   /ws 终止+扇出 │
        └───────────────────────────────────────────────┘
              │ 反代            │ authN/authZ      │ Resolve 属主
              │                 │ Authenticate     │ + Session 流
              │                 │ CanAccess        │ (OT wire)
              ▼                 ▼                  ▼
   ┌────────────────────────────────┐   ┌──────────────────────────┐
   │  app-service                   │   │  collab-engine  :9082 gRPC│
   │  :8090 HTTP / :9090 gRPC       │   │  有状态协同引擎            │
   │  无状态业务                    │   │                           │
   │  ┌──────────────────────────┐  │   │  ┌─────────────────────┐  │
   │  │ HTTP: auth/docs/grants   │  │   │  │ CollabEngine gRPC   │  │
   │  │ AppAuthz gRPC:           │  │   │  │ Session 双向流       │  │
   │  │  CanAccess · Authenticate│  │   │  │ Resolve             │  │
   │  └──────────────────────────┘  │   │  └─────────┬───────────┘  │
   └───────┬─────────────┬──────────┘   │  ┌─────────▼───────────┐  │
           │             │              │  │ OT 内核·单文档串行定序│  │
           │             │              │  └─────────┬───────────┘  │
           │             │              └────────────┼──────────────┘
           ▼             ▼                    ▼       ▼
      ┌─────────┐   ┌─────────┐        ┌─────────┐  ┌─────────────┐
      │  MySQL  │   │  Redis  │        │  MySQL  │  │    etcd     │
      │  :3306  │   │  :6380  │        │  :3306  │  │   :2379     │
      │ users/  │   │ session │        │ op 日志 │  │ 属主协调    │
      │ docs/   │   │         │        │ + 快照  │  │ + fence     │
      │ grants  │   │         │        │         │  │             │
      └─────────┘   └─────────┘        └─────────┘  └─────────────┘

  ── 跨服务共享契约 contracts/(三个二进制编译期共用,非运行时节点)──
     contracts/rpc  : AppAuthz + CollabEngine 的 protobuf 生成物
     contracts/wire : OT 线上消息结构 ClientMsg/ServerMsg/OpWithRev/CursorInfo

三个服务:

  • edge-gateway/(:8081,浏览器入口)— 无状态接入网关:静态托管 + /api/* 反向代理 + WS 终止,向 collab-engine gRPC 扇出。
  • app-service/(:8090 HTTP / :9090 gRPC)— 无状态业务服务:auth/docs/grants CRUD + AppAuthz gRPC(唯一鉴权决策点:CanAccess + Authenticate)。
  • collab-engine/(:9082 gRPC)— 有状态协同引擎:OT 引擎 + etcd 属主协调 + CollabEngine gRPC(Session 双向流 + Resolve)。无对外 HTTP 口,只被 edge-gateway 内部调用。

顶层共享契约 contracts/(三个二进制都 import 的极瘦叶子,零业务逻辑):

  • contracts/rpc — protobuf 生成物:AppAuthz(CanAccess / Authenticate)与 CollabEngine(Session 流 + Resolve)。
  • contracts/wire — 客户端/服务端 OT 线上消息结构(ClientMsg / ServerMsg / OpWithRev / CursorInfo)。

各服务内部分层:

  • collab-engine/ot — OT 核心:Apply / Transform / Compose / TransformPosition + JSON 线上编解码。
  • collab-engine/route — 属主发现 + 惰性认领的 Directory / Router
  • collab-engine/service — 单文档权威状态与串行处理循环(document)、本实例 docId → *Document 管理(hub)、Store 端口。
  • collab-engine/infra — 基础设施实现:store_mysql(op 日志 + 快照)、store_mem、etcd 协调器(coord,fence token)。
  • collab-engine/transportCollabEngine gRPC server + remote_client
  • app-service/service — 领域层:auth/docs/grants + Repo 端口 + web 层共享上下文工具(webctx,session cookie 名等)。
  • app-service/infra — 基础设施实现:docs/user/grants MySQL + session Redis。
  • app-service/transport — HTTP handlers + RequireAuth 中间件 + AppAuthz gRPC server。
  • edge-gateway/transport — WS 桥接与 gRPC 扇出:wsproxy / pool / dial / engine_stream / authz_client / auth_client
  • web/ — 原生 HTML/CSS/JS 前端:ot.js(客户端 OT)、client.js(三态状态机)、cursors.js(光标覆盖层)。

数据流:浏览器 WS → edge-gateway 终止并鉴权 → gRPC 扇出到 collab-engine 属主实例 → OT 串行定序 → 广播回各 gateway → 各客户端。属主解析走 engine 的 Resolve RPC,客户端不直接访问路由。

运行

依赖 etcd(协调)、MySQL(持久化)、Redis(session),均由 docker compose 提供。一条命令起全栈(依赖容器 + 三进程,前台聚合日志,Ctrl-C 全清):

scripts/dev.sh                # 起依赖容器 + 三进程
scripts/dev.sh --no-docker    # 依赖已在跑时跳过 docker compose
scripts/dev.sh --build        # 先编译成二进制再跑(避免 go run fork 子进程残留端口)

浏览器打开 http://localhost:8081/?doc=demo,多开几个标签页即可协同。

也可手动分别起(需先 docker compose up -d 起依赖):

export OT_APP_GRPC=localhost:9090 OT_ENGINE_GRPC=localhost:9082
go run ./app-service   --addr localhost:8090 &
go run ./collab-engine --addr localhost:9082 &
go run ./edge-gateway  --addr localhost:8081 &

依赖服务(docker compose)

docker compose up -d   # 起 mysql:8.4 + redis:7 + etcd:v3.5
  • MySQL(:3306,库/账号/密码均为 otdemo)— op 日志与快照持久化。
  • Redis(宿主 :6380 → 容器 6379,避让宿主 6379 上其他项目)— session 存储。
  • etcd(:2379)— 属主协调与 fence token。

配置(环境变量)

变量 默认值 说明
OT_MYSQL_DSN 指向本地 compose 的 otdemo 库 MySQL 连接串
OT_REDIS_ADDR 127.0.0.1:6380 app-service 连接的 Redis 地址
OT_ETCD_ENDPOINTS 本地 127.0.0.1:2379 collab-engine 的 etcd 端点
OT_APP_GRPC localhost:9090 app-service 的 gRPC 监听/被调地址
OT_ENGINE_GRPC localhost:9082 collab-engine 的 gRPC 地址
OT_APP_URL http://localhost:8090 gateway 反代 app-service 的 HTTP 地址

持久化(MySQL)

首次启动自动建表:doc_ops(append-only op 日志,真相源)+ doc_snapshots(每文档最新快照,每 100 条 op 打一次)。恢复时读最新快照并 replay 其后的 op 重建内存状态。处理顺序为「Apply 验证 → 同步写 op → 提交内存 → ack」,故 ack 代表已持久化。文档在无客户端连接时从内存淘汰,再次打开时从 MySQL 恢复。持久化由 collab-engine 承载。

需要覆盖连接串时设 OT_MYSQL_DSN

export OT_MYSQL_DSN="user:pass@tcp(host:3306)/dbname?parseTime=true&multiStatements=true"

用户与登录

注册/登录后才能进入文档。账号存 MySQL(users 表),session 存 Redis(HttpOnly cookie,7 天 TTL),密码 bcrypt 存哈希。session 由 app-service 持有;edge-gateway 不直连 Redis/ws 的 authN(session cookie → uid)经 app-service 的 AppAuthz.Authenticate gRPC 完成。

接口:POST /api/registerPOST /api/loginPOST /api/logoutGET /api/me/ws 与文档接口均需登录(RequireAuth 中间件从 cookie 还原 uid)。前端首屏访问 /api/me,401 自动跳 /login.html

授权与共享

owner 可按用户名把文档授权给他人,scope 分 single(仅该文件)与 subtree(含全部子孙)。被授权者在「共享给我」区浏览并进入协同编辑。

接口:

  • GET /api/docs[?parent=<id>] — 懒加载按层拉取自有文档(省略 parent 拉根层)。
  • GET /api/docs/{id}/grantsPOST /api/docs/{id}/grantsDELETE /api/docs/{id}/grants/{uid} — owner 管理协作者名单。
  • GET /api/shared[?parent=<id>] — 共享区:省略 parent 列授权根节点(带 owner 用户名),带 parent 展开该节点下当前用户可访问的子节点。

/ws join 受 CanAccess 保护:owner 或 grant 命中(single 仅授权点、subtree 含子孙)才放行,越权 join 返回 forbidden

手动端到端验证

打开两个标签页 http://localhost:8081/?doc=demo,验证:

  1. 一个标签页打字,另一个实时出现相同文本。
  2. 两个标签页在不同位置同时快速打字,最终两边文本一致(收敛)。
  3. 一个标签页移动光标,另一个看到彩色远端光标 + 标签,且对方打字时光标位置跟随漂移。
  4. 关闭一个标签页,另一个的对应远端光标消失。
  5. 打开 ?doc=alpha?doc=beta,起多个 collab-engine 实例时,二者可能由不同实例认领属主(属主解析走 engine 的 Resolve RPC)。

安全说明

已有基于 Redis session + HttpOnly cookie 的登录鉴权(密码 bcrypt 存哈希)。当前传输未加密,WebSocket cookie 未加 Secure,且 CheckOrigin 恒为 true(允许任意来源)。对外部署前必须补齐传输加密(wss/https + cookie Secure)与 Origin 校验。

About

一个多人实时文档协同编辑工具,基于 OT(Operational Transformation)算法,实现多用户实时协同编辑、冲突解决

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages