目錄
MediaSoup中的 protoo.websocket 建立連接流程
前言
上篇文章對【流媒體服務器Mediasoup】 NodeJs與C++信令通信詳解及Linux下管道通信的詳解(五),本章節主要對MediaSoup的客戶端與服務端 源碼中源碼中 信令通訊使用的protoo.WebSocket 詳解,以及整個信令的處理過程
在下一篇文章中將繼續對MediaSoup的源碼進行分析和架構的講解。
介紹protoo.WebSocket
protoo是一個面向多方實時通信應用的最小可擴展Node.js信令框架。
它提供了一個服務器端Node.js模塊和一個客戶端JavaScript庫。其主要目的是爲應用程序提供輕鬆添加羣聊、狀態和多方多媒體功能的能力。與protoo.WebSocket 一樣有着房間管理的有 Socket.IO
Messages protoo消息體介紹
(消息體)
protoo定義了一個基於JSON請求、響應和通知的信令協議。由應用程序定義和擴展信令協議以及這些消息的內容,以實現所需的特性集。
Request (請求格式)
{
request : true,
id : 12345678,
method : 'chatmessage',
data :
{
type : 'text',
value : 'Hi there!'
}
}
Response(響應體)
Success response 成功響應體
{
response : true,
id : 12345678,
ok : true,
data :
{
foo : 'lalala'
}
}
Error response 失敗響應體
Notification(通知)
需要進行響應,主動發數據給服務端
{
notification : true,
method : 'chatmessage',
data :
{
foo : 'bar'
}
}
protoo-server 介紹
//nodeJs安裝服務端庫
npm install --save protoo-server
//代碼中引用
const protooServer = require('protoo-server');
WebSocketServer
const options =
{
//允許的最大接收幀大小(以字節爲單位)。單幀消息也將限於此最大值。
maxReceivedFrameSize : 960000, // 960 KBytes.
//允許的最大消息大小(對於分段消息),以字節爲單位。
maxReceivedMessageSize : 960000,
//是否對傳出消息進行分段。如果爲true,則郵件將自動分成最大爲fragmentationThreshold字節的塊
fragmentOutgoingMessages : true,
//在自動分段之前,幀的最大大小(以字節爲單位)。
fragmentationThreshold : 960000
};
const server = new protooServer.WebSocketServer.Room(httpServer, options);
當WebSocket客戶端嘗試連接到WebSocket服務器時觸發事件。
server.on('connectionrequest', (info, accept, reject) =>
{
// info 是屬於一個連鏈接的所有信息,可以根據info獲取如一些url或者其他信息來判斷處理相對應的業務邏輯
if (something in info)
{
const transport = accept();
// 創建一個房間和房間用戶
const peer = async room.createPeer('bob', transport);
}
else
{
reject(403, 'Not Allowed');
}
});
函數接收的參數:
參數 | 描述 |
---|---|
info | 具有有關連接嘗試信息的對象。 |
accept | 如果接受連接將調用的函數。 |
reject | 如果拒絕連接,則調用該函數。 |
info 是屬於一個連鏈接的所有信息,可以根據info獲取如一些url或者其他信息來判斷處理相對應的業務邏輯
info 對象象具有以下字段:
領域 | 描述 |
---|---|
request | 代表在Websocket握手期間收到的HTTP請求的Node.js 連接對象的信息。 |
origin | HTTP請求中基礎機地址的值 |
socket | Node.js net.Socket 對象。 |
WebSocketTransport
accept()在的connectionrequest事件內調用時創建WebSocketServer。它代表與客戶端建立的WebSocket連接。如:
server.on('connectionrequest', (info, accept, reject) =>
{
//建立和遠程客戶端的連接
const protooWebSocketTransport = accept();
});
Room(房間)
創建一個新的房間
const room = new protooServer.Room();
返回房間裏 多有的Peer(成員)
for (let peer of room.peers)
{
console.log('peer id: %s', peer.id);
}
//------------------------
if(room.closed){
//房間關閉了
}else{
//房間沒有關閉了
}
在此房間內創建用戶。它解析爲用戶實例。如果給出了錯誤的參數,或者房間中已經有一個具有相同ID的用戶,則它會拒絕。
close關閉房間併發出關閉事件。這個房間內的所有人也將被關閉,他們的關閉事件將被觸發。
const peer = await room.createPeer('alice', transport);
//hasPeer如果存在會返回PeerId
if(room.hasPeer(peerId)){
//房間內有這個人
}else{
// 房間內沒有這個人
}
//返回房間的爲pereID的用戶對象
const user= room.getPeer(peerId);
//關閉房間時觸發的回調函數
room.on('close', () =>
{
//DO SOMETINGS
}
);
//關閉房間
room.close();
Peer(房間成員)
代表遠端的一個連接實例,其實也可以理解成一個成員用戶。
perr對象中主要有2個字段
id 唯一標識
data 可自定義數據
發送信令數據給 指定的peer客戶端
//請求信令
try
{
const data = await peer.request('chicken', { foo: 'bar' });
console.log('got response data:', data);
}
catch (error)
{
console.error('request failed:', error);
}
//服務端主動下發 通知的信令
peer.notify('lalala', { foo: 'bar' });
接收來自peer客戶端的信令消息
peer.on('request', (request, accept, reject) =>
{
if (根據 request 的一些信息做一些判斷)
accept({ foo: 'bar' });
else
reject(400, 'Not Here');
});
//關閉對等方及其底層傳輸,併發出關閉事件。
peer.close();
參數 | 描述 |
---|---|
請求 | 一個protoo請求。 |
接受 | 如果接受請求,則調用該函數。 |
拒絕 | 如果拒絕請求,則調用該函數。 |
accept函數具有以下參數:
Parameter | Default | Description |
---|---|---|
[data] | {} |
響應的數據體 |
rejuct函數具有以下參數:
Parameter | Default | Description |
---|---|---|
errorCode | 錯誤碼 | |
[errorReason] | 錯誤原因 |
or:
Parameter | Default | Description |
---|---|---|
error | 錯誤的對象實例 |
收到通知時 以及 關閉時
通過對對等方調用close()關閉peer、遠程關閉基礎傳輸或關閉時時激發的事件。
//接收到服務端通知
peer.on('notification', (notification) =>
{
// Do something.
});
//客戶端關閉時觸發
peer.on('close', fn())
protoo-client 介紹
//安裝依賴庫
npm install --save protoo-client
//nodejs使用庫
const protooServer = require('protoo-server');
WebSocketTransport
WebSocketTransport創建WebSocket連接。
const transport = new protooClient.WebSocketTransport('wss://example.org');
Parameter | Description |
---|---|
url | WebSocket 連接的地址 |
[options] | 包括websocket.W3CWebSocket的選項(除了requestUrl之外的所有選項)和一個retry參數 |
retry參數與給retry.operation()的options對象匹配,並控制連接和重新連接嘗試,如果options.retry
未給出,則默認爲以下值
{
forever : true //是否一直重連,默認爲false。
retries : 10, //重試連接該操作的最大時間。默認值爲10
factor : 2, //重練的的次數
minTimeout : 1 * 1000, //開始第一次連接之前的毫秒數。默認值爲1000
maxTimeout : 8 * 1000 //兩次重連之間的最大毫秒數。默認值爲Infinity。
};
Peer(房間的參與者,客戶端用戶)
創建本地peer
const peer = new protooClient.Peer(transport);
參數 | 描述 |
---|---|
transport | 一個WebSocketTransport 實例。 |
Peer對象中有data參數 ,可寫的自定義對象,直到應用程序爲止。
peer.data.bar = 1234;
console.log(peer.data.bar);
在客戶端中 Peer的 close、notify、request 函數等 和服務端的一樣,這裏不做過多講解
peer.connected 字段
標識對 客戶端是否已連接到服務端。
連接傳輸時激發的事件
on('open', fn())
與服務器的連接失敗時激發的事件(由於網絡錯誤、未運行服務器、無法訪問服務器地址等)。
客戶端將嘗試連接其重試選項中定義的次數。重試之後,關閉事件將觸發。
on('failed', fn(currentAttempt))
參數 | 描述 |
---|---|
currentAttempt | 重新連接嘗試(從1開始)。 |
當已建立的連接突然關閉時激發的事件。peer將啓動在其重試選項中定義的重新連接過程。
peer將嘗試重新連接其重試選項中定義的次數。重試之後,關閉事件將觸發
on('disconnected', fn())
如果
close()
是在服務器端中觸發,服務器端peer
或此客戶端中的peer
,則不會進行任何重新連接嘗試。
MediaSoup中的 protoo.websocket 建立連接流程
下面將簡單介紹源碼中使用到的創建連接、創建房間。創建peer的地方
WebSocket的連接
文件定位: mediasoup-demo/server/server.js
runProtooWebSocketServer() 方法中主要對protoo socket的初始化,具體看源碼
方法中體現了創建socket server的實例,並監聽客戶端來的連接
/** 創建ProtooWebSocket地服務*/
async function runProtooWebSocketServer()
{
// 創建實例 option參數參見上面的講解
protooWebSocketServer = new protoo.WebSocketServer(httpsServer,
{
maxReceivedFrameSize : 960000, // 960 KBytes.
maxReceivedMessageSize : 960000,
fragmentOutgoingMessages : true,
fragmentationThreshold : 960000
});
// 監聽客戶端連接
protooWebSocketServer.on('connectionrequest', (info, accept, reject) =>
{
...省略部分代碼
//這裏使用了一個同步隊列,爲了防止同一時刻創建相同的房間
queue.push(async () =>
{
const room = await getOrCreateRoom({ roomId, forceH264, forceVP9 });
// 確定客戶端的請求,並生成一個連接Transport
const protooWebSocketTransport = accept();
room.handleProtooConnection({ peerId, protooWebSocketTransport });
})
.catch((error) =>
{
logger.error('room creation or room joining failed:%o', error);
reject(error);
});
});
}
room的創建
文件定位: mediasoup-demo/server/lib/Room.js
在上述的 runProtooWebSocketServer() 方法中,調用 const room = await getOrCreateRoom({ roomId, forceH264, forceVP9 });
最後再getOrCreateRoom中調用了Room類中的create 方法具體看下部分源碼,
當一個客戶端連接首先會查看房間是否已經創建,如果沒有創建那麼就調用create 方法最後實例化了一個房間。
static async create({ mediasoupWorker, roomId, forceH264 = false, forceVP9 = false })
{
// 創建 protoo room 實例
const protooRoom = new protoo.Room();
....省略部分代碼
//並實例化一個封裝的Room
return new Room(
{
roomId,
protooRoom,
mediasoupRouter,
audioLevelObserver,
bot
});
}
Peer(成員)的創建
文件定位: mediasoup-demo/server/lib/Room.js
在上述的 runProtooWebSocketServer() 方法中,監聽事件
protooWebSocketServer.on('connectionrequest', (info, accept, reject) => {}
方法中,當有一個客戶端連接,先去判斷房間是否存在,如果不存在則創建,走上面講述的創建房間流程,
這時候客戶端連接相當於一個成員加入,當房間創建好的時候,需要在房間中添加這個連接進來的成員
最後調到 room.handleProtooConnection({ peerId, protooWebSocketTransport }); 具體源碼:
從源碼以及源碼註釋中可以看出整個清晰的邏輯
handleProtooConnection({ peerId, consume, protooWebSocketTransport })
{
//查看房間是否存在這個用戶
const existingPeer = this._protooRoom.getPeer(peerId);
//如果存在則關閉此用戶
if (existingPeer)
{
existingPeer.close();
}
let peer;
// 在此房間創建一個peer 成員
try
{
peer = this._protooRoom.createPeer(peerId, protooWebSocketTransport);
}
catch (error)
{
....省略部分代碼
peer.on('request', (request, accept, reject) =>
{
//接收信令數據
this._handleProtooRequest(peer, request, accept, reject)
.catch((error) =>
{
logger.error('request failed:%o', error);
reject(error);
});
});
peer.on('close', () =>
{
// 通知其他用戶 此用戶已經退出
if (peer.data.joined)
{
for (const otherPeer of this._getJoinedPeers({ excludePeer: peer }))
{
otherPeer.notify('peerClosed', { peerId: peer.id })
.catch(() => {});
}
}
// 遍歷關閉用戶所有的數據連接通道
for (const transport of peer.data.transports.values())
{
transport.close();
}
// 退出去爲房間最後一個人,則關閉此房間
if (this._protooRoom.peers.length === 0)
{
this.close();
}
});
}
信令的接收與處理
文件定位: mediasoup-demo/server/lib/Room.js
async _handleProtooRequest(peer, request, accept, reject)
源碼中的信令不多,也可以自己添加自定義的信令
針對目前demo,一個用戶進入房間,信令的執行順序爲:
/* 信令執行的順序
getRouterRtpCapabilities
createWebRtcTransport (收 傳輸通道)
createWebRtcTransport (發 傳輸通道)
join
connectWebRtcTransport
connectWebRtcTransport
produce
produceData
produceData
*/
async _handleProtooRequest(peer, request, accept, reject)
{
switch (request.method)
{
...省略處理信令過程
}
}
小結
上述主要對protoo socket的一些Api進行簡單的講解,熟悉Api帶入源碼去分析顯得更容易一些,protoo對node的支持是相當的好,在源碼中的整個執行順序也是比較明朗,對於定製源碼也起到了關鍵的作用。
在往後的博文中,將對信令的處理從上層到C++ 進行一些系統的分析,包括整個流程的走向,調試等。