搭建信令服務器
在創建WebRTC應用程序的某個時刻,您將不得不脫離爲客戶端開發並構建服務器。大多數WebRTC應用程序不僅僅依賴於能夠通過音頻和視頻進行通信,而且通常需要許多其他功能才能引起興趣。在本章中,我們將使用JavaScript和Node.js深入研究服務器編程。我們將爲本書的其餘部分創建基本信令服務器的基礎。
這一章,主要分爲以下部分:
- 用nodeJs搭建開發環境
- websocket與客戶端連接
- 識別用戶
- 啓動並回答WebRTC呼叫
- 處理ICE候選人傳輸
- 掛斷
在本章中,我們將僅關注應用程序的服務器部分。在下一章中,我們將構建此示例的客戶端部分。我們的示例服務器本質上是簡單的,這足以讓我們建立一個WebRTC對等連接。
搭建信令服務器
我們將在本章中構建的服務器幫助我們將不在同一臺計算機上的兩個用戶連接在一起。服務器的目標是用通過網絡傳輸的信息機制替換信令機制。服務器簡單明瞭,僅支持最基本的WebRTC連接。
我們的實施必須響應並回答來自多個用戶的請求。它將通過在客戶端之間使用簡單的雙向消息傳遞給系統來實現此目的。它將允許一個用戶呼叫另一個用戶並在它們之間建立WebRTC
連接。一旦用戶呼叫另一個用戶,服務器將在兩個用戶之間傳遞offer,answer和ICE候選者。這將允許他們成功設置WebRTC連接。
上圖顯示了使用信令服務器建立連接時客戶端之間的消息流。每一方都將通過向服務器註冊自己開始。我們的登錄將只是向服務器發送一個基於字符串能唯一標識用戶的ID。一旦兩個用戶都註冊了服務器,他們就可以呼叫另一個用戶。使用他們希望調用的用戶標識符進行迴應即可,其他用戶也是依次回答。最後,候選人在客戶端之間發送,直到他們能夠成功建立連接。在任何時候,用戶都可以通過發送離開消息來終止連接。實現很簡單,主要用作用戶向對方發送消息的傳遞。
設置環境
我們將利用Node.js的強大功能來構建我們的服務器。如果您以前從未在Node.js中編程,請不要擔心!該技術利用JavaScript引擎完成所有工作。這意味着所有編程都將使用JavaScript,因此不需要學習新語言。現在,讓我們執行以下步驟來設置Node.js環境:
- 運行node.js服務器的第一步是安裝node.js.
-
現在,您可以打開終端應用程序並使用node命令啓動Node.js VM。Node.js基於Google Chrome附帶的V8 JavaScript引擎。這意味着它與瀏覽器解釋JavaScript的方式非常接近。鍵入一些命令以熟悉它的工作原理:
> 1 + 1 2 > var hello = "world"; undefined > "Hello" + hello; 'Helloworld'
- 從這裏開始,我們可以開始創建服務器程序。幸運的是,Node.js運行JavaScript文件和終端輸入命令是一樣的。用下列內容創建
index.js
,並且用node index.js
運行:console.log("Hello from node!");
當你執行
node index.js
命令後,將會在Node.js控制檯看到如下信息:Hello from node!
這是我們將在本書中介紹的Node.js概念的結束。我們對信號服務器的實現並不是最先進的,而深入研究服務器工程需要整整一本書的內容。隨着我們繼續前進,花些時間瞭解更多有關Node.js的信息,甚至我們將用自己喜歡的語言構建信令服務器!
獲取連接
創建WebRTC
連接所需的步驟必須是實時的。這意味着客戶端必須能夠在不使用WebRTC
對等連接的情況下實時地在彼此之間傳輸消息。這是我們將利用HTML5的另一個強大功能WebSockets
。
WebSocket
正是它聽起來的樣子 - 兩個端點之間的開放雙向套接字連接 - Web瀏覽器和Web服務器。您可以使用字符串和二進制信息在套接字上來回發送消息。它旨在Web瀏覽器和Web服務器中實現,以便在AJAX請求範圍之外實現它們之間的通信。
WebSocket
協議自2010年左右開始出現,是當今大多數瀏覽器都可以使用的定義明確的標準。它對Web客戶端提供廣泛的支持,許多服務器技術都有專門用於它們的框架。甚至整個框架都依賴於WebSocket
技術,例如Meteor JavaScript框架。
WebSocket
協議和WebRTC
協議之間的最大區別在於使用TCP堆棧。WebSockets
本質上是客戶端到服務器,並利用TCP傳輸實現可靠的連接。這意味着它有許多WebRTC
沒有的瓶頸,我們在第3章創建基本WebRTC應用程序中的"理解UDP傳輸和實時傳輸"一節中對此進行了描述。這也是它作爲信令傳輸協議很好地工作的原因。由於它是可靠的,我們的信號不太可能在用戶之間丟失,從而爲我們提供更成功的連接。它也內置在瀏覽器中,使用Node.js
可以輕鬆設置,這使我們的信令服務器的實現更容易理解。
要在我們的項目中利用WebSockets
的強大功能,我們必須首先爲Node.js安裝支持的WebSockets庫。我們將使用npm註冊表中的ws項目。要安裝庫,請進入到服務器的目錄並運行以下命令:
npm install ws
你會看到如下輸出:
現在我們安裝了websocket
庫,我們可以在服務器中開始使用,您可以在index.js
文件中插入以下代碼:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({ port: 8888 });
wss.on('connection', function (connection) {
console.log("User connected");
connection.on('message', function (message) {
console.log("Got message:", message);
});
connection.send('Hello World');
});
首先,我們需要引入我們在命令行安裝的ws包。之後,我們創建一個websocket
服務,告訴客戶端連接的端口,如果你想更改設置,你可以填寫任何你喜歡的端口。
接下來,我們監聽來自服務器的連接事件。只要用戶與服務器建立WebSocket
連接,就會調用此代碼。它將爲您提供一個連接對象,其中包含有關剛剛連接的用戶的各種信息。
然後,我們收聽用戶發送的任何消息。現在,我們只是將這些消息記錄到控制檯。
最後,當服務器完成與客戶端的WebSocket
連接時,服務器向客戶發送回覆Hello World
。
請注意,連接事件發生在連接到服務器的任何用戶。這意味着您可以讓多個用戶連接到同一服務器,每個用戶將單獨觸發連接事件。這種基於異步的代碼通常被視爲Node.js
編程的優勢之一。
現在我們可以通過運行node index.js
來運行我們的服務器。該過程開始並等待處理WebSocket
連接。它會無限期地執行此操作,直到您停止運行該進程。
測試服務
測試我們的代碼是否正常運行,我們可以使用ws庫附帶的wscat
命令。關於npm的好處是,您不僅可以安裝要在應用程序中使用的庫,還可以全局安裝庫以用作命令行工具。運行npm install -g ws
,運行此命令時可能需要使用管理員權限。
這應該給我們一個名爲wscat
的新命令。此工具允許我們從命令行直接連接到WebSocket
服務器,並針對它們測試命令。爲此,我們在一個終端窗口中運行我們的服務器,然後打開一個新服務器並運行wscat -c ws:// localhost:8888
命令。您會注意到ws://
,它是WebSocket
協議的自定義指令,而不是HTTP。
您的輸出應該類似於:
服務器端打印log如下:
如果其中任何一步都不起作用,那麼請根據列表檢查代碼並閱讀ws庫以及Node.js和npm的文檔。這些工具在不同環境中的工作方式可能不同,在某些情況下需要額外設置。如果一切正常,請在Node.js中編寫一個包含12行代碼的WebSocket服務器。
識別用戶
在典型的Web應用程序中,服務器需要一種方法來識別連接的客戶端。今天的大多數應用程序使用唯一身份規則,並讓每個用戶登錄到相應的基於字符串的標識符,稱爲用戶名。我們還將在信令應用程序中使用同樣的規則。它不會像今天使用的某些應用那樣複雜,因爲我們甚至不需要用戶輸入密碼。我們只需要爲每個連接提供一個ID,這樣我們就知道在哪裏發送消息。
首先,我們將稍微更改一下連接處理程序,看起來類似於:
connection.on('message', function (message) {
var data;
try {
data = JSON.parse(message);
} catch (e) {
console.log("Error parsing JSON");
data = {};
}
});
這會將我們的WebSocket
實現更改爲僅接受JSON
消息。
由於WebSocket
連接僅限於字符串和二進制數據,因此我們需要一種通過線路發送結構化數據的方法。JSON允許我們定義結構化數據,然後將其序列化爲可以通過WebSocket
連接發送的字符串。它也是在JavaScript中使用的最簡單的序列化形式。
接下來,我們需要一種方法來存儲所有已連接的用戶。由於我們的服務器本質上是簡單的,我們將使用JavaScript中已知的哈希映射作爲對象來存儲我們的數據。我們可以將文件的頂部更改爲與此類似:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({ port: 8888 });
users = {};
要登錄,我們需要知道用戶正在發送登錄類型消息。爲了支持這一點,我們將爲客戶端發送的每條消息添加一個類型字段。這將允許我們的服務器知道如何處理它正在接收的數據。
首先,我們將定義用戶嘗試登錄時要執行的操作:
connection.on('message', function (message) {
var data;
try {
data = JSON.parse(message);
} catch (e) {
console.log("Error parsing JSON");
data = {};
}
switch (data.type) {
case "login":
console.log("User logged in as", 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;
default:
sendTo(connection, {
type: "error",
message: "Unrecognized command: " + data.type
});
break;
}
});
我們使用switch語句來相應地處理每種消息類型。如果用戶發送帶有登錄類型的消息,我們首先需要查看是否有人已使用該ID登錄到服務器。如果有,我們告訴客戶他們沒有成功登錄並需要選擇一個新名稱。如果沒有人使用此ID,我們將連接添加到用戶對象中,ID爲密鑰。如果我們遇到任何我們無法識別的命令,我們還會向客戶端發送一條消息,說明處理他們的請求時出錯。
我還在代碼中添加了一個名爲sendTo
的輔助函數,用於處理向連接發送消息。這可以添加到文件中的任何位置:
function sendTo(conn, message) {
conn.send(JSON.stringify(message));
}
此函數的作用是確保我們的所有消息始終以JSON
格式編碼。這也有助於減少我們必須編寫的代碼量。將消息封裝成一個方法是好的做法,以便在多個地方同時調用。
我們要做的最後一件事是提供一種在斷開連接時清理客戶端連接的方法。幸運的是,我們的類庫在發生這種情況時會提供一個事件。我們可以通過這種方式收聽此活動並刪除我們的用戶:
connection.on('close', function () {
if (connection.name) {
delete users[connection.name];
}
});
這應該在連接事件中添加,就像消息處理程序一樣。
現在是時候用我們的login命令測試我們的服務器了。我們可以像以前一樣使用客戶端來測試我們的登錄命令。要記住的一件事是,我們現在發送的消息必須以JSON
格式編碼,以便服務器接受它們。
{ ""type"": ""login"", ""name"": ""Foo"" }
您收到的輸出應該類似於:
發送請求
從現在起,我們的代碼不會比登錄處理程序複雜得多。
我們將創建一組處理程序,以便爲每個步驟正確傳遞消息。登錄後進行的第一個調用是offer
處理程序,它指定一個用戶想要調用另一個用戶。
最好不要將這裏的發送請求與WebRTC的offer
步驟混淆。
在這個例子中,我們將兩者結合起來使我們的API
更易於使用。在大多數設置中,這些步驟將分開。這可以在諸如Skype之類的應用程序中看到,其中另一個用戶必須在兩個用戶之間建立連接之前接受來電。
我們現在可以將offer處理程序添加到此代碼中:
case "offer":
console.log("Sending offer to", data.name);
var conn = users[data.name];
if (conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
我們要做的第一件事是獲取我們試圖呼叫的用戶連接。這很容易做到,因爲其他用戶的ID始終是我們的連接存儲在用戶查找對象中的位置。然後我們檢查其他用戶是否存在,如果存在,則向他們發送要約的詳細信息。我們還在用戶的連接對象中添加了一個otherName
屬性,以便我們稍後可以在代碼中輕鬆查找。您可能還注意到,此代碼都不是特定於WebRTC
的。這可能涉及兩個用戶之間的任何類型的呼叫技術。我們將在本章後面詳細介紹這一點。
您可能還注意到這裏缺少錯誤處理。這可能是WebRTC
最繁瑣的部分之一。由於呼叫在進程的任何一點都可能失敗,因此我們有很多地方有可能使連接失敗。它也可能由於各種原因而失敗,例如網絡可用性,防火牆等。在本書中,我們將其留給用戶以他們想要的方式單獨處理每個錯誤情況。
迴應請求
迴應請求就像offer
一樣容易。我們遵循類似的模式,讓客戶完成大部分工作。我們的服務器會讓任何消息通過,作爲對其他用戶的回答。我們可以在offer
處理案例之後添加:
case "answer":
console.log("Sending answer to", data.name);
var conn = users[data.name];
if (conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
您可以看到代碼在前面的列表中看起來有很多相似。注意,我們也依賴於來自其他用戶的答案。如果用戶首先發送答案而不是提議,則可能會破壞我們的服務器實施。有許多用例,這個服務器不夠用,但在下一章中它將很好地用於集成。
這應該是WebRTC
中offer
和answer
機制的良好開端。
您應該看到它遵循RTCPeerConnection
上的createOffer
和createAnswer
函數。這正是我們開始插入服務器連接以處理遠程客戶端的地方。
我們甚至可以使用之前使用的WebSocket
客戶端測試,同時連接兩個客戶端允許我們在兩者之間發送請求和響應。這可以讓您更深入地瞭解這最終將如何運作。您可以在終端窗口中看到同時運行兩個客戶端的結果,如以下屏幕截圖所示:
就我而言,我的offer
和answer
都是簡單的字符串消息。如果您還記得第3章,創建基本WebRTC
應用程序,請參閱WebRTC API
部分,我們詳細介紹了會話描述協議(SDP
)。這是在進行WebRTC
調用時實際應用的offer
和answer
字符串。如果您不記得SDP是什麼,請參閱第3章,回憶一下,創建基本WebRTC
應用程序中的WebRTC API
部分下的會話描述協議部分。