我一直以來都習慣使用即時工具,很少去想它們背後的工作原理。例如 Google Docs、Figma、Replit 多人協作、VSCode Live Share……你輸入什麼,對方就能立刻看到,一切正常,不會出現任何衝突,也不會覆蓋別人的內容。它……就是這麼好用。
不知為何,我一直很好奇這背後的運作機制。一個人的改變怎麼會神祕地出現在所有地方?衝突是如何處理的?如果兩個人同時改動了同樣的東西會發生什麼事?
我不想只得到「高屋建瓴」的答案,我想親身感受這個系統。就像建立Veridian幫助我理解 Git 一樣,我也想透過自己建立一個即時同步系統來理解它們。
所以我開發了Conflux ,一個用 Rust 寫的小型即時協作引擎。這並不是因為世界需要另一個後端,而是因為我需要了解這些即時系統究竟是如何在多個使用者之間同步狀態而不崩潰的。
事實證明,這並非什麼神秘的技術,而只是一系列簡單理念的有序堆砌。
每當我看到有人同時編輯同一個東西時,我的腦子裡就會冒出這樣的想法:「好吧,肯定是某個神秘的庫在做一些複雜的操作。」但後來我了解了CRDT ,一切就豁然開朗了。
無衝突複製資料類型 (CRDT)本質上是一種永遠不會發生衝突的資料結構。每次更新都是可合併的。任何人都可以自由編輯而無需鎖定。最終,所有資料都會收斂到相同的狀態。
意識到這一點後,我決定親眼看看:
好的,我們來分析一下。 「無衝突」這一點才是關鍵。
「正常」方式(問題所在):
想像一下你我正在編輯一個文字檔。
我們都下載了該檔案。文件內容是: "Hello" 。
我把副本改成: "Hello world" 。
同時,你將文字改為: "Hello there" 。
我上傳了我的版本。伺服器現在顯示"Hello world" 。
您上傳了您的版本。伺服器現在顯示"Hello there" 。
我的修改沒了,永遠沒了。你覆蓋了我的修改。這是個衝突。這就是像 Git 這樣的版本控制系統花費大量時間試圖透過「合併衝突」來管理的問題。
“CRDT”方法(解決方案):
CRDT 的工作方式並非如此。它們不會來回發送整個文件,而是發送指令。
我們兩個的狀態都是: "Hello" 。
我做了一些更改。我的本地 CRDT 沒有顯示“新檔案是‘Hello world’”。它產生了一條指令:( (At position 5, add: " world") 。
同時,你進行了一項更改。你的 CRDT 產生了一條指令:( (At position 5, add: " there") 。
我向伺服器發送指令。
您將指令傳送到伺服器。
伺服器以及最終所有客戶端都會收到這兩個指令。 CRDT 的「魅力」在於它擁有一套數學規則來合併這些指令,從而使所有人最終都達到完全相同的狀態。
最終文本可能是"Hello world there"或"Hello there world" 。重要的是沒有資料遺失,而且我們最終都看到了相同的內容,沒有出現「合併衝突」錯誤。
就是這樣。 CRDT 只是一種資料結構,它擁有一個非常優秀的「合併」演算法,因此永遠不會發生衝突。
現在我們知道了什麼是 CRDT,整個系統就更容易理解了。
去掉多餘的細節,Conflux 其實只做三件事:
它維護著各種房間,例如“文件”或“會話”。
每個房間都包含一個CRDT 文件-該文件儲存共用狀態。
客戶端發送指令(更新),伺服器合併這些指令,然後將它們廣播給其他所有人。
這是同樣的循環,但現在它的意義就顯而易見了:
你輸入內容 → 你的本地 CRDT 產生一條指令→ 你將該指令(「更新」)發送到伺服器 → 伺服器將其合併到自己的 CRDT 中 → 伺服器將該指令廣播給所有其他客戶端 → 它們的本地 CRDT 將其合併 → 每個人的 UI 都更新。
就是這樣。這就是整個循環。沒有複雜的演算法,沒有奇怪的變換,也沒有分支時間軸。
只需:更新→合併→廣播。
由於 CRDT 的設計初衷就是為了實現無縫合併,因此不會發生任何衝突。
為了方便理解,我畫了出來:

圖中每個方框只負責一項任務。沒有哪個方框會同時執行五項任務。正是這種簡潔性讓整個系統顯得平易近人。
Conflux 伺服器由四個主要部分組成,每個部分都只負責一項工作。
這是用來追蹤所有活躍房間的部分。
如果房間不存在,它會建立它。如果房間長時間閒置,它會清理它。
沒什麼特別的-就是生命週期管理。
房間本身基本上就是一個小型伺服器。
它執行一個循環,監聽類似這樣的指令:
ApplyUpdate (這是我們的 CRDT指令!)
SetAwareness
Chat
Join
Leave
並且它會將這些更新套用到它自己的 YDoc 中。
這種「演員」設計意味著每個房間都是獨立的,從而避免了通常的共享狀態混亂。
這是入口。
它:
使用 JWT 對使用者進行身份驗證。
從URL中提取房間ID。
將連線升級為 WebSocket。
將收到的訊息轉發到正確的房間。
將房間發出的訊息轉發回客戶端。
它接受兩種類型的訊息:
純文字:這將變成聊天訊息。
JSON:這將變成結構化的 CRDT 或感知更新。
這樣一來,除錯就變得很容易了,因為你可以直接在終端機上輸入聊天資訊。
每次登入都會在令牌中建立一個新的會話 ID ( sid )。
這樣就解決了「所有人都使用同一個令牌登入」的問題。
當客戶端連接時,伺服器就知道:
哪個用戶
哪個會議
哪個房間
它雖然簡單,但卻提供了一個清晰的身份模型。
假設兩個人正在編輯一份共享文件。
Their local CRDT applies the change instantly and generates an *instruction*.
This is just a small binary chunk.
This keeps the server's copy of the document authoritative.
All *other* clients in that room receive the same update.
Their local CRDT merges the instruction into whatever they already have.
Even if many people change the same thing at the same time, the CRDT handles the merges smoothly.
聽起來很複雜,但看到實際效果就覺得很簡單了。
即時系統不僅僅關乎文件內容。
用戶需要了解:
還有誰在線上?
他們的遊標在哪裡
誰在打字
誰加入了或離開了
所以我加入了「感知事件」。這些只是簡短的 JSON 訊息,可以立即傳播,顯示誰是誰以及他們在哪裡。
聊天功能很簡單——如果客戶端發送的是普通文字而不是 JSON,伺服器會將其視為聊天訊息並進行廣播。
這是一個很小的功能,但它讓系統感覺更加生動。
我還建立了一個小型儀錶板介面:
GET /dashboard
回傳結果:
房號
已連接客戶端數量
文件更新次數
宣傳活動數
雖然沒什麼特別之處,但看到系統運作起來卻非常有用。
就像 Veridian 讓我真正理解了 Git 一樣,Conflux 讓我真正了解即時協作。
以下是幾個突出的要點:
即時同步主要涉及訊息排序和廣播。
CRDT 可以消除約 90%的「衝突」問題。
演員式房間使得並發性出奇地好。
WebSocket 比我想像的要容易使用得多。
儘早建立完善的辨識系統可以避免日後出現許多麻煩。
大多數「複雜系統」只不過是一系列簡單步驟的集合。
在建造 Conflux 之前,這些系統感覺就像黑盒子一樣。
建成之後,我覺得它們是我可以理解的東西,甚至可以改進它們。
Conflux 並不完美,它可能缺少一些功能,也絕對還達不到生產就緒的程度。但它完全實現了我當初建構它的目的,並且幫助我理解了即時協作的底層工作原理。
如果你對這些東西有興趣,不妨自己嘗試建造一個小型版本。
你不需要重新建立一個 Google Docs。即使是基於 CRDT 的小型共享計數器也能讓你學到很多東西。
您可以在這裡查看 Conflux:
https://github.com/Kayleexx/conflux
我會繼續改進它,以後可能會寫更多關於更深層的內容。
原文出處:https://dev.to/kayleecodez/building-conflux-my-own-real-time-collaboration-engine-in-rust-41lm