在WebRTC中,信令發(fā)揮著舉足輕重的作用,但是webrtc工作組并沒有對信令交互進行標(biāo)準(zhǔn)化,留給開發(fā)人員自行選擇。這也導(dǎo)致了信令交互的方案出現(xiàn)了多種,了解這些方案之間的差異,將有助于我們在研發(fā)WebRTC應(yīng)用程序時做出正確的選擇。
1 信令的作用
在實時通信中,信令的作用主要體現(xiàn)在以下幾個方面:
- 協(xié)商媒體功能和設(shè)置
- 標(biāo)識和驗證會話參與者的身份
- 控制媒體會話、指示進度、更改會話和終止會話
- 當(dāng)會話雙方同時嘗試建立或更改會話時,實施雙占用分解
以上幾點功能,在WebRTC中,只有第1項是必須功能,第2、3和4項均為可選功能。
1.1 為何沒有建立信令標(biāo)準(zhǔn)
在WebRTC中,要讓兩個(web應(yīng)用程序)瀏覽器之間能夠進行互操作,無需建立標(biāo)準(zhǔn)信令。因為web服務(wù)可以確保兩個瀏覽器通過下載同一份JavaScript代碼,來實現(xiàn)相同的定制信令通信協(xié)議。
Web模型只對極少的組件建立了統(tǒng)一的標(biāo)準(zhǔn),如下圖中,服務(wù)器負責(zé)選擇信令協(xié)議。并確保web應(yīng)用程序或網(wǎng)站的各個用戶支持改協(xié)議。web服務(wù)器A和B無需使用相同的信令協(xié)議,但各自都能夠使用兩個瀏覽器建立媒體會話。

需要了解的是,WebRTC是可以支持與VoIP或視頻系統(tǒng)進行集成的,這個時候由于IP電話或視頻終端為特殊終端,無法運行web應(yīng)用程序。那么要實現(xiàn)與其互操作的唯一方式,就是支持其使用的專用信令協(xié)議,例如SIP或Jingle。
1.2 媒體協(xié)商
WebRTC規(guī)范包含了針對“信令通道”的要求。信令最重要的任務(wù)就在于,在參與對等連接的兩個瀏覽器之間交換會話描述協(xié)議(SDP)對象中包含的信息。SDP包含供瀏覽器中的RTP媒體棧配置媒體會話所需要的全部信息。
- 媒體類型(音頻、視頻、數(shù)據(jù))
- 所用的編碼器(Opus、G711等)
- 用于編解碼器的各個參數(shù)或設(shè)置
- 有關(guān)寬帶信息
- 交換候選地址,用于ICE打洞
- 交換用于SRTP的密鑰材料
1.3 標(biāo)識和身份驗證
使用標(biāo)準(zhǔn)信令協(xié)議(如SIP或Jingle)發(fā)起實時通信時,信令通道將提供參與者的標(biāo)識,并可以選擇進行身份認證。在WebRTC中,除信令之外,還有兩個渠道可用于確定身份。第一種渠道是參見Web應(yīng)用程序的上下文。例如一個web用戶希望與另一個用戶建立會話時,此web應(yīng)用程序?qū)⑵聊幻鳛闃?biāo)識提供給對方,對方用戶只能無條件相信對端提供的標(biāo)識符可信。第二種渠道是查看URL中可能傳遞的標(biāo)識。此URL中包含隨機令牌。通過這種方式建立webrtc會話時,雙方都應(yīng)該知道該令牌標(biāo)識。
WebRTC中定義了另外一種標(biāo)識方法,即通過媒體通道來進行身份確認。在信令交互階段。瀏覽器A生成自己的證書對pub-cert-A,pri-key-A,并產(chǎn)生公鑰的指紋fgrp-A,此指紋作為SDP的一部分,隨信令交互到達瀏覽器B。同樣瀏覽器B也會以相同的方式,將代表自己身份的公鑰指紋隨信令交互傳遞給瀏覽器A。在媒體通道建立階段,瀏覽器A和瀏覽器B開始ICE打洞,然后進行DTLS握手,建立DTLS后,此時瀏覽器B已經(jīng)獲得瀏覽器A的公鑰證書pub-cert-A與其指紋fgrp-A,并進行驗證,確認其是否匹配,從而驗證瀏覽器A的身份確實是會話協(xié)商階段的用戶,同樣瀏覽器A也可以對瀏覽器B進行驗證。
1.4 控制媒體會話
傳統(tǒng)多媒體信令協(xié)議(如SIP 或 Jingle或某種專用協(xié)議)可提供會話呼叫控制。在WebRTC中,雖然需要信令才能發(fā)起或更改媒體會話,但不需要信令來指示狀態(tài)或終止會話。
1.5 雙占用分解
當(dāng)通信會話的雙方同時嘗試建立或更改會話的時候,就會出現(xiàn)雙占用問題。SIP等信令協(xié)議內(nèi)置有雙占用分解功能。
2 信令傳輸
WebRTC信令的傳輸方式通常有三種:HTTP、Websocket和數(shù)據(jù)通道。
2.1 HTTP傳輸
HTTP也可以用于傳輸WebRTC信令。瀏覽器可發(fā)起新的HTTP請求,以便向服務(wù)器發(fā)送信令信息并從中接收信令信息。信令信息可使用GET或POST方法或已應(yīng)答形式傳輸。如果信令服務(wù)器(注意此處之所以為信令服務(wù)器,因為信令服務(wù)器收到的HTTP請求中包含CORS相關(guān)字段,需要信令服務(wù)器進行處理)支持跨域資源共享,則其IP地址可以不同于web服務(wù)器。即web服務(wù)和HTTP信令服務(wù)可以是同一個服務(wù),也可以是兩個獨立的服務(wù)。
2.2 websocket傳輸
WebSocket傳輸允許瀏覽器開通一個與服務(wù)器的雙向連接,此連接最初采用HTTP請求的形式,但是隨后升級為websocket。只要websocket服務(wù)支持CORS(Cross-Origin Resource Sharing),websocket服務(wù)器的地址可以不同于web服務(wù)器。
2.3 數(shù)據(jù)通道傳輸
由于兩個瀏覽器之間建立數(shù)據(jù)通道之后,它就會提供直接的低延遲連接,這非常適合用于傳輸信令。但是由于最初數(shù)據(jù)通道建立時需要單獨的信令機制,因此數(shù)據(jù)通道無法單獨用于傳輸所有的webrtc信令。
3 信令協(xié)議
WebRTC信令協(xié)議的選擇至關(guān)重要,而且不必局限于所選的信令傳輸方式。開發(fā)人員可選擇創(chuàng)建自己的專有信令協(xié)議,采用 SIP 或 Jingle 等標(biāo)準(zhǔn)信令協(xié)議,或者使用通過抽象化處理剝離了信令協(xié)議細節(jié)的庫。
采用專有信令協(xié)議的優(yōu)點在于,它可以非常簡單,并且只有提供應(yīng)用程序所需的功能。如果 WebRTC 對等連接始終僅限于兩個瀏覽器,而不通過中間環(huán)節(jié)或不通向 SIP 或 Jingle VoIP 或者視頻終端,則適合采取這一選項。
下面我們主要介紹一下可能用到的兩種自定義信令的方式,對于專有信息協(xié)議(如SIP或Jingle)就不進行詳細介紹了。
3.1 信令標(biāo)識
為實現(xiàn)標(biāo)識和驗證會話參與者的身份的作用,需要在服務(wù)器中設(shè)置某種路由邏輯。如果瀏覽器和服務(wù)器之間的給定連接可以通過令牌標(biāo)識,則發(fā)送至服務(wù)器的信令消息可包含另一個“服務(wù)器至瀏覽器”連接的令牌。這樣,Web服務(wù)器代碼將在兩個連接之間充當(dāng)代理或用于轉(zhuǎn)發(fā)信息。
3.2 HTTP輪詢
HTTP輪詢是一種簡單的專有信令方案,通過在 JavaScript 或 jQuery 中調(diào)用 XHR,可使 JavaScript 應(yīng)用程序針對Web服務(wù)器生成新的HTTP請求并處理HTTP響應(yīng)。XHR 是一種W3C標(biāo)準(zhǔn)API,XHR JavaScript API 的各個功能組件均受瀏覽器支持,雖然 XHR 的名稱中包含 HTTP請求,但除了發(fā)送 XML 請求之外,它還可以發(fā)送 JSON 或 明文。XHR 可以使瀏覽器生成新的 HTTP 或 HTTPS 請求,例如 GET、PUT、POST等,相應(yīng)的 API 調(diào)用將指定要使用的方法以及IP地址和端口號。針對請求的響應(yīng)將會被返回給 JavaScript。
要使用 XHR 作為 WebRTC 信令通道,Web服務(wù)器需要運行相應(yīng)的應(yīng)用程序,用于通過另一個XHR通道接收HTTP請求并從一個瀏覽器接收信息以代理的形式轉(zhuǎn)發(fā)給另外一個瀏覽器。
為了交換信令信息。每個瀏覽器中運行的JavaScript會定期向信令服務(wù)器發(fā)起HTTP消息輪詢,瀏覽器使用POST方法發(fā)送信息消息,服務(wù)器收到的信令信息包含在發(fā)送給POST的200 OK響應(yīng)中。(請注意,此處HTTP請求均為短連接,每一個消息都是一個新的請求。)

3.3 WebSocket代理
對于用來傳輸 WebRTC 信令的 WebSockets 代理,其使用的服務(wù)器將具有公共的IP地址,兩個對等連接的服務(wù)器均可以訪問。每個瀏覽器都與 websocket代理服務(wù)器建立一個連接,代理服務(wù)器進行消息的轉(zhuǎn)發(fā)。

3.4 信令協(xié)議總結(jié)
| 方案 | 服務(wù)器要求 | 優(yōu)點 |
|---|---|---|
| WebSocket代理 | 提供服務(wù)器代碼的websocket服務(wù)器 | 無需信令基礎(chǔ)架構(gòu) |
| XML HTTP | 提供服務(wù)器代碼的web服務(wù)器 | 無需基礎(chǔ)信令架構(gòu) |
| SIP | 支持SIP websocket傳輸?shù)?SIP 注冊/ 代理服務(wù)器 | 易與SIP終端或基礎(chǔ)架構(gòu)互操作,無需無服務(wù)器代碼 |
| Jingle | 支持 XMPP websocket傳輸?shù)?XMPP服務(wù)器 | 易與Jingle終端或基礎(chǔ)架構(gòu)互操作,無需服務(wù)器代碼 |
| 數(shù)據(jù)通道 | 用于建立數(shù)據(jù)通道的websocket或web服務(wù)器 | 信令延遲短并可以保護信令隱私 |
4 websocket信令服務(wù)器示例
此處使用js庫 socket.io
var log4js = require('log4js');
var http = require('http');
var https = require('https');
var fs = require('fs');
var socketIo = require('socket.io');
var express = require('express');
var serveIndex = require('serve-index');
var USERCOUNT = 3;
log4js.configure({
appenders: {
file: {
type: 'file',
filename: 'app.log',
layout: {
type: 'pattern',
pattern: '%r %p - %m',
}
}
},
categories: {
default: {
appenders: ['file'],
level: 'debug'
}
}
});
var logger = log4js.getLogger();
var app = express();
app.use(serveIndex('./public'));
app.use(express.static('./public'));
//http server
var http_server = http.createServer(app);
http_server.listen(80, '0.0.0.0');
var options = {
key : fs.readFileSync('./public/cert/server-key.pem'),
cert: fs.readFileSync('./public/cert/server-cert.pem')
}
//https server
var https_server = https.createServer(options, app);
var io = socketIo.listen(https_server);
//服務(wù)端收到連接后的處理函數(shù)
io.sockets.on('connection', (socket)=> {
/*處理此連接上的 message 類型的消息*/
socket.on('message', (room, data)=>{
logger.debug('message, room: ' + room + ", data, type:" + data.type);
socket.to(room).emit('message',room, data);
});
/*處理此連接上的 join 類型的消息*/
socket.on('join', (room)=>{
socket.join(room);
var myRoom = io.sockets.adapter.rooms[room];
var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
logger.debug('the user number of room (' + room + ') is: ' + users);
if(users < USERCOUNT){
socket.emit('joined', room, socket.id); //發(fā)給除自己之外的房間內(nèi)的所有人
if(users > 1){
socket.to(room).emit('otherjoin', room, socket.id);
}
}else{
socket.leave(room);
socket.emit('full', room, socket.id);
}
//socket.emit('joined', room, socket.id); //發(fā)給自己
//socket.broadcast.emit('joined', room, socket.id); //發(fā)給除自己之外的這個節(jié)點上的所有人
//io.in(room).emit('joined', room, socket.id); //發(fā)給房間內(nèi)的所有人
});
/*處理此連接上的 leave 類型的消息*/
socket.on('leave', (room)=>{
socket.leave(room);
var myRoom = io.sockets.adapter.rooms[room];
var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
logger.debug('the user number of room is: ' + users);
//socket.emit('leaved', room, socket.id);
//socket.broadcast.emit('leaved', room, socket.id);
socket.to(room).emit('bye', room, socket.id);
socket.emit('leaved', room, socket.id);
//io.in(room).emit('leaved', room, socket.id);
});
});
https_server.listen(443, '0.0.0.0');
console.log("start singal");