WebRTC - Real-Time Communications on the web

大家應該都聽過 WebSocket,讓瀏覽器和伺服器兩者之間可以建立持久性的連接,並進行雙向數據傳輸;那 WebRTC 是什麼?比起 WebSocket,WebRTC 可以直接讓瀏覽器跟瀏覽器連線,一開始主要的目的是 Voice/Video conferencing,但是也提供 Data channel 傳送資料,今天就來試玩看看

WebRTC 連線架構

WebSocket 只要打個 Http 到要連線的伺服器,伺服器回覆了 101 Switching Protocols 以及 Upgrade: websocket 連線就完成,兩邊即可自由傳送資料

相較之下 WebRTC 為了讓 Client 直接連線,必須要克服 NAT 等等網路架構的問題,連線的方式可說是蠻複雜的,我把它大致分成兩個步驟:

不過在直接連線之前必須要有個雙方都連的到的伺服器來幫忙傳送 SDP 以及 ICE 資料…

示範:做個簡單的聊天室

為了實驗 WebRTC,我打算做一個最簡單的雙人聊天室,透過 Elixir Phoenix 實做一個交換 SDP, ICE 用的 Server,要聊天的雙方輸入自己以及對方的名字,WebRTC 連線建立之後透過這個連線傳送聊天訊息,弄起來大概像是這樣:

demo screenshot

為了方便了解其中的流程,我在 Javascript 中加入了很多 console.log 來方便解析

實做解析

Javascript 部份大量使用 callback,不容易看出相依狀況以及順序,我整理成大概的順序如下:

1. 加入名為 "handshake:" + 自己的名字 的 phoenix channel

使用者輸入完自己跟對方的名字之後按下 Connect 按鈕,執行 A. initAndConnectPhxChannel

  • 加入 phoenix channel phxSocket.channel("handshake:" + myName, {})

2. phoenix channel 接受加入請求

def join("handshake:" <> name , _, socket)

3. 產生 Offer SDP 並送到 phoenix channel

成功加入 phoenix channel 時,執行 B. prepareOfferAndPush

  • 建立 WebRTC rtcConnection = new RTCPeerConnection(); 並設定 callback
  • 先建立 WebRTC data channel rtcChannel = rtcConnection.createDataChannel("msg");
    • 要先建立 WebRTC 才知道要建立哪種的連線
  • 建立 Offer SDP rtcConnection.createOffer()
  • 打包上傳 Offer SDP phxChannel.push("offer", {target: peerName, offer: JSON.stringify(rtcConnection.localDescription)});

4. phoenix 幫忙把 Offer SDP 傳送到被連接者手上

def handle_in("offer", %{"target" => target, "offer" => offer}, socket)

  • WebrtcPhxWeb.Endpoint.broadcastOffer SDP 廣播到被連接者所在的 phoenix channel

5. 被連接者產生 Answer SDP 並送到 phoenix channel

從 phoenix channel 收到 offer 時,執行 C. prepareAnswerAndPush

  • 第一個按下 Connect 按鈕的使用者所傳送的 Offer 其實不會被接到,只是單純加入 phoenix channel 而已,因此這個使用者就是『被連接者』,收到 offer 時重新建立『被連線』用的 WebRTC
  • rtcConnection.setRemoteDescription(JSON.parse(offer)) 設定 offer
  • 建立 Answer SDP rtcConnection.createAnswer())
  • 打包上傳 Answer SDP phxChannel.push("answer", {target: peerName, answer: JSON.stringify(rtcConnection.localDescription)});

6. phoenix 幫忙把 Answer SDP 傳送回連接者手上

def handle_in("answer", %{"target" => target, "answer" => answer}, socket),同樣地用廣播傳送到對應的使用者所在的 channel

7. 連接者設定 Answer SDP

從 phoenix channel 收到 answer 時,執行 D. useAnswer

  • 設定好 Answer SDP rtcConnection.setRemoteDescription(JSON.parse(answer));

8. 送 ICE candidate 到 phoenix channel 上

連線條件改變時,像是 rtcConnection.setRemoteDescription 之後(無論連線方或是被連線方),WebRTC 會重新產生可用的連線方法並呼叫 rtcConnection.onicecandidate ,這邊設定成去執行 E. pushIceCandidate

  • 打包上傳 ICE candidate phxChannel.push("ice", {target: peerName, ice: JSON.stringify(candidate)});

9. phoenix 幫忙把 ICE candidate 傳送到另一個使用者

def handle_in("ice", %{"target" => target, "ice" => ice}, socket),同樣地用廣播傳送到對應的使用者所在的 channel

10. 接收 ICE candidate

從 phoenix channel 收到 ice 時,執行 F. useIceCandidate

  • 加入另一方提供之 ICE candidate rtcConnection.addIceCandidate(JSON.parse(ice));

步驟 8, 9, 10 可能會發生數次,而且似乎在 Offer SDP, Anser SDP 交換完成之前就開始(雖然應該是之後的才有效),其實 data channel 建立的瞬間就會觸發 rtcConnection.onicecandidate

透過 oniceconnectionstatechange 確認完成連線建立

rtcConnection.oniceconnectionstatechange 可以用來監控 WebRTC 的狀態,rtcConnection.iceConnectionStateconnected 或是 completed 就是連線完成

  • 送資料:rtcChannel.send(target.value);
  • 收到資料的時候 rtcChannel.onmessage 會被呼叫

接下來就是單純的聊天室 UI 元件控制

STUN & TURN

new RTCPeerConnection() 不設定任何東西的情況下在同個區域網路內兩台電腦可以透過這個流程全自動連線,但看起來只要一出 NAT 就完全連不到了…

在 ICE 過程中,可以用 UDP hole punching 打破 NAT 的限制,但是需要 STUN server 幫忙建立連線,只有連線建立初期需要這個 server 幫忙,想要詳細了解流程可以看一下 Wiki,其實比想像中還要不黑魔法

透過 new RTCPeerConnection(rtcConfig) 可以設定 STUN server我從網路上好心人士整理的 public STUN server list 中選了 google 提供的來用更動如此這般,測了一下,分別在 NAT 後面的兩個裝置也可以連線了!

UDP hole punching 並不是所有的 NAT 都能夠穿,例如 symetric NAT 就不行,要解決更複雜狀況的時候其實就是透過 TURN Server relay 所有的 traffic 了,可以想像的到這個 server 的負擔會比 STUN server 重非常多,所以網路上找不到免費的 TURN server 也是蠻合理的

稍微玩一下 & 結論

我把這個佈署到 VPS 上面玩了一下,順便測試一下手機的相容性:

  • Firefox 62 跟 Chrome 69 可以互相連線
  • Android Chrome 69 也有支援了,關螢幕打開還會自己接回來

詳細的相容性請見 caniuse.com

STUN server 幫助下,跟朋友在不同區網可以連線,透過手機網路也沒問題,兩端都是手機網路也 OK,還不清楚生活中哪裡有 symetric NAT 或是其他狀況會需要用到 TURN server 的

如果想要把這個小聊天室跑起來,請見 https://github.com/pastleo/webrtc-phx

以後使用者之間需要直接傳輸資料,像是 Web 多人遊戲,或是 video/voice conferencing 的時候,這似乎是個很不錯的解決方案