用 WebRTC 半分散網路弄一個聊天室
在 COSCUP 2019 講完用 WebRTC 建立半分散式網路,想說來把投影片整理成一篇文章來紀錄一下這段時間拿 WebRTC 嘗試形成網路的一些實驗結果
WebRTC 是什麼?
如果不知道什麼是 WebRTC,可以參考去年寫的文章:https://5xruby.tw/posts/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-group
group1
怎麼走,因此對著 wss 發問 - wss 對著
"/"
群組的節點詢問route-group
group1
怎麼走,因為詢問來自 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 之間如果又收到同樣的
msgId
123
,則會避免繼續再把訊息散布下去
不過如果有 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