GoのwasmでWebRTC P2Pによるチャットを作ってみた
GoのWebAssemblyでWebRTC DataChannel P2Pでチャットを作る検証をしました!
百聞は一見に如かずかと思いますので作成したチャットを紹介します。
STARTボタンを押すとマッチングの待機列に入り、マッチングすると待機している誰かとP2Pでのチャットが始まります。
テキストボックスにメッセージを入力してSENDボタンを押すと相手に送れるというシンプルな作りです。
Gitリポジトリは以下になります
構成
フロント
後述するAyameクライアントのwasmファイルを含む静的コンテンツはCloudflare Pagesにデプロイしています。
始めはGoogle Cloudのインスタンス(無料枠@オレゴン州)に立てたnginxの下に置いていたのですが、5MBのwasmファイルのDLに15s以上かかってしまったため、Cloudflare Pagesに移行しました。
バックエンド
以下の2つのバックエンドサービスをGoogle Cloudのインスタンスにデプロイしています。
signalingサーバ
WebRTCの接続のために必要なSDPというデータの交換のために必要なsignalingという操作を捌くWebSocketサーバです。(詳しくは時雨堂さんの記事を参照ください: WebRTC 入門)
時雨堂さんのOSSであるAyameを使わせていただいています。
こちらはP2P専用のsignalingを実現でき、今回実現自体ことにピッタリと合っていました。
matchmakingサーバ
チャットに接続してきたユーザ同士をP2P限定でマッチングさせるためのサーバです。
Go製でWebSocket経由でのマッチングサービスが見当たらず自作しています(なぜかRedisクライアント用のものはいくつかある)。
以下コア部分実装
// Matchmaking Server Code
for {
if session.CanMatch() {
now := time.Now()
roomID := entity.NewHash(now).String()
p1, _ := session.Dequeue()
p2, _ := session.Dequeue()
match[p1.ID()], match[p2.ID()] = p2, p1
broadcast <- NewResMsg(p1.Conn(), roomID, p2.ID(), now)
broadcast <- NewResMsg(p2.Conn(), roomID, p1.ID(), now)
log.Printf("Matched!: %s vs %s", p1.ID(), p2.ID())
continue
}
}
処理の流れ
誰かと繋がるためのSTARTボタンを押してからSENDボタンでメッセージが送れるようになるところまでの処理の流れを説明します。
ユーザのマッチング
STARTボタンを押すとブラウザと自作のマッチングサーバとWebSocket通信を開始します。
ここで自作のマッチングサーバ内のメモリ上にある待機列に登録されます(待機列の実態はGoのmap)
待機列に入った人数が2人になったら、その2人に対して後段で使うroomIDを返却します。
ここでマッチングサーバの役割は終了しているので張ったWebSocket接続ももここでCloseします。
WebRTC P2P接続 + DataChannel接続
前段で取得したroomIDを使ってWebRTC P2Pの接続を行います。
前述したAyameというサーバ経由でsignalingを行います、GoのwasmからAyameとやりとりするためにgo-ayameというAyameクライアントのOSSを使っています。
ただし、以下の修正を加えています。
- candidateの送信処理(これがないとgo-ayame同士のWebRTC通信ができない)
- WebRTCパッケージpionのversion up(v2->v3)
- wasmをbuildする際にundefindになる処理の削除(これをしないとwasmにbuildできない)
- 幸いDataChannelでのチャットに必要な処理は削らずに済んだ
- DataChannel開通後にsignalingサーバを閉じる
// Wasm Code
// create signaling server conection
conn = ayame.NewConnection(signalingURL.String(), resMsg.RoomID, ayame.DefaultOptions(), false, false)
conn.OnOpen(func(metadata *interface{}) {
log.Println("Open")
var err error
dc, err = conn.CreateDataChannel("matchmaking-example", nil)
if err != nil && err != fmt.Errorf("client does not exist") {
log.Printf("CreateDataChannel error: %v", err)
return
}
log.Printf("CreateDataChannel: label=%s", dc.Label())
// recieve message
dc.OnMessage(onMessage())
})
conn.OnConnect(func() {
logElem("[Sys]: Matching! Start P2P chat not via server\n")
// close connection for signaling after create Datachannel
conn.CloseWebSocketConnection()
connected <- true
})
conn.OnDataChannel(func(c *webrtc.DataChannel) {
log.Printf("OnDataChannel: label=%s", c.Label())
if dc == nil {
dc = c
}
// recieve message
dc.OnMessage(onMessage())
})
// connect to signaling server
if err := conn.Connect(); err != nil {
log.Fatal("Failed to connect Ayame", err)
}
DataChannel接続後
SENDボタン押下で入力されたテキストをDataChannelへ送信
DataChannelがデータを受け取ったら画面の所定の要素に書き込む処理を書き込んでいます。
ちなみに画面のレイアウトははWebRTC DataChannelのサンプルプログラムのdemo.htmlを参考にしています。
まとめ
GoのWebAssemblyでもWebRTC DataChannel P2Pを扱える、ブラウザ上でP2P通信できるということが検証によってわかりました。
Ayameを提供してくださっている時雨堂さんとgo-ayameを作ってくださったhakoberaさんに感謝いたします。
加えてXを見て触ってくれた方々とお話しすることができて大変嬉しかったです、お話しした方ありがとございました...
(前述のAyameを提供されている時雨堂さんの方ともお話できた😭)
Discussion