WebRTC学习笔记四 RTCDataChannel

一、RTCDataChannel

参见MDN RTCDataChannel 简单示例

简单来说,RTCDataChannel 就是在点对点连接中建立一个双向的数据通道,从而获得文本、文件等数据的点对点传输能力。它依赖于流控制传输协议(SCTP),SCTP 是一种传输协议,类似于 TCP 和 UDP,可以直接在 IP 协议之上运行。但是,在 WebRTC 的情况下,SCTP 通过安全的 DTLS 隧道进行隧道传输,该隧道本身在 UDP 之上运行。

image.png

这个示例在Connect之后,可以输入文本后,使用send发送到另一端。注意: 尽管参与连接的两端都在同一页面,我们将启动连接的一端称为 "local" 端,另一端称为 "remote" 端。

1.建立本地节点
localConnection = new RTCPeerConnection();

sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;

第一步是建立该连接的 "local" 端,它是发起连接请求的一方。 下一步是通过调用RTCPeerConnection.createDataChannel() 来创建 RTCDataChannel 并设置事件侦听以监视该数据通道, 从而获知该通道的打开或关闭 (即获得该对等连接的通道打开或者关闭的时机)。

请务必记住该通道的每一端都拥有自己的 RTCDataChannel 对象。

createDataChannel方法允许传入第二个参数进行不同的配置

  • reliable 设置消息传递是否进行担保
  • ordered 用来设置消息的接受是否需要按照发送时的顺序
  • maxRetransmitTime 设置消息发送失败时,多久重新发送
  • maxRetransmits 设置消息发送失败时,最多重发次数
  • protocol 设置强制使用其他子协议,但当用户代理不支持该协议时会报错
  • negotiated 此选项用来设置开发人员是否有责任在两边创建数据通道,还是浏览器来自动完成这个步骤
  • id 这个用来设置通道的唯一标识,可以在多通道时进行区分

这些配置项很多,不过大部分只在高级应用中才会使用。主要使用的配置项是reliable和 ordered,当设置为true时数据通道表现得更像TCP,设置为False时表现得更像UDP。

negotiated 为解决两边用户同步创建数据通道而设置。用来处理ondatachannel事件触发时的不同行为。默认值是false,表示浏览器会在通道另一边自动触发这个事件,告诉它这个新通道。如果设置成True,开发者需要自己在通道两边创建相同ID的数据通道。

2.建立远程节点
remoteConnection = new RTCPeerConnection();
remoteConnection.ondatachannel = receiveChannelCallback;

远程端的建立过程类似“local”端, 但它无需自己创建 RTCDataChannel , 因为我们将通过上面建立的渠道进行连接。 我们创建对 datachannel 的事件处理回调;数据通道打开时该逻辑将被执行, 该回调处理将接收到一个 RTCDataChannel 对象,此过程将在文章后面部分描述。

3.设立ICE 候选人

下一步为每个连接建立 ICE 候选侦听处理, 当连接的一方出现新的 ICE 候选时该侦听逻辑将被调用以告知连接的另一方此消息。

注意: 在现实场景,当参与连接的两节点运行于不同的上下文,建立连接的过程或稍微复杂些,每一次双方通过调用RTCPeerConnection.addIceCandidate(),提出连接方式的建议 (例如: UDP,、中继UDP 、 TCP之类的) , 双方来回往复直到达成一致。本文既然不涉及现实网络环境,因此我们假定双方接受首次连接建议。

    localConnection.onicecandidate = e => !e.candidate
        || remoteConnection.addIceCandidate(e.candidate)
        .catch(handleAddCandidateError);

    remoteConnection.onicecandidate = e => !e.candidate
        || localConnection.addIceCandidate(e.candidate)
        .catch(handleAddCandidateError);

我们配置每个 RTCPeerConnection 对于事件 icecandidate 建立事件处理。

4.启动连接尝试

建立节点连接的最后一项是创建一个连接offer.

    localConnection.createOffer()
    .then(offer => localConnection.setLocalDescription(offer))
    .then(() => remoteConnection.setRemoteDescription(localConnection.localDescription))
    .then(() => remoteConnection.createAnswer())
    .then(answer => remoteConnection.setLocalDescription(answer))
    .then(() => localConnection.setRemoteDescription(remoteConnection.localDescription))
    .catch(handleCreateDescriptionError);
5.数据通道(data channel)的连接

RTCPeerConnection 一旦open, 事件datachannel 被发送到远端以完成打开数据通道的处理, 该事件触发 receiveChannelCallback() 方法,如下所示:

  function receiveChannelCallback(event) {
    receiveChannel = event.channel;
    receiveChannel.onmessage = handleReceiveMessage;
    receiveChannel.onopen = handleReceiveChannelStatusChange;
    receiveChannel.onclose = handleReceiveChannelStatusChange;
  }

事件datachannel 在它的channel属性中包括了: 对代表remote节点的 channel的RTCDataChannel 的指向, 它保存了我们用以在该channel上对我们希望处理的事件建立的事件监听。 一旦侦听建立, 每当remote节点接收到数据 handleReceiveMessage() 方法将被调用, 每当通道的连接状态发生改变 handleReceiveChannelStatusChange() 方法将被调用, 因此通道完全打开或者关闭时我们都可以作出相应的相应。

6.对通道状态变化的处理

local节点和remote节点采用同样的方法处理表示通道连接状态变更的事件。

当local节点遭遇open 或者 close 事件, handleSendChannelStatusChange() 方法被调用:

function handleSendChannelStatusChange(event) {
    if (sendChannel) {
      var state = sendChannel.readyState;

      if (state === "open") {
        messageInputBox.disabled = false;
        messageInputBox.focus();
        sendButton.disabled = false;
        disconnectButton.disabled = false;
        connectButton.disabled = true;
      } else {
        messageInputBox.disabled = true;
        sendButton.disabled = true;
        connectButton.disabled = false;
        disconnectButton.disabled = true;
      }
    }
  }

如果通道状态已经变更为 "open", 意味着我们已经完成了在两对等节点之间建立连接。 相应地用户界面根据状态更新,许用并将输入光标聚焦在text 输入框,以便用户可以立即输入要发送给对方的文本消息, 同时界面许用 "Send" 和 "Disconnect" 按钮(既然它们已经准备好了),禁用"Connect"按钮,既然在已经建立连接的情况下用不着它。

当连接状态变更为 "closed"时,界面执行相反的操作: 禁用文本输入框和 "Send" 按钮 , 许用"Connect" 按钮, 以便用户在需要时可以打开新的连接,禁用"Disconnect" 按钮,既然没有连接时用不着它。

另一方面,作为我们例子的remote 节点, 则无视这些状态改变事件, 仅仅是在控制台输出它们:

function handleReceiveChannelStatusChange(event) {
    if (receiveChannel) {
      console.log("Receive channel's status has changed to " +
                  receiveChannel.readyState);
    }
  }
7.发送消息

当用户按下 "Send" 按钮,触发我们已建立的该按钮的 click事件处理逻辑,在处理逻辑中调用sendMessage() 方法。 该方法也足够简单:

  function sendMessage() {
    var message = messageInputBox.value;
    sendChannel.send(message);

    messageInputBox.value = "";
    messageInputBox.focus();
  }

send方法可以发送String,Blob,ArrayBuffer,ArrayBufferView。

8.接收消息

当远程通道发生“message”事件时,我们的handleReceiveMessage()方法被调用来处理事件。

  function handleReceiveMessage(event) {
    var el = document.createElement("p");
    var txtNode = document.createTextNode(event.data);

    el.appendChild(txtNode);
    receiveBox.appendChild(el);
  }
二、多人画板例子

参考
【从头到脚】WebRTC + Canvas 实现一个双人协作的共享画板

// 有省略
constructor(canvas, {moveCallback}) {
    ···
    this.moveCallback = moveCallback || function () {}; // 鼠标移动的回调
}
onmousemove(e) { // 鼠标移动
    this.isMoveCanvas = true;
    let endx = e.offsetX;
    let endy = e.offsetY;
    let width = endx - this.x;
    let height = endy - this.y;
    let now = [endx, endy]; // 当前移动到的位置
    switch (this.drawType) {
        case 'line' : {
            let params = [this.last, now, this.lineWidth, this.drawColor];
            this.moveCallback('line', ...params);
            this.line(...params);
        }
        ...
moveCallback(...arr) { // 同步到对方
    this.send(arr);
},
send(arr) { // 发送消息
    if (arr[0] === 'text') {
        ···
    } else { // 处理数据同步
        this.channel.send(JSON.stringify(arr));
    }
}

页面收到 Callback 通知以后,直接调用 send 方法,将数据传递给对方。接收到数据后,调用封装类相应方法进行绘制。

handleChannel(channel) { // 处理 channel
    ···
    channel.onmessage = (e) => { // 收到消息 普通消息类型是 对象
        if (Array.isArray(JSON.parse(e.data))) { // 如果收到的是数组,进行结构
            let [type, ...arr] = JSON.parse(e.data);
            this.palette[type](...arr); // 调用相应方法
        } else {
            this.messageList.push(JSON.parse(e.data)); // 接收普通消息
        }
        // console.log('channel onmessage', e.data);
    };
}
```·
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容