0%

webrtc学习笔记-手动建立DataChannel


概述

例子来自一个golang的webrtc项目https://github.com/pion/webrtc,这个项目提供了webrtc的go语言实现。


DataChannel

Data Communication

DataChannel使用SCTP来传输数据,提供可靠传输,确保数据有序发送。

DataChannel可以发送文本或者二进制数据,意思就是发送什么数据都可以。


DataChannel的连接建立过程

下面这张图来自博客WebRTC源码分析——呼叫建立过程之三(创建PeerConnection):

image

这张图可以清晰的看出整个连接建立的过程,其中除了两个客户端之外,还有两个重要的角色Signal-ServerStun-Server

  • Signal-Server,信令服务器。两个客户端要首先连上信令服务器,这样才能互相知道对方的存在,而且在P2P的连接真正建立成功之前,两端的数据交互全部依靠信令服务器来传输。
  • Stun-Server,stun服务器。客户端需要去询问它来得到自己的NAT地址,这样最后两端才能进行直接连接。

这里主要关注这个地方:

  1. caller调用SetLocalDescription,然后发送offer到callee(经过信令服务器)。
  2. callee收到offer,先调用SetRemoteDescription,然后生成answer,同样的SetLocalDescription,最后发送answer到caller(经过信令服务器)。

手动建立DataChannel代码

代码就是webrtc项目下自带的例子,简单改了几行,让两边都可以互相发送消息。

caller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package main

import (
"fmt"
"github.com/pion/webrtc/v3"
"myWebrtc1/signal"
)

func main() {

// Configure and create a new PeerConnection.
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}

// 可以设置日志级别
//err := os.Setenv("PION_LOG_INFO", "ALL")
//if err != nil {
// panic(err)
//}

pc, err := webrtc.NewPeerConnection(config)

// Create DataChannel.
sendChannel, err := pc.CreateDataChannel("foo", nil)
if err != nil {
panic(err)
}
sendChannel.OnClose(func() {
fmt.Println("sendChannel has closed")
})
sendChannel.OnOpen(func() {
fmt.Println("sendChannel has opened")
})
sendChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
fmt.Printf("Message from DataChannel %s payload %s\n", sendChannel.Label(), string(msg.Data))
})

// Add handlers for setting up the connection.
pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
fmt.Println(state)
})
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
if candidate != nil {
fmt.Println(signal.Encode(*pc.LocalDescription()))
}
})

// Create offer
offer, err := pc.CreateOffer(nil)
if err != nil {
panic(err)
}
if err := pc.SetLocalDescription(offer); err != nil {
panic(err)
}

// 设置对面的 Description
answer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &answer)

err = pc.SetRemoteDescription(answer)
if err != nil {
panic(err)
}

// 发送消息
go func() {
for {
message := signal.MustReadStdin()
if err := sendChannel.SendText(message); err != nil {
panic(err)
}
}
}()

// Stay alive
select {}

}

callee:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package main

import (
"fmt"
"github.com/pion/webrtc/v3"
"myWebrtc1/signal"
)

func main() {

config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}

// 可以设置日志级别
//err := os.Setenv("PION_LOG_INFO", "ALL")
//if err != nil {
// panic(err)
//}

peerConnection, err := webrtc.NewPeerConnection(config)
if err != nil {
panic(err)
}

peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
})

peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID())

// Register channel opening handling
d.OnOpen(func() {
fmt.Printf("Data channel '%s'-'%d' open.\n", d.Label(), d.ID())
})

// Register text message handling
d.OnMessage(func(msg webrtc.DataChannelMessage) {
fmt.Printf("Message from DataChannel '%s': '%s'\n", d.Label(), string(msg.Data))
})

// 发送消息
go func() {
for {
message := signal.MustReadStdin()
if err := d.SendText(message); err != nil {
panic(err)
}
}
}()
})

offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)

err = peerConnection.SetRemoteDescription(offer)
if err != nil {
panic(err)
}

// Create an answer
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
}

// Create channel that is blocked until ICE Gathering is complete
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)

// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer)
if err != nil {
panic(err)
}

// Block until ICE Gathering is complete, disabling trickle ICE
// we do this because we only can exchange one signaling message
// in a production application you should exchange ICE Candidates via OnICECandidate
<-gatherComplete

// Output the answer in base64 so we can paste it in browser
fmt.Println(signal.Encode(*peerConnection.LocalDescription()))

// Block forever
select {}

}

其中的辅助函数也是例子里面的:

  • MustReadStdin()从输入读入一行数据。
  • Encode()Decode(),将一个对象先进行marshall或者unmarshall,然后编解码成base64格式。

注意到上面对连接过程的描述中,除了caller和callee,还有两个重要角色stun信令服务器

这里stun可以直接使用公开的服务器stun:stun.l.google.com:19302,也可以自己搭建一个。

信令服务器则就是人,也就是人工手动操作:

  1. 启动caller,将输出的Description复制下来。
  2. 启动callee,将caller的Description输入进去。
  3. callee输出Description,同样的复制下来。
  4. 将callee的Description输入到caller。
  5. 连接建立成功,两边可以开始互相发送消息。

存在的问题

有时候在完成两边的Description交换之后,会卡住,然后连接建立超时,目前未研究出原因。