详解WebRTC——网页实时通信技术

| 导语 WebRTC相关技术有很多可以创新的点,比如业界已有创业团队在做Web P2P,核心技术就是WebRTC + DASH协议,共享空闲资源,基于此可以做雾CDN,节点都在用户侧,去中心化

详解WebRTC——网页实时通信技术

简介

全称是: Web browser Real Time Communication

特点如下:

  • 是基于浏览器的实时音视频(数据)通信技术
  • 免插件
  • 开源
  • 已被W3C纳入HTML5标准
  • 跨平台,跨浏览器,跨移动应用
  • Mac OSX、Windows、iOS、Android、Linux

应用场景

适用于网页间音视频实时通信,点对点数据共享,QQ、腾讯视频已有应用

优势

  1. 方便。对于用户来说,在WebRTC出现之前想要进行实时通信就需要安装插件和客户端,但是对于很多用户来说,插件的下载、软件的安装和更新这些操作是复杂而且容易出现问题的,现在WebRTC技术内置于浏览器中,用户不需要使用任何插件或者软件就能通过浏览器来实现实时通信。
  2. 免费。虽然WebRTC技术已经较为成熟,其集成了最佳的音/视频引擎,十分先进的codec,但是Google对于这些技术不收取任何费用。
  3. 强大的打洞能力。WebRTC技术包含了使用STUN、ICE、TURN、RTP-over-TCP的关键NAT和防火墙穿透技术,并支持代理。

缺点

  1. 传输质量难以保证,比如跨地区、跨运营商、低带宽、高丢包、P2P连接率、呼叫成功率。
  2. 设备端适配,如回声、录音失败等问题层出不穷。这一点在安卓设备上尤为突出。由于安卓设备厂商众多,每个厂商都会在标准的安卓框架上进行定制化,导致很多可用性问题(访问麦克风失败)和质量问题(如回声、啸叫)。
WebRTC媒体会话原理

WebRTC内部结构简化图

WebRTC架构图(截图来自官网https://webrtc.org/

WebRTC核心技术点,简要概括为三部分

下文详细介绍WebRTC核心API和信令服务器部分

WebRTC 核心API详解

运用RTCPeerConnection和RTCDataChannel两个核心API,能够实现任意数据的点对点交换,官网Demo如下:

该Demo不需要servers,因为呼叫方(发送数据)和呼叫应答方(接收数据)在同一页面上,这样能够清晰的了解RTCPeerConnection API的原理,页面上的RTCPeerConnection对象可以直接交换数据和消息,而无需使用信令服务器。 可以用开发者工具查看WebRTC统计信息

  • Chrome:chrome://webrtc-internals
  • Opera:opera://webrtc-internals
  • FireFox:about:webrtc

chrome开发者工具中查看WebRTC,如下图所示:

Demo 代码分析

以Demo为例,分析Web P2P创建、通信、传输数据等流程,具体分析API中各个关键属性、方法、事件的含义和标准操作姿势 完整源码见GitHub

function createConnection() {
  sendButton.disabled = true;
  megsToSend.disabled = true;
  var servers = null;

  bytesToSend = Math.round(megsToSend.value) * 1024 * 1024;

  // 创建连接,servers可以传入一些描述信息,由于这个demo不需要验证连接信息,在同一个页面上可以直接连接,该参数传null即可
  localConnection = localConnection = new RTCPeerConnection(servers);
  //打印log
  trace('Created local peer connection object localConnection');

  var dataChannelParams = {ordered: false};
  if (orderedCheckbox.checked) {
    dataChannelParams.ordered = true;
  }

  //创建数据通道 语法:dataChannel = RTCPeerConnection .createDataChannel(label [,options ]);,lable:通道的名称;optins:是个可选参数,传入数据通道配置参数,有很多参数可选,例子中的ordered:true表示有序模式,false即为无序模式,还有其他参数maxPacketLifeTime 、maxRetransmits等等
  sendChannel = localConnection.createDataChannel(
      'sendDataChannel', dataChannelParams);
  sendChannel.binaryType = 'arraybuffer';
  trace('Created send data channel');

  //绑定onopen、onclose、onicecandidate(当RTCPeerConnection被createPeerConnection()成功创建时触发,回调会返回待连接端的配置信息)
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;
  localConnection.onicecandidate = function(e) {
    onIceCandidate(localConnection, e);
  };

  //创建呼叫实例
  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );

  //创建远端接收连接实例
  remoteConnection = remoteConnection = new RTCPeerConnection(servers);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = function(e) {
    onIceCandidate(remoteConnection, e);
  };
  //当一个RTC数据通道已被远端调用createDataChannel()添加到连接中时触发
  remoteConnection.ondatachannel = receiveChannelCallback;
}

function receiveChannelCallback(event) {
  trace('Receive Channel Callback');
  receiveChannel = event.channel;
  receiveChannel.binaryType = 'arraybuffer';
  //接收到数据时触发
  receiveChannel.onmessage = onReceiveMessageCallback;

  receivedSize = 0;
}

function onReceiveMessageCallback(event) {
  receivedSize += event.data.length;
  receiveProgress.value = receivedSize;

  if (receivedSize === bytesToSend) {
    closeDataChannels();
    sendButton.disabled = false;
    megsToSend.disabled = false;
  }
}

function onSendChannelStateChange() {
  var readyState = sendChannel.readyState;
  trace('Send channel state is: ' + readyState);
  if (readyState === 'open') {
    sendGeneratedData();
  }
}

function sendGeneratedData() {
  sendProgress.max = bytesToSend;
  receiveProgress.max = sendProgress.max;
  sendProgress.value = 0;
  receiveProgress.value = 0;

  var chunkSize = 16384;
  var stringToSendRepeatedly = randomAsciiString(chunkSize);
  var bufferFullThreshold = 5 * chunkSize;
  var usePolling = true;
  if (typeof sendChannel.bufferedAmountLowThreshold === 'number') {
    trace('Using the bufferedamountlow event for flow control');
    usePolling = false;

    // 缓冲区大小限值
    bufferFullThreshold = chunkSize / 2;

    // 缓冲区大小控制
    sendChannel.bufferedAmountLowThreshold = bufferFullThreshold;
  }

  // bufferedamountlow 事件处理
  var listener = function() {
    sendChannel.removeEventListener('bufferedamountlow', listener);
    sendAllData();
  };
  var sendAllData = function() {
    // 把一堆数据排队进行处理,在数据通道被填满时停止,这里不建议每次发送后设置Timeout,这样会降低吞吐量
    while (sendProgress.value < sendProgress.max) {
      if (sendChannel.bufferedAmount > bufferFullThreshold) {
        if (usePolling) {
          setTimeout(sendAllData, 250);
        } else {
          sendChannel.addEventListener('bufferedamountlow', listener);
        }
        return;
      }
      sendProgress.value += chunkSize;
      // send方法发送数据,RTCDataChannel的语法跟WebSocket语法非常相似,都有message事件和sand方法
      sendChannel.send(stringToSendRepeatedly);
    }
  };
  setTimeout(sendAllData, 0);
}

WebRTC核心API兼容性

MediaStream and getUserMedia
  • Chrome desktop 18.0.1008+; Chrome for Android 29+
  • Opera 18+; Opera for Android 20+
  • Opera 12, Opera Mobile 12 (基于Presto引擎)
  • Firefox 17+
  • Microsoft Edge
RTCPeerConnection
  • Chrome desktop 20+ (now ‘flagless’, i.e. no need to set about:flags); Chrome for Android 29+ (flagless)
  • Opera 18+ (默认开启); Opera for Android 20+ (默认开启)
  • Firefox 22+ (默认开启)
RTCDataChannel
  • Chrome 25中的实验版本,在Chrome 26+中更稳定(and with Firefox interoperability); Chrome for Android 29+
  • Opera 18+中的稳定版本(and with Firefox interoperability); Opera for Android 20+
  • Firefox 22+ (默认开启)

信令服务器

信令就是协调通讯的过程,为了建立一个webRTC的通讯过程,客户端需要交换如下信息:

  • 会话控制信息,用来开始和结束通话,即开始视频、结束视频这些操作指令。
  • 处理错误的消息。
  • 元数据,如各自的音视频解码方式、带宽。
  • 网络数据,对方的公网IP、端口、内网IP及端口。

我们需要一个中间服务器来在客户端之间交换信令消息和数据,这个过程在WebRTC里面是没有实现的,但WebRTC协议没有规定与服务器的通信方式,因此可以采用各种方式,比如WebSocket。初学者可以用NodeJS搭建简易的信令服务器,交换双方的元数据,真实项目里还会有STUN和TURN服务器 【更多】

下面是NodeJS创建信令服务器的源码:

'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // 打印日志功能
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // 本示例使用广播方式,真实项目中应该是指定房间号(Socket.IO适用于学习WebRTC信号,因为它内置了'房间'的概念)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // 最多两个客户端
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

});
利用WebRTC相关技术有很多可以创新的点,比如业界已有创业团队在做Web P2P,核心技术就是WebRTC + DASH协议,共享空闲资源,基于此可以做雾CDN,节点都在用户侧,去中心化,这里还是有很多挖掘空间的。

参考资料

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

Kevinylzhao的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏微信终端开发团队的专栏

Android M doze特性预研

Android M doze特性预研 2015年5月29日GoogleI/O大会发布新一代Android系统 - Android M preview 版本(AP...

2068
来自专栏FreeBuf

获取来源IP地址的正确姿势

背景 笔者从去年6月份开始研究IP地址,陆续踩了很多很多坑,也结识了一大批同行业的前辈。 我能说我是这个圈子里年龄最小的么…..我一直在承受我这个年纪不该有的...

4216
来自专栏Kirito的技术分享

深入理解JWT的使用场景和优劣

经过前面两篇文章《JSON Web Token - 在Web应用间安全地传递信息》《八幅漫画理解使用JSON Web Token设计单点登录系统》的科普,相信大...

5167
来自专栏Linyb极客之路

工作流引擎之Activiti使用总结

在第一家公司工作的时候主要任务就是开发OA系统,当然基本都是有工作流的支持,不过当时使用的工作流引擎是公司一些牛人开发的(据说是用一个开源的引擎修改的),名称叫...

1534
来自专栏SmartSql

SmartSql 常见问题

SmartSql 希望 开发人员更多的接触 Sql ,获得绝对的控制权与安全感。所以目前没有计划支持 Code First 编程模式。

992
来自专栏张善友的专栏

[腾讯社区开放平台]介绍开放授权协议-OAuth

OAuth (开放授权) 是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所...

1847
来自专栏大内老A

WCF技术剖析之二十七: 如何将一个服务发布成WSDL[编程篇]

对于WCF服务端元数据架构体系来说,通过MetadataExporter将服务的终结点导出成MetadataSet(参考《如何导出WCF服务的元数据》),仅仅是...

1767
来自专栏企鹅号快讯

专为渗透测试人员设计的 Python 工具大合集

如果你对漏洞挖掘、逆向工程分析或渗透测试感兴趣的话,我第一个要推荐给你的就是Python编程语言。Python不仅语法简单上手容易,而且它还有大量功能强大的库和...

2028
来自专栏腾讯Bugly的专栏

三步走起 提升 iOS 审核通过率 上篇

image.png 2016年的第一天,各位小伙伴儿们,新年快乐~~~在过去一年里,负责 iOS 应用开发的同学们,想必已被 APP Store 的审核机制折磨...

4029
来自专栏phodal

技巧 - 如何好一个 Git 提交信息及几种不同的规范

受 Growth 3.0 开发的影响,最近更新文章的频率会有所降低。今天,让我们来谈谈一个好的 Git、SVN 提交信息是怎样规范出来的。 在团队协作中,使用版...

1797

扫码关注云+社区