兩種不用伺服器的連法
同一年做的兩個東西都拒絕把資料放到雲上,也都用 WebRTC。可是把連線架構攤開並排,形狀幾乎沒有一處相同。
AbstractTwo projects from the same year, both refusing to put data in the cloud, both on WebRTC. Laid side by side, almost nothing about their architectures matches.
黑白江湖跟柴米帳是同一年做的,一個是連線對戰的遊戲,一個是兩個人的記帳本。它們共用一條原則,能不用伺服器就不用,也共用同一種傳輸,WebRTC 的資料通道。
前陣子我把兩邊的連線架構攤開來擺在一起看,想確認一件事:這兩個「不用伺服器」,是不是同一件事。
結論是不是。它們共用結論,不共用理由,也不共用做法。
黑白江湖是怎麼連的
我自己開了一台訊令的 Worker,上面掛三個 Durable Object。房間一個、公開榜一個、客棧廣播一個。
它只做撮合,不碰對局內容。這句話如果只是寫在文件裡,那就只是一句話,所以我在 eslint 設定裡把那個套件的 fetch、window、document 全部禁掉,也禁掉所有非 Cloudflare 的 import。要讓它偷偷連去哪裡,得先改設定檔,那件事會出現在 diff 裡。
連上之後就跟伺服器沒關係了。三到九個人是星型,房主當權威,收到落子、驗證、套用,再轉發給除了來源以外的每一條線。
最麻煩的是房主會掉線。我的解法是繼位,凍結窗先撐住,剩下的人裡選席次最小的接手。選誰這件事完全從日誌推導,每個人各自算,答案一定一樣,所以整個過程不用交換任何一則協商訊息。
沒連上的話就是失敗。沒有中繼的退路,只有一句文案叫你們連同一個 WiFi。
柴米帳是怎麼連的
這邊我一台伺服器都沒開。
訊令是去借 Nostr 的公共 relay,四十七個裡面依 appId 洗牌取五個。那些是別人的機器,所以 SDP 是加密過的,relay 看得到的只有 IP、連線時間、房間主題的雜湊,還有大概兩 KB 的密文。帳本本身從來不經過那裡。
配對是面對面掃一次 QR。那個 QR 刻意不是網址,因為 iOS 的系統相機看到網址會給一個「用 Safari 開啟」,按下去會把人丟進另一個儲存分區、空的帳本,然後把整本帳靜默倒進一個用完就丟的殼。
連上之後兩邊是對等的,沒有誰說了算。合併靠 HLC 加上逐列後寫贏,兩支手機各自算,結果會收斂到同一份。
攤開來比
vite.config.ts 註解裡的原話,那時候順手把兩個決定並排寫了下來,現在看就是這整篇的縮影。三件事
第一件,兩邊都用同一招解決「你是誰」。
沒有帳號、沒有登入。身分就是持有一個秘密位址。黑白江湖繼位用的 reunionId 是六十四位元,只走加密的資料通道傳出去;柴米帳的六碼房號是面對面掃出來的。兩邊的文件都明講「知道房號的人就能連」不算漏洞,那個房號就是憑證本身。
這兩個專案我是分開做的,最後各自走到同一個答案。
第二件,失敗都是靜默的。
沒有伺服器就沒有人回你一個五百。錯誤不會有堆疊、不會進日誌、不會有告警,只會顯示成「找不到對方」,或者更糟,什麼都不顯示。
所以兩邊的力氣都花在同一件事上。柴米帳那邊有列數計數器,收到的不夠就不 ack;寫入失敗就不存 checkpoint;還有一道空帳本的閘門。黑白江湖那邊有每手一次的 hash 心跳、pagehide 的幽靈認輸修正,還有把 bye 延後一個 macrotask 讓它逃得出去。
這是無伺服器真正的稅,不是省下來的那點機器錢。
第三件,分歧其實不在技術,在你願意信任誰。
遊戲那邊我自己開了一台,然後用編譯期的規則把自己綁住。想讓那個 Worker 連去別的地方,得先改 eslint 設定,而那件事會留在 diff 裡。
帳本那邊我不開,去借別人的公共設施,然後假設那些人不可信,所以先加密。代價是那條借來的路會因為別人的版本更新而斷掉,症狀只顯示成一句「找不到對方」。
一個是自己蓋、自己立規矩,一個是借用、可是假設對方不懷好意。這兩種都叫無伺服器,可是它們對「誰不可信」的答案不一樣。