音视频流媒体开发【七十八】- WebRTC5-Nodejs实战

音视频流媒体开发-目录
iOS知识点-目录
Android-目录
Flutter-目录
数据结构与算法-目录
uni-pp-目录

5. Nodejs实战

对于我们WebRTC项目而言,nodejs主要是实现信令服务器的功能,客户端和服务器端的交互我们选择websocket作为通信协议,所以该章节的实战以websocket的使用为主。

web客户端的websocket和nodejs服务器端的websocket有一定的差别,所以我们分开两部分进行讲解。

5.1 web客户端 websocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocketAPI 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过TCP 连接直接交换数据。

当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

以下 API 用于创建 WebSocket 对象。

var Socket = new WebSocket(url, [protocol] );

以上代码中的第一个参数 url, 指定连接的 URL。第二个参数 protocol 是可选的,指定了可接受的子协议。

WebSocket 属性

以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:

image.png
WebSocket 事件

以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:

image.png
WebSocket 方法

以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:

为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息"Upgrade: WebSocket"表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

5.2 Nodejs服务器 websocket

Nodejs教程

简单的说 Node.js 就是运行在服务端的 JavaScript。

服务器端使用websocket需要安装nodejs­websocket

cd 工程目录
# 此刻我们需要执行命令:
sudo npm init
#创建package.json文件,系统会提示相关配置,也可以使用命令:
sudo npm init ‐y
sudo npm install nodejs‐websocket

官方参考

我们只要关注:
(1)如何创建websocket服务器,通过createServer和listen接口;
(2)如何判断有新的连接进来,createServer的回调函数判断;
(3)如何判断关闭事件,通过on("close", callback) 事件的回调函数;
(4)如何判断接收到数据,通过on("text", callkback)事件的回调函数;
(5)如何判断接收异常,通过on("error", callkback)事件的回调函数;
(6)如何主动发送数据,调用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) { // 收到数据的响应
    console.log("Received "+str)
    conn.sendText(str.toUpperCase()+"!!!") // 发送
  })
  conn.on("close", function (code, reason) { // 关闭时的响应
    console.log("Connection closed")
  })
  conn.on("error", function (err) { // 出错
    console.log("error:" + err);
  });
}).listen(8001)

5.3 websocket聊天室实战

效果展示+框架分析
效果展示
客服端

框架分析

消息类型分为三种:

  1. enter:新人进入 (蓝色字体显示)
  2. message:普通聊天信息 (黑色字体显示)
  3. leave:有人离开 (红色字体显示)

服务器在收到某个客户端的消息(message+enter+leave),然后将其广播给所有的客户端(包括发送者)。

客户端代码

目录和文件名:05/5.3/client.html

<html>

<body>
  <h1>Websocket简易聊天</h1>
  <div id="app">
    <input id="sendMsg" type="text" />
    <button id="submitBtn">发送</button>
  </div>
</body>

<script type="text/javascript">
  //在页面显示聊天内容
  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);
}

//新建一个websocket
var websocket = new WebSocket("[ws://192.168.221.132:8010");](ws://192.168.221.132:8010/)
//打开websocket连接
websocket.onopen = function () {
  console.log("已经连上服务器‐‐‐‐");
  document.getElementById("submitBtn").onclick = function () {
     var txt = document.getElementById("sendMsg").value;
     if (txt) {
      //向服务器发送数据
      websocket.send(txt);
    }
  };
};

//关闭连接
websocket.onclose = function () {
  console.log("websocket close");
};

//接收服务器返回的数据
websocket.onmessage = function (e) {
  var mes = JSON.parse(e.data); // json格式
  showMessage(mes.data, mes.type);
};
</script>

</html>
服务器端代码

目录和文件名:05/5.3/server.js

var ws = require("nodejs‐websocket")
var port = 8010;
var user = 0;

// 创建一个连接
var server = ws.createServer(function (conn) {
  console.log("创建一个新的连接‐‐‐‐‐‐‐‐");
  user++;
  conn.nickname="user" + user;
  conn.fd="user" + user;
  var mes = {};
  mes.type = "enter";
  mes.data = conn.nickname + " 进来啦"
  broadcast(JSON.stringify(mes)); // 广播

  //向客户端推送消息
  conn.on("text", function (str) {
    console.log("回复 "+str)
    mes.type = "message";
    mes.data = conn.nickname + " 说: " + str;
    broadcast(JSON.stringify(mes));
  });

  //监听关闭连接操作
  conn.on("close", function (code, reason) {
    console.log("关闭连接");
    mes.type = "leave";
    mes.data = conn.nickname+" 离开了"
    broadcast(JSON.stringify(mes));
  });

  //错误处理
  conn.on("error", function (err) {
    console.log("监听到错误");
    console.log(err);
  });
}).listen(port);

function broadcast(str){
  server.connections.forEach(function(connection){
    connection.sendText(str);
  })
}

5.4 Map实战

因为信令服务器使用map管理房间,所以我们先做个小练习。

主要涉及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;
    }
  };
  // 根据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内key‐value的数量
  this.size = function () {
    return this._entrys.length;
  };

  // 获取所有的key
  this.getEntrys = function () {
    return this._entrys;
  };

  // 内部函数
  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对应的websocket连接
  this.roomId = roomId;
  console.log('uid:' + uid +', conn:' + conn + ', roomId: ' + roomId);
}

var roomMap = new ZeroRTCMap();

// Math.random() 返回介于 0(包含) ~ 1(不包含) 之间的一个随机数:
// toString(36)代表36进制,其他一些也可以,比如toString(2)、toString(8),代表输出为二进制和八进制。最高支持几进制
// 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());
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容