信令服务器分析

Signaling

信令和协商Signaling and Negotiation

用户建立连接的过程以信令和协商为主。有下面几个步骤:

  • 为peer连接创建一个潜在候选项的list
  • 用户A或应用选择一个要连接的用户B
  • 信令层通知用户B有人想和你连。他可以同意/拒绝
  • 用户A被通知offer同意
  • 用户A初始化与B的RTCPeerConnection
  • 俩用户通过信令服务器交换软硬件信息
  • 俩用户交换本地信息
  • 连接成功/失败

建立服务器

我们现在来建立一个自己的信令机制。允许一个用户呼叫另一个。一旦一个用户被呼叫,服务器在他们之间传递offer,answer,ICE候选。然后建立连接。

信令服务器

上个图是用户使用信令服务器时的信息流。

首先,每个用户在服务器注册。传一个简单的字符串username即可。注册后的用户可以相互呼叫。用户1发一个带有他想呼叫的用户id的offer。其他人应当answer。最后,在用户中间发送ICE候选直到他们建立连接。

去创建一个WebRTC连接服务器必须有能力在没有使用WebRTCpeer连接的情况下发送messages。因此我们使用HTML5 WebSockets—一个两端(一个web服务器一个web浏览器)的双向socket连接。现在我们开始使用WebSocket库。创建server.js文件并加入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//依赖ws库 声明一个WebSocketServer服务器
var WebSocketServer = require('ws').Server;

//创建端口9090的WebSocket服务器
var wss = new WebSocketServer({port: 9090});

//当有一个用户接入服务器
wss.on('connection', function(connection) {
console.log("user connected");

//服务器收到已接入用户的消息
connection.on('message', function(message){
console.log("Got message from a user:", message);
});

connection.send("Hello from server");
});

先要求WebSocket库。再创建9090端口的socket服务器。下一步,监听connection事件。监听用户发来的任何消息。最后我们发送一个回复给已连接的用户说Hello from Server.

终端运行node server服务器就跑起来了。

为了测试服务器。我们用wscat这个程序。他可以直接连入服务器并测试命令。先在终端让服务器跑起来之后,再开一个终端,运行wscat -c ws://localhost:9090命令。

用户注册

在我们的信令服务器中,我们将使用基于字符串的username来让我们知道信息的去向。下面更改一下connection的句柄。

1
2
3
4
5
6
7
8
9
10
11
12
connection.on('message', function(message) { 
var data;

//只接收JSON
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}

});

这样我们就做到了只接收JSON。下一步我们需要存储所有连接到用户。使用一个简单的js对象。

在我们的文件头部增加一句var users={};

这个数组里存的都是connection,而下标就是name。

现在我们还要为每一个从客户端发来的message增加一个type域。例如是登录就发login type的message。现在来定义。

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
connection.on('message', function(message){
var data;

//只接收JSON
try {
data = JSON.parse(message); //是JSON就给它分组
} catch (e) {
console.log("Invalid JSON");
data = {};
}

//分开各种type
switch (data.type) {
//一个用户尝试登录
case "login":
console.log("User logged:", data.name);

//存在这个name则拒绝登录
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//保存这个用户连接
users[data.name] = connection; //connection进入users[]
connection.name = data.name;

sendTo(connection, {
type: "login",
success: true
});

}

break;

default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});

break;
}

});

下面这一段代码是发送消息到连接的辅助函数。这个函数可以确保我们发送的消息都是JSON格式。

1
2
3
function sendTo(connection, message) { 
connection.send(JSON.stringify(message));
}

你可以在wscat发送一段json来测试一下了。

呼叫

登录后的用户想要呼叫就要做一个offer发给别人。现在我们来添加offer句柄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case "offer": 
//for ex. A
console.log("Sending offer to: ", data.name);

//如果B存在则发送A的offer细节
var conn = users[data.name];

if(conn != null){
//设置A的连接对象是B
connection.otherName = data.name;
//A的offer发送给B
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}

break;

接听

接听回话和offer句柄类似。在offer句柄后面加入answer,answer的内容无论是什么都会被发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case "answer": 
console.log("Sending answer to: ", data.name);

//for ex. UserB answers UserA
var conn = users[data.name];

if(conn != null) {
//同理回了人家的answer你的连接对象也就是他了
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}

break;

在例子中,offer和answer都是简单的字符串。但在真正的应用中他们会被SDP数据填充。

ICE候选

最后一部分是处理用户之间的ICE候选。我们在用户中间用相同的机制通过message。主要的不同就是候选消息可能会多次、无序地发送给每个用户。添加candidate句柄:

1
2
3
4
5
6
7
8
9
10
11
12
case "candidate": 
console.log("Sending candidate to:",data.name);
var conn = users[data.name];

if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}

break;

断开对话

为了让用户之间断开对话。我们要一个挂起函数。她同时会告诉服务器删掉所有的用户引用。添加leave句柄:

1
2
3
4
5
6
7
8
9
10
11
12
13
case "leave": 
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;

//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}

break;

同时吧leave事件发送给另一个用户,让他也你能相应的断开对话。

当用户从信令服务器中丢弃掉连接的时候我们还需要有close句柄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
connection.on("close", function() { 

if(connection.name) {
delete users[connection.name];

if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;

if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});

信令服务器完成。

完整的代码

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//依赖ws库
var WebSocketServer = require('ws').Server;

//开9090端口。服务器名字叫wss
var wss = new WebSocketServer({port: 9090});

//用户群组。里面存的是connection。下标是name。
var users = {};

//有人要连接时
wss.on('connection', function(connection) {

console.log("User connected");

//服务器收到了message
connection.on('message', function(message) {

var data;
//只接收JSON
try {
data = JSON.parse(message); //切分JSON存入data
} catch (e) {
console.log("Invalid JSON");
data = {};
}

//用户消息的type
switch (data.type) {
//尝试登录

case "login":
console.log("User logged", data.name);

//重名了
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//可以登则存入数组
users[data.name] = connection;
connection.name = data.name;

sendTo(connection, {
type: "login",
success: true
});
}

break;

case "offer":
//for ex. A想打给B
console.log("Sending offer to: ", data.name);

//B存在则发给B offer
var conn = users[data.name];

if(conn != null) {
/A 的连接对象设为 B
connection.otherName = data.name;

sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}

break;

case "answer":
console.log("Sending answer to: ", data.name);
//for ex. B要接听A
var conn = users[data.name];

if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}

break;

case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];

if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}

break;

case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;

//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}

break;

default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});

break;
}
});

//用户退出,比如关闭了浏览器窗口
//如果我们仍在offer\answer\candidate状态。则起作用。
connection.on("close", function() {

if(connection.name) {
delete users[connection.name];

if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
//A的连接对B象存到Conn
var conn = users[connection.otherName];
//设置A为无连接状态
conn.otherName = null;
//让B也leave会话
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});

connection.send("Hello world");

});

function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}

这篇文章一定要和入门教程02一起看~