请点击上方蓝字TonyBai订阅公众号!
在《WebRTC第一课:从信令、ICE到NAT穿透的连接建立全流程》一文中,我们从理论层面全面细致地了解了WebRTC连接建立的完整流程。这个流程大致可以分为以下几个阶段:
与信令服务器的交互
ICE候选项的采集、交换与排序
形成ICE候选检查表、进行连通性检查,并最终确定最优候选路径
这个过程的复杂性不言而喻。即便多次阅读全文,读者可能仍难以形成深入的理解。因此,如果能够配上一个真实的示例,相信会更有助于读者全面把握这一过程的细节和原理。
在这篇文章中,我就为大家呈现一个真实的示例,我将使用Go语言开源WebRTC项目pion/webrtc[1]来实现一个基于datachannel的WebRTC演示版程序,通过将pion/webrtc的日志级别设置为TRACE级,输出更多pion/webrtc实现层面的日志,以帮助大家理解WebRTC建连过程。同时,我还会实现一个简易版的基于“Room抽象模型”的信令服务器,供WebRTC通信两端交换信息使用。希望该示例能帮助大家更好的理解WebRTC端到端的建连流程。
按照WebRTC建连的流程,我们先来实现一个简易版的信令服务器。
注:提醒各位读者,本文中所有例子均以演示和帮助大家理解为目的,不建议在生产中使用示例中的代码。
1. 信令服务器(signaling-server)
下面是一个基于WebSocket的WebRTC信令服务器的简化实现,使用WebSocket进行WebRTC信令交换可以提供更快速、更高效和更灵活的通信体验,同时WebSocket生态丰富,可复用的代码库有很多,实现起来也比较简单。
这个信令服务器是基于Room抽象模型的,因此其主要结构是一个Room结构体,代表一个聊天室。我们具体看一下该信令服务器的实现代码:
// webrtc-first-lesson/part2/signaling-server/main.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
type Room struct {
Clients map[*websocket.Conn]bool
mu sync.Mutex
}
var (
upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
rooms = make(map[string]*Room)
roomMu sync.Mutex
)
func main() {
http.HandleFunc("/ws", handleWebSocket)
log.Println("Signaling server starting on :28080")
log.Fatal(http.ListenAndServe(":28080", nil))
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Error upgrading to WebSocket:", err)
return
}
defer conn.Close()
remoteAddr := conn.RemoteAddr().String()
log.Println("New WebSocket connection from:", remoteAddr)
roomID := r.URL.Query().Get("room")
if roomID == "" {
roomID = fmt.Sprintf("room_%d", len(rooms)+1)
log.Printf("Created new room: %s\n", roomID)
}
roomMu.Lock()
room, exists := rooms[roomID]
if !exists {
room = &Room{Clients: make(map[*websocket.Conn]bool)}
rooms[roomID] = room
}
roomMu.Unlock()
room.mu.Lock()
room.Clients[conn] = true
room.mu.Unlock()
log.Printf("Client[%v] joined room %s\n", remoteAddr, roomID)
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Error reading message:", err)
break
}
var msg map[string]interface{}
if err := json.Unmarshal(message, &msg); err != nil {
log.Println("Error unmarshaling message:", err)
continue
}
msg["roomId"] = roomID
updatedMessage, _ := json.Marshal(msg)
room.mu.Lock()
for client := range room.Clients {
if client != conn {
clientAddr := client.RemoteAddr().String()
if err := client.WriteMessage(messageType, updatedMessage); err != nil {
log.Println("Error writing message:", err)
} else {
log.Printf("writing message to client[%v] ok\n", clientAddr)
}
}
}
room.mu.Unlock()
}
room.mu.Lock()
delete(room.Clients, conn)
room.mu.Unlock()
log.Printf("Client[%v] left room %s\n", remoteAddr, roomID)
}
我们看到:Room结构体包含一个WebSocket连接的map和一个互斥锁。演示程序使用全局变量rooms(房间map)和相应的互斥锁管理房间和加入房间的连接,并在房间内进行消息广播,以保证消息能转发到参与通信的所有端(Peer)。当然,如果仅有两端在一个房间中,那么这就变成了一对一的实时通信。
这个信令服务器程序启动后,默认监听28080端口,当客户端连接时,会根据URL参数来将客户端连接加入到某个房间,如果房间号参数为空,则代表该客户端期望创建一个房间。先创建房间并加入的客户端作为answer端,等待offer端的连接。当从某个客户端连接收到消息后,会广播给房间内的其他客户端。当客户端断开连接时,便将其从房间中移除。
当然这仅是一个演示版程序,并未对历史建立的房间进行回收,同时也没有进行身份认证等安全方面的控制。
接下来,我们再来看看借助信令服务器进行端到端实时通信的端侧WebRTC应用的实现。
2. 端侧WebRTC应用(webrtc-peer)
WebRTC应用的代码通常都很“样板化”。在开发WebRTC应用程序时,信令连接、设置本地和远程描述、收集ICE候选以及转发信令消息等步骤都是一些常见且重复性较高的任务。这些步骤在不同的WebRTC应用程序中通常都大同小异。以下是这些重复性任务的一些具体步骤示例:
信令连接处理
创建信令通道(如WebSocket连接)
监听连接建立、断开等事件
通过信令通道交换offer/answer等信令消息
本地和远程描述设置
创建RTCPeerConnection实例
设置本地描述(createOffer/createAnswer)
<