作者:李超,文章首發於 RTC 開發者社群,如遇到相關問題,可以點選這裡與作者直接交流。
前言
我們在學習 WebRTC 時,首先要把實驗環境搭建好,這樣我們就可以在上面做各種實驗了。
對於 WebRTC 來說,它有一整套規範,如怎樣使用它的介面、使用SDP進行媒體協商、通過ICE收集地址並進行連通性檢測等等。除此之外,WebRTC還需要房間伺服器將多端聚集到一起管理,以及信令伺服器進行信令資料交換(如媒體描述資訊SDP的交換,連線地址的交抽換等),但在WebRTC的規範中沒有對這部分內容進行規定,所以需要由使用者自己處理。
你可以根據自己的喜好選擇伺服器(如 Apache,Nginx 或 Nodejs),我今天將介紹如何使用 Nodejs 來搭建信令伺服器。
為什麼選擇 Nodejs
Apache、Nginx和Nodejs都是非常成熟的Web伺服器,Nginx 可以說是的效能是最好的Web伺服器了。但從未來的發展來說,Nodejs可能會更有優勢。
現在以Chrome為代表的瀏覽器的功能越來越強大,以前認為通過瀏覽器不可能完成的事兒,現在它都可以輕鬆實現。H5、 WebSocket的出現以及現在WebRTC的加入,讓大家越來越覺得以後的瀏覽器可以說是“無所不能”。因此,推動 JavaScript 語言的發展越來越迅速。這可以從現在 JavaScript 技術的火爆,以及各種層疊不窮JS FrameWork的出現得以印證。
而 Nodejs 的最大優點即是可以使用 JS 語言開發伺服器程式。這樣使得大量的前端同學可以無縫的轉到伺服器開發,甚至有可能前後端使用同一套程式碼實現。對於這一點我想無論是對個人還是對於企業都是具大的誘惑。
-
一方面 JS 語言的簡單性可以方便開發出各種各樣功能的服務端程式。
-
更可貴的是 Nodejs 的生態鏈非常的完整,有各種各樣的功能庫。你可以根據自己的需要通過安裝工具 NPM 快速的安裝,這也使它也得到了廣大開發者的喜歡。
Nodejs 現在是非常流行的 Web 伺服器,它在伺服器端使用 V8(JavaScript)引擎,通過它解析 JS 指令碼來控制伺服器的行為。這對於廣大的 JS 同學來說真是太幸福了,在10年前還很難想像可以通過 JS 指令碼語言來寫伺服器程式。
當然,如果你想對Nodejs作能力擴充的話,還是要寫C/C++庫,然後載入到 Nodejs 中去。
Nodejs的基本原理
Nodejs的工作原理如上圖所示, 其核心是 V8 引擎。通過該引擎,可以讓 js 呼叫 C/C++方法 或 物件。相反,通過它也可能讓 C/C++ 訪問 javascript 方法和變數。Nodejs 首先將 JavaScript 寫好的應用程式交給 V8 引擎進行解析,V8理解應用程式的語義後,再呼叫 Nodejs 底層的 C/C++ API將服務啟動起來。 所以 Nodejs 的強大就在於 js 可以直接呼叫 C/C++ 的方法,使其能力可以無限擴充套件。
以開發一個 HTTP 服務為例,Nodejs 開啟偵聽的服務埠後,底層會呼叫 libuv 處理該埠的所有 http 請求。其網路事件處理如下圖所示:
當有網路請求過來時,首先會被插入到一個事件處理佇列中。libuv會監控該事件佇列,當發現有事件時,先對請求做判斷,如果是簡單的請求,就直接返回響應了;如果是複雜請求,則從執行緒池中取一個執行緒進行非同步處理;執行緒處理完後,有兩種可能:一種是已經處理完成,則向使用者傳送響應;另一種情況是還需要進一步處理,則再生成一個事件插入到事件佇列中等待處理;事件處理就這樣迴圈往復下去,永不停歇。
兩個 V8 引擎
如上圖所示,在我們使用 Nodejs之後實際存在了兩個 V8 引擎。一個V8用於解析服務端的 JS 應用程式,它將服務啟動起來。另一個 V8 是瀏覽器中的 V8 引擎,用於控制瀏覽器的行為。對於使用 Nodejs 的新手來說,很容易出現思維混亂,因為在服務端至少要放兩個 JS 指令碼。其中一個是服務端程式,控制 Nodejs 的行為,它由 Nodejs 的V8引擎解析處理;另一個是客戶端程式,它是要由瀏覽器請求後,下發到瀏覽器,由瀏覽器中的 V8 引擎進行解析處理。如果分不清這個,那麻煩就大了。
安裝 Nodejs
下面我們就來看看具體如何安裝 Nodejs。
安裝 Nodejs 非常的簡單:
在Ubuntu系統下執行:
apt install nodejs
複製程式碼
或在Mac 系統下執行:
brew install nodejs
複製程式碼
通過上面的步驟我們就將 Nodejs 安裝好了。我這裡安裝的 Nodejs版本為:v8.10.0。
安裝NPM
除了安裝 Nodejs 之外,我們還要安裝NPM(Node Package Manager),也就是 Nodejs 的包管理器。它就像Ubuntu下的 apt 或Mac 系統下的brew 命令類似,是專門用來管理各種依賴庫的。
在它們沒有出現之前,我們要安裝個包特別麻煩。以Linux為例,假設要安裝一個工具,其基本步驟是:
- 先將這個工具的原始碼下載下來。
- 執行./configure 生成Makefile 檔案。
- 執行 make 命令對其進行編譯。
- 最後,執行 make install 將其安裝到指定目錄下。
- 如果編譯過程中發現有依賴的庫,則要對依賴庫執行前面的4步,也就是先將依賴庫安裝好,然後再來安裝該工具。
大家可以看到,以前在Linux下安裝個程式或工具是多麼的麻煩。
Linux 有了apt 之後,一切都變得簡單了。我們只要執行 apt install xxx 一條命令就好了,它會幫你完成上面的一堆操作。
對於 Nodejs的安裝包也是如此,NPM 就是相當於 Linux 下的 apt,它的出現大大提高了人們的工作效率。
NPM 的安裝像安裝 Nodejs 一樣簡單:
在Ubuntu下執行:
apt install npm
複製程式碼
或在Mac下執行:
brew install npm
複製程式碼
socket.io
此次,我們使用 Nodejs 下的 socket.io 庫來實現 WebRTC 信令伺服器。socket.io特別適合用來開發WebRTC的信令伺服器,通過它來構建信令伺服器特別的簡單,這主要是因為它內建了房間 的概念。
上圖是 socket.io 與 Nodejs配合使用的邏輯關係圖, 其邏輯非常簡單。socket.io 分為服務端和客戶端兩部分。服務端由 Nodejs載入後偵聽某個服務埠,客戶端要想與服務端相連,首先要載入 socket.io 的客戶端庫,然後呼叫io.connect();
就與服務端連上了。
需要特別強調的是 socket.io 訊息的傳送與接收。socket.io 有很多種傳送訊息的方式,其中最常見的有下面幾種,是我們必須要撐握的:
-
給本次連線發訊息
socket.emit() 複製程式碼
-
給某個房間內所有人發訊息
io.in(room).emit() 複製程式碼
-
除本連線外,給某個房間內所有人發訊息
socket.to(room).emit() 複製程式碼
-
除本連線外,給所以人發訊息
socket.broadcast.emit() 複製程式碼
訊息又該如何接收呢?
-
傳送 command 命令
S: socket.emit('cmd’); C: socket.on('cmd',function(){...}); 複製程式碼
-
送了一個 command 命令,帶 data 資料
S: socket.emit('action', data); C: socket.on('action',function(data){...}); 複製程式碼
-
傳送了command命令,還有兩個資料
S: socket.emit(action,arg1,arg2); C: socket.on('action',function(arg1,arg2){...}); 複製程式碼
有了以上這些知識,我們就可以實現信令資料通訊了。
搭建信令伺服器
接下來我們來看一下,如何通過 Nodejs下的 socket.io 來構建的一個伺服器:
這是客戶端程式碼,也就是在瀏覽器裡執行的程式碼。index.html:
<!DOCTYPE html>
<html>
<head>
<title>WebRTC client</title>
</head>
<body>
<script src='/socket.io/socket.io.js'></script>
<script src='js/client.js'></script>
</body>
</html>
複製程式碼
該程式碼十分簡單,就是在body裡引入了兩段 JS 程式碼。其中,socket.io.js 是用來與服務端建立 socket 連線的。client.js 的作用是做一些業務邏輯,並最終通過 socket 與服務端通訊。
首先,在server.js
目錄下建立 js
子目錄,然後在 js目錄下生成 client.js。
下面是client.js的程式碼:
var isInitiator;
room = prompt('Enter room name:'); //彈出一個輸入視窗
const socket = io.connect(); //與服務端建立socket連線
if (room !== '') { //如果房間不空,則傳送 "create or join" 訊息
console.log('Joining room ' + room);
socket.emit('create or join', room);
}
socket.on('full', (room) => { //如果從服務端收到 "full" 訊息
console.log('Room ' + room + ' is full');
});
socket.on('empty', (room) => { //如果從服務端收到 "empty" 訊息
isInitiator = true;
console.log('Room ' + room + ' is empty');
});
socket.on('join', (room) => { //如果從服務端收到 “join" 訊息
console.log('Making request to join room ' + room);
console.log('You are the initiator!');
});
socket.on('log', (array) => {
console.log.apply(console, array);
});
複製程式碼
在該程式碼中:
- 首先彈出一個輸入框,要求使用者寫入要加入的房間。
- 然後,通過 io.connect() 建立與服務端的連線,
- 根據socket返回的訊息做不同的處理:
- 當收到房間滿"full"時的情況;
- 當收到房間空“empty"時的情況;
- 當收到加入“join"時的情況;
以上是客戶端(也就是在瀏覽器)中執行的程式碼。下面我們來看一下服務端的處理邏輯:
伺服器端程式碼,server.js:
const static = require('node-static');
const http = require('http');
const file = new(static.Server)();
const app = http.createServer(function (req, res) {
file.serve(req, res);
}).listen(2013);
const io = require('socket.io').listen(app); //偵聽 2013
io.sockets.on('connection', (socket) => {
// convenience function to log server messages to the client
function log(){
const array = ['>>> Message from server: '];
for (var i = 0; i < arguments.length; i++) {
array.push(arguments[i]);
}
socket.emit('log', array);
}
socket.on('message', (message) => { //收到message時,進行廣播
log('Got message:', message);
// for a real app, would be room only (not broadcast)
socket.broadcast.emit('message', message); //在真實的應用中,應該只在房間內廣播
});
socket.on('create or join', (room) => { //收到 “create or join” 訊息
var clientsInRoom = io.sockets.adapter.rooms[room];
var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; //房間裡的人數
log('Room ' + room + ' has ' + numClients + ' client(s)');
log('Request to create or join room ' + room);
if (numClients === 0){ //如果房間裡沒人
socket.join(room);
socket.emit('created', room); //傳送 "created" 訊息
} else if (numClients === 1) { //如果房間裡有一個人
io.sockets.in(room).emit('join', room);
socket.join(room);
socket.emit('joined', room); //傳送 “joined”訊息
} else { // max two clients
socket.emit('full', room); //傳送 "full" 訊息
}
socket.emit('emit(): client ' + socket.id +
' joined room ' + room);
socket.broadcast.emit('broadcast(): client ' + socket.id +
' joined room ' + room);
});
});
複製程式碼
在服務端引入了 node-static 庫,使伺服器具有釋出靜態檔案的功能。伺服器具有此功能後,當客戶端(瀏覽器)向服務端發起請求時,伺服器通過該模組獲得客戶端(瀏覽器)執行的程式碼,也就是上我面我們講到的 index.html 和 client.js 並下發給客戶端(瀏覽器)。
服務端偵聽 2013 這個埠,對不同的訊息做相應的處理:
- 伺服器收到 message 訊息時,它會直接進行廣播,所有連線到該伺服器的客戶端都會收收廣播的訊息。
- 服務端收到 “create or join”訊息時,它會對房間裡有人數進行統計,如果房間裡沒有人,則傳送"created" 訊息;如果房間裡有一個人,傳送"join"訊息和“joined"訊息;如果超過兩個人,傳送"full"訊息。
要執行該程式,需要使用 NPM 安裝 socket.io 和 node-static,安裝方法如下:
進入到 server.js
所在的目錄,然後執行下面的命令。
npm install socket.io
npm install node-static
複製程式碼
啟動伺服器並測試
通過上面的步驟我們就使用 socket.io 構建好一個伺服器,現在可以通過下面的命令將服務啟動起來了:
node server.js
複製程式碼
如果你是在本機上搭建的服務,則可以在瀏覽器中輸入 localhost:2013 ,然後新建一個tab 在裡邊再次輸入localhost:2013 。此時,開啟控制檯看看發生了什麼?
在Chrome下你可以使用快捷鍵 Command-Option-J或Ctrl-Shift-J的DevTools訪問控制檯。
小結
以上我向大家介紹了 Nodejs 的工作原理、Nodejs的安裝與佈署,以及如何使用 要sokcet.io 構建 WebRTC 信令訊息伺服器。socket.io 由於有房間的概念所以與WebRTC非常匹配,用它開發WebRTC信令伺服器非常方便。
另外,在本文中的例子只是一個簡單例子並沒有太多的實際價值。在後面的文章中我會以這個例子為基礎,在其上面不斷增加一些功能,最終你會看到一個完整的Demo程式。