音視頻流媒體開發(fā)-目錄
iOS知識(shí)點(diǎn)-目錄
Android-目錄
Flutter-目錄
數(shù)據(jù)結(jié)構(gòu)與算法-目錄
uni-pp-目錄
5. Nodejs實(shí)戰(zhàn)
對(duì)于我們WebRTC項(xiàng)目而言放仗,nodejs主要是實(shí)現(xiàn)信令服務(wù)器的功能赃蛛,客戶端和服務(wù)器端的交互我們選擇websocket作為通信協(xié)議,所以該章節(jié)的實(shí)戰(zhàn)以websocket的使用為主载城。
web客戶端的websocket和nodejs服務(wù)器端的websocket有一定的差別,所以我們分開兩部分進(jìn)行講解权埠。
5.1 web客戶端 websocket
WebSocket 是 HTML5 開始提供的一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議沈贝。
WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡(jiǎn)單眶蕉,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)填帽。在 WebSocketAPI 中蛛淋,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接盲赊,并進(jìn)行雙向數(shù)據(jù)傳輸铣鹏。
在 WebSocket API 中,瀏覽器和服務(wù)器只需要做一個(gè)握手的動(dòng)作哀蘑,然后诚卸,瀏覽器和服務(wù)器之間就形成了一條快速通道。兩者之間就直接可以數(shù)據(jù)互相傳送绘迁。
現(xiàn)在合溺,很多網(wǎng)站為了實(shí)現(xiàn)推送技術(shù),所用的技術(shù)都是 Ajax 輪詢缀台。輪詢是在特定的的時(shí)間間隔(如每1秒)棠赛,由瀏覽器對(duì)服務(wù)器發(fā)出HTTP請(qǐng)求,然后由服務(wù)器返回最新的數(shù)據(jù)給客戶端的瀏覽器膛腐。這種傳統(tǒng)的模式帶來很明顯的缺點(diǎn)睛约,即瀏覽器需要不斷的向服務(wù)器發(fā)出請(qǐng)求,然而HTTP請(qǐng)求可能包含較長(zhǎng)的頭部哲身,其中真正有效的數(shù)據(jù)可能只是很小的一部分辩涝,顯然這樣會(huì)浪費(fèi)很多的帶寬等資源。
HTML5 定義的 WebSocket 協(xié)議勘天,能更好的節(jié)省服務(wù)器資源和帶寬怔揩,并且能夠更實(shí)時(shí)地進(jìn)行通訊。
瀏覽器通過 JavaScript 向服務(wù)器發(fā)出建立 WebSocket 連接的請(qǐng)求脯丝,連接建立以后商膊,客戶端和服務(wù)器端就可以通過TCP 連接直接交換數(shù)據(jù)。
當(dāng)你獲取 Web Socket 連接后宠进,你可以通過 send() 方法來向服務(wù)器發(fā)送數(shù)據(jù)晕拆,并通過 onmessage 事件來接收服務(wù)器返回的數(shù)據(jù)。
以下 API 用于創(chuàng)建 WebSocket 對(duì)象砰苍。
var Socket = new WebSocket(url, [protocol] );
以上代碼中的第一個(gè)參數(shù) url, 指定連接的 URL潦匈。第二個(gè)參數(shù) protocol 是可選的,指定了可接受的子協(xié)議赚导。
WebSocket 屬性
以下是 WebSocket 對(duì)象的屬性。假定我們使用了以上代碼創(chuàng)建了 Socket 對(duì)象:
WebSocket 事件
以下是 WebSocket 對(duì)象的相關(guān)事件赤惊。假定我們使用了以上代碼創(chuàng)建了 Socket 對(duì)象:
WebSocket 方法
以下是 WebSocket 對(duì)象的相關(guān)方法吼旧。假定我們使用了以上代碼創(chuàng)建了 Socket 對(duì)象:
為了建立一個(gè) WebSocket 連接,客戶端瀏覽器首先要向服務(wù)器發(fā)起一個(gè) HTTP 請(qǐng)求未舟,這個(gè)請(qǐng)求和通常的 HTTP 請(qǐng)求不同圈暗,包含了一些附加頭信息掂为,其中附加頭信息"Upgrade: WebSocket"表明這是一個(gè)申請(qǐng)協(xié)議升級(jí)的 HTTP 請(qǐng)求,服務(wù)器端解析這些附加的頭信息然后產(chǎn)生應(yīng)答信息返回給客戶端员串,客戶端和服務(wù)器端的 WebSocket 連接就建立起來了勇哗,雙方就可以通過這個(gè)連接通道自由的傳遞信息,并且這個(gè)連接會(huì)持續(xù)存在直到客戶端或者服務(wù)器端的某一方主動(dòng)的關(guān)閉連接寸齐。
5.2 Nodejs服務(wù)器 websocket
簡(jiǎn)單的說 Node.js 就是運(yùn)行在服務(wù)端的 JavaScript欲诺。
服務(wù)器端使用websocket需要安裝nodejs-websocket
cd 工程目錄
# 此刻我們需要執(zhí)行命令:
sudo npm init
#創(chuàng)建package.json文件,系統(tǒng)會(huì)提示相關(guān)配置渺鹦,也可以使用命令:
sudo npm init ‐y
sudo npm install nodejs‐websocket
我們只要關(guān)注:
(1)如何創(chuàng)建websocket服務(wù)器扰法,通過createServer和listen接口;
(2)如何判斷有新的連接進(jìn)來毅厚,createServer的回調(diào)函數(shù)判斷塞颁;
(3)如何判斷關(guān)閉事件,通過on("close", callback) 事件的回調(diào)函數(shù)吸耿;
(4)如何判斷接收到數(shù)據(jù)祠锣,通過on("text", callkback)事件的回調(diào)函數(shù);
(5)如何判斷接收異常咽安,通過on("error", callkback)事件的回調(diào)函數(shù)伴网;
(6)如何主動(dòng)發(fā)送數(shù)據(jù),調(diào)用sendText
參考代碼
var ws = require("nodejs‐websocket")
// Scream server example: "hi" ‐> "HI!!!"
var server = ws.createServer(function (conn) {
console.log("New connection")
conn.on("text", function (str) { // 收到數(shù)據(jù)的響應(yīng)
console.log("Received "+str)
conn.sendText(str.toUpperCase()+"!!!") // 發(fā)送
})
conn.on("close", function (code, reason) { // 關(guān)閉時(shí)的響應(yīng)
console.log("Connection closed")
})
conn.on("error", function (err) { // 出錯(cuò)
console.log("error:" + err);
});
}).listen(8001)
5.3 websocket聊天室實(shí)戰(zhàn)
效果展示+框架分析
效果展示
客服端
框架分析
消息類型分為三種:
- enter:新人進(jìn)入 (藍(lán)色字體顯示)
- message:普通聊天信息 (黑色字體顯示)
- leave:有人離開 (紅色字體顯示)
服務(wù)器在收到某個(gè)客戶端的消息(message+enter+leave)板乙,然后將其廣播給所有的客戶端(包括發(fā)送者)是偷。
客戶端代碼
目錄和文件名:05/5.3/client.html
<html>
<body>
<h1>Websocket簡(jiǎn)易聊天</h1>
<div id="app">
<input id="sendMsg" type="text" />
<button id="submitBtn">發(fā)送</button>
</div>
</body>
<script type="text/javascript">
//在頁面顯示聊天內(nèi)容
function showMessage(str, type) {
var div = document.createElement("div");
div.innerHTML = str;
if (type == "enter") {
div.style.color = "blue";
} else if (type == "leave") {
div.style.color = "red";
}
document.body.appendChild(div);
}
//新建一個(gè)websocket
var websocket = new WebSocket("[ws://192.168.221.132:8010");](ws://192.168.221.132:8010/)
//打開websocket連接
websocket.onopen = function () {
console.log("已經(jīng)連上服務(wù)器‐‐‐‐");
document.getElementById("submitBtn").onclick = function () {
var txt = document.getElementById("sendMsg").value;
if (txt) {
//向服務(wù)器發(fā)送數(shù)據(jù)
websocket.send(txt);
}
};
};
//關(guān)閉連接
websocket.onclose = function () {
console.log("websocket close");
};
//接收服務(wù)器返回的數(shù)據(jù)
websocket.onmessage = function (e) {
var mes = JSON.parse(e.data); // json格式
showMessage(mes.data, mes.type);
};
</script>
</html>
服務(wù)器端代碼
目錄和文件名:05/5.3/server.js
var ws = require("nodejs‐websocket")
var port = 8010;
var user = 0;
// 創(chuàng)建一個(gè)連接
var server = ws.createServer(function (conn) {
console.log("創(chuàng)建一個(gè)新的連接‐‐‐‐‐‐‐‐");
user++;
conn.nickname="user" + user;
conn.fd="user" + user;
var mes = {};
mes.type = "enter";
mes.data = conn.nickname + " 進(jìn)來啦"
broadcast(JSON.stringify(mes)); // 廣播
//向客戶端推送消息
conn.on("text", function (str) {
console.log("回復(fù) "+str)
mes.type = "message";
mes.data = conn.nickname + " 說: " + str;
broadcast(JSON.stringify(mes));
});
//監(jiān)聽關(guān)閉連接操作
conn.on("close", function (code, reason) {
console.log("關(guān)閉連接");
mes.type = "leave";
mes.data = conn.nickname+" 離開了"
broadcast(JSON.stringify(mes));
});
//錯(cuò)誤處理
conn.on("error", function (err) {
console.log("監(jiān)聽到錯(cuò)誤");
console.log(err);
});
}).listen(port);
function broadcast(str){
server.connections.forEach(function(connection){
connection.sendText(str);
})
}
5.4 Map實(shí)戰(zhàn)
因?yàn)樾帕罘?wù)器使用map管理房間,所以我們先做個(gè)小練習(xí)募逞。
主要涉及put/get/remove/size等操作蛋铆。
目錄和文件名:05/5.4/map.js
/** ‐‐‐‐‐ ZeroRTCMap ‐‐‐‐‐ */
var ZeroRTCMap = function () {
this._entrys = new Array();
// 插入
this.put = function (key, value) {
if (key == null || key == undefined) {
return;
}
var index = this._getIndex(key);
if (index == ‐1) {
var entry = new Object();
entry.key = key;
entry.value = value;
this._entrys[this._entrys.length] = entry;
} else {
this._entrys[index].value = value;
}
};
// 根據(jù)key獲取value
this.get = function (key) {
var index = this._getIndex(key);
return (index != ‐1) ? this._entrys[index].value : null;
};
// 移除key‐value
this.remove = function (key) {
var index = this._getIndex(key);
if (index != ‐1) {
this._entrys.splice(index, 1);
}
};
// 清空map
this.clear = function () {
this._entrys.length = 0;
};
// 判斷是否包含key
this.contains = function (key) {
var index = this._getIndex(key);
return (index != ‐1) ? true : false;
};
// map內(nèi)key‐value的數(shù)量
this.size = function () {
return this._entrys.length;
};
// 獲取所有的key
this.getEntrys = function () {
return this._entrys;
};
// 內(nèi)部函數(shù)
this._getIndex = function (key) {
if (key == null || key == undefined) {
return ‐1;
}
var _length = this._entrys.length;
for (var i = 0; i < _length; i++) {
var entry = this._entrys[i];
if (entry == null || entry == undefined) {
continue;
}
if (entry.key === key) {// equal
return i;
}
}
return ‐1;
};
}
function Client(uid, conn, roomId) {
this.uid = uid; // 用戶所屬的id
this.conn = conn; // uid對(duì)應(yīng)的websocket連接
this.roomId = roomId;
console.log('uid:' + uid +', conn:' + conn + ', roomId: ' + roomId);
}
var roomMap = new ZeroRTCMap();
// Math.random() 返回介于 0(包含) ~ 1(不包含) 之間的一個(gè)隨機(jī)數(shù):
// toString(36)代表36進(jìn)制,其他一些也可以放接,比如toString(2)刺啦、toString(8),代表輸出為二進(jìn)制和八進(jìn)制纠脾。最高支持幾進(jìn)制
// substr(2) 舍去0/1位置的字符
console.log('\n\n‐‐‐‐‐‐‐‐‐‐Math.random() ‐‐‐‐‐‐‐‐‐‐');
var randmo = Math.random();
console.log('Math.random() = ' + randmo);
console.log('Math.random().toString(10) = ' + randmo.toString(10));
console.log('Math.random().toString(36) = ' + randmo.toString(36));
console.log('Math.random().toString(36).substr(0) = ' + randmo.toString(36).substr(0));
console.log('Math.random().toString(36).substr(1) = ' + randmo.toString(36).substr(1));
console.log('Math.random().toString(36).substr(2) = ' + randmo.toString(36).substr(2));
console.log('\n\n‐‐‐‐‐‐‐‐‐‐create client ‐‐‐‐‐‐‐‐‐‐');
var roomId = 100;
var uid1 = Math.random().toString(36).substr(2);
var conn1 = 100;
var client1 = new Client(uid1, conn1, roomId);
var uid2 = Math.random().toString(36).substr(2);
var conn2 = 101;
var client2 = new Client(uid2, conn2, roomId);
// 插入put
console.log('\n\n‐‐‐‐‐‐‐‐‐‐‐‐‐‐put‐‐‐‐‐‐‐‐‐‐‐‐‐‐');
console.log('roomMap put client1');
roomMap.put(uid1, client1);
console.log('roomMap put client2');
roomMap.put(uid2, client2);
console.log('roomMap size:' + roomMap.size());
// 獲取get
console.log('\n\n‐‐‐‐‐‐‐‐‐‐‐‐‐‐get‐‐‐‐‐‐‐‐‐‐‐‐‐‐');
var client = null;
var uid = uid1;
client = roomMap.get(uid);
if(client != null) {
console.log('get client‐>' + 'uid:' + client.uid +', conn:' + client.conn + ', roomId: '+ client.roomId);
} else {
console.log("can't find the client of " + uid);
}
uid = '123345';
client = roomMap.get(uid);
if(client != null) {
console.log('get client‐>' + 'uid:' + client.uid +', conn:' + client.conn + ', roomId: '+ client.roomId);
} else {
console.log("can't find the client of " + uid);
}
console.log('\n\n‐‐‐‐‐‐‐‐‐‐‐‐‐‐traverse‐‐‐‐‐‐‐‐‐‐‐‐‐‐');
// 遍歷map
var clients = roomMap.getEntrys();
for (var i in clients) {
let uid = clients[i].key;
let client = roomMap.get(uid);
console.log('get client‐>' + 'uid:' + client.uid +', conn:' + client.conn + ', roomId: '+ client.roomId);
}
console.log('\n\n‐‐‐‐‐‐‐‐‐‐‐‐‐‐remove‐‐‐‐‐‐‐‐‐‐‐‐‐‐');
console.log('roomMap remove uid1');
roomMap.remove(uid1);
console.log('roomMap size:' + roomMap.size());
console.log('\n\n‐‐‐‐‐‐‐‐‐‐‐‐‐‐clear‐‐‐‐‐‐‐‐‐‐‐‐‐‐');
console.log('roomMap clear all');
roomMap.clear();
console.log('roomMap size:' + roomMap.size());