在 COSCUP 2019 講完用 WebRTC 建立半分散式網路,想說來把投影片整理成一篇文章來紀錄一下這段時間拿 WebRTC 嘗試形成網路的一些實驗結果
WebRTC 是什麼?
簡單來說 WebRTC 可以讓瀏覽器直接跟瀏覽器連線,但是連線的建立很麻煩,開發者要自己想辦法讓兩個瀏覽器交換 handshake 資訊 (offer, answer, ice)
Multiplayer game using WebRTC
跟朋友開始一個遊戲(?)專案 -- Bazaar,就如同下面這個影片這樣,可以有多個玩家在畫面上走來走去跳來跳去,移動等資訊就是透過 WebRTC 傳輸的

Bazaar client 之間的連線方法
其實很無腦,首先用 websocket 跟伺服器 (wss) 連線,接著與所有玩家連線

- firefox 與 wss 連線
- firefox 與 wss 連線完成
- chromium 與 wss 連線
- chromium 與 wss 連線完成
- chromium 透過 wss 與 firefox 建立 WebRTC 連線
- chromium 與 firefox 的 WebRTC 連線建立完成
- safari 與 wss 連線
- safari 與 wss 連線完成
- safari 透過 wss 與 firefox 以及 chromium 建立 WebRTC 連線
- safari 與 firefox 以及 chromium 的 WebRTC 連線建立完成
這邊很快就會發現一件事,就是每個玩家的連線數是總玩家數 - 1,也就是說假設有 20 個人在場上,瀏覽器要建立 19 個連線...雖然沒有明定 WebRTC 的連線數限制...但是我自己後來做測試時大概 30 ~ 40 個連線時 chromium 會跟我說太多連線不允許建立更多連線
一個發想:如果 server 可以是一個 NPC...
不知道當時在想什麼,這個念頭讓我開始把 server 當成 client 看待,為了讓 browser 對待 server 就跟其他 browser 一樣,嘗試製作 WebRTC 跟 WebSocket 共用的界面
WebRTC 與 WebSocket 共用的 interface
對於 wsConn 以及 rtcConn 來說,提供的功能就是送訊息以及接收訊息,很容易做成共用的界面
雖然兩者建立連線的方式有很大的不同,但是 WebRTC 連線建立其實就是需要一個節點幫忙傳送 handshaking 資料,於是試著蓋一層 peerConns,上面提供 API connect(peer, viaPeer)
peer如果是 WebSocket URL,則不需要viaPeer,直接連線就好,而且就算給viaPeer也不會怎麼樣peer如果是一個瀏覽器 (每個瀏覽器初始化時會自動產生一個 id),必須給上viaPeer指定一個節點幫忙傳送 handshaking 資料建立連線
不過 bazaar 遊戲到目前為止
viaPeer永遠會是 wss
透過 browser 進行 WebRTC 連線建立會是什麼樣子?
WebRTC 連線一定會需要一個節點幫忙做連線,因此
- 第一個連線一定是 WebSocket
後來可以透過瀏覽器幫忙連線,因此
- 加入網路之後可以關閉 WebSocket 連線
舉個例子來跑一次看看:

- 假設一開始 chromium 已經跟 firefox 建立 WebRTC 連線,chromium 已經跟 wss 建立 WebSocket 連線,而且 firefox 沒有跟 wss 連線
- safari 與 wss 連線
- safari 與 wss 連線完成
- safari 透過 wss 與 chromium 建立 WebRTC 連線
- safari 與 chromium 的 WebRTC 連線建立完成
- safari 透過 chromium 與 firefox 建立 WebRTC 連線
- safari 與 firefox 的 WebRTC 連線建立完成
雖然第一個連線一定是 WebSocket,不過還是可以某種程度的降低對 WebSocket server 的依賴性,於是就開始嘗試把整個連線的東西抽出來做...
半分散式網路
這個抽出來的東西暫時叫他 unnamed-network(?),總結前面的實驗以及遇到的問題,抽出來之後做了這些事:
- 在這之前 WebSocket server 都是 Elixir 做的,這邊改用 nodejs 實做
- WebSocket server 是個 client, 網路中同時可以有複數個
- 群組功能
- 一個 client 可以同時加入多個 group
- 群組廣播
unnamed-network 架構
- wsConn 與 rtcConn 部份幾乎與上面一樣負責收發訊息
- connProvider 部份負責處理 browser 與 wss 接受連線時行為上的不同
- connManager 與上面差不多,提供
connect(peer, viaPeer)給上層使用 - client 部份負責網路的維持並提供群組功能
connManager 連線建立策略
因為 wss 也只是一個 client 了,wss 也很有可能會想要主動與他人連線,列舉一下整理成四種情況:
| 左下連右上 | wss | browser |
|---|---|---|
| wss | ws 直連 | 邀請連 ws (!) |
| browser | ws 直連 | WebRTC (!) |
這邊比較有趣的就是『當 wss 想要連到 browser』的狀況,這邊想到的方法是透過 viaPeer 邀請 browser 來連 wss;需要 viaPeer 的部份標上 (!),所以結論來說不是只有 WebRTC 會需要 viaPeer
網路的維持以及群組功能
這就是還很不完善的部份了,這邊就稍微講一下目前運作的行為
- 初始化時
- 每個 client 有一個 known list 紀錄已知的 wss
- 所有 client 都會加入
"/"群組
- 加入群組時
- 對
"/"請求route-group - max hops: 10
- 加入群組時發現找不到人就是建立新群組
- 對
沒關係這邊有簡易的案例:

- 假設一開始有一個
group1有三個人,其中 chrome 與 wss 連線 - safari 因為 known list 知道 wss 的存在,加入
"/"群組時會去找 wss 連線 - safari 與 wss 連線建立
- safari 對著
"/"群組的節點詢問route-groupgroup1怎麼走,因此對著 wss 發問 - wss 對著
"/"群組的節點詢問route-groupgroup1怎麼走,因為詢問來自 safari,因此對著 chrome 發問 - chrome 自己在
group1,回覆自己的 id - wss 收到回覆,回覆 safari 得到的 id 加上自己的 id
- safari 透過 wss 與 chrome 連線進入
group1,因此透過 wss 與 chrome 進行 WebRTC 連線並請求加入group1 - safari 與 chrome 的 WebRTC 連線建立完成並且加入
group1
不過這個網路顯然大家會來來去去的,safari 只跟 group1 保持一個連線感覺很危險,所以加上 neighbor 數量維護的機制,每個節點自己會盡可能維持一個群組內的連線數(也就是鄰居 neighbor 數)在 3 - 6 個之間:
- 低水位: 3 -- 低於這個數量會嘗試尋找更多 neighbor
- 高水位: 6 -- 高於這個數量會去減少 neighbor 數量
- 滿水位: 10 -- 不接受更多連線
- wss 的滿水位設定在 100 避免加入不了網路
所以舉例來說:

- 假設延續剛剛建立的狀態,且 wss 已經與另外 5 個節點連線
- wss 有 7 個連線了,需要隨機挑選一個節點斷線,這邊假設選擇 safari 斷線,同時 safari 發覺自己的 neighbor 過少,詢問
group1是否還有其他人 - chrome 回覆其他 neighbor 的 id (firefox1, firefox2)
- safari 透過 chrome 與 firefox1, firefox2 建立 WebRTC 連線
- safari 與 firefox1, firefox2 的 WebRTC 連線建立完成
花了這麼多力氣建立了網路...總得來送些訊息吧?群組廣播的時候發起廣播的節點需要附上隨機的 msgId 然後送給 neighbors,收到廣播訊息的節點先看一下 msgId 是否有看過,如果看過就不要理他,反之則觸發事件告訴更上面的 application 層並且繼續傳遞自己的其他 neighbors,一樣舉個例子:

- 假設 group1 已經有 6 個節點,連接的拓撲上有環
- safari 發出廣播,假設
msgId為123,傳送給 chrome, firefox 與另一個 safari - chrome 與 firefox 之間如果又收到同樣的
msgId123,則會避免繼續再把訊息散布下去
不過如果有 neighbor 離開, 斷線又或是一堆人離開的時候,還沒有想到一個好方法避免孤島產生,目前的方式就是重新找 wss 加入群組,其實是很沒效率而且很容易出問題的方法...所以這個網路或許建立連線的時候還行,但如果要在人來來去去的情況下持續運作是肯定有問題的
要讓這個 network 能用的話需要...
- 能跨多個 node 進行連線以增加多 hop 連線效率
- 上面提到透過 viaPeer,意思是只能透過一個節點來幫忙連線,如果要跨多個點連線則是要依序跟路上所有人建立連線
- wss 能夠提供 stun/turn 服務,有自己嘗試架設了 stun server 用在下面的 demo 看起來還行
- 分散式,並且同時可以避免孤島產生的機制
- 感覺上 Distributed hash table 可能是解法之一
- 解決安全性問題
- 釋出為一個連線框架
拿來做個 Youtube sync 聊天室
畢竟都花這麼多力氣了,就來做點東西刷點存在感,這是一個群組聊天室,加上 Youtube 同步播放(或是同步顯示 imgur 圖片)的功能,跑起來大概像是這樣:
DEMO 站連結:
https://static.pastleo.me/unnamed-network-chat-ysync/
可以的話請用 chrome / chromium 最新電腦版,同時網路順暢的情況下試玩,這個東西就如同上面講的還有問題,所以有 bug 也算是在預期內
個人是期望這個 network 可以做到這些事情:
- 使得瀏覽器不用安裝任何 plugins / add-ons 就可以使用分散式應用程式
- 降低架設 wss 的技術門檻,人人都可以成為網路進入點
- 讓不同應用程式可以共用這個 network 來建構多人遊戲、共筆系統等
unnamed-network 專案:
https://github.com/pastleo/unnamed-network
如果看完有興趣歡迎來按個星星讓我知道這個東西可以繼續做,或是透過我的個人網站 https://pastleo.me/ 上的聯絡方式找到我,感謝各位的閱讀!
本技術文章同時也發布在個人站上:https://md.pastleo.me/webrtc-unnamed-network-chatroom