前端使用WebRTC———DataChannel

640 阅读3分钟

上一篇中介绍了如何在局域网内使用浏览器进行1v1视频通话。现在我们介绍一下如何在前端使用WebRTC进行消息通信。

简单的Demo

通过PeerConnection让两个浏览器建立连接,并进行相互发送消息

效果

image.png 在控制台中,通过使用datachannel.send进行发送,可以看到两个页面之间可以实现相互发送消息。

流程

建立连接的流程可以参考局域网内单向通信的流程,只是在createOffer之前先添加了DataChannel而已。服务端的代码是完全一致的。

API

创建DataChannel

通过RTCPeerConnection.createDataChannel方法来创建一个DataChannel,需要在createOffer之前创建。

function createDataChannel(label: string, dataChannelDict?: RTCDataChannelInit): RTCDataChannel;
  • label

其中labtel是必须要设置的,是data channel的名字,长度与能超过65535个字节。

  • dataChannelDict

dataChannelDict是可选字段,下面会详细解释dataChannelDict的用法

interface RTCDataChannelInit {
    id?: number;
    maxRetransmits?: number;
    negotiated?: boolean;
    ordered?: boolean;
}
  • id

id是通道的id,可以取值范围是0-65534。如果不设置id,会自动生成一个id。

  • negotiated

默认是false,如果是true,在PeerConnection的另一端不会收到"datachannel",如果想要使用这个datachannel进行发送消息,需要在createAnswer之前,也创建一个相同id和label的datachannel。

  • maxRetransmits

消息的最大重穿次数

  • ordered

默认是true。表示是否是可靠通道。如果是可靠通道,则消息是有序的。

DataChannel的事件

DataChannel常用的事件和WebSocket是一样的。

  • open

表示DataChannel建立连接成功了,收到open消息后,就可以开始接收和发送消息了。

  • message

收到消息,同时消息的类型有string和binary两种。

  • close

DataChannel被关闭

  • error

DataChannel发生错误

DataChannel的方法

  • send

发送数据,支持string和binary类型

  • close

断开DataChannel连接

DataChannel VS WebSocket

通过上面的介绍可以看到,DataChannel和WebSocket在建立连接的部分是不同的,在建立连接之后的用法是基本一致的。他们之间还是有一些不同之处的。

  1. 传输协议

WebSocket是基于TCP协议的,传输数据都是有序的。而DataChannel是基于UDP的,并且可以在创建的时候通过ordered字段来控制是否是可靠传输。如果选择无序模式,在弱网条件下延迟相对更低,但是不会保证消息的可靠性和有序。

  1. 架构不同

WebSocket是客户端和服务端建立连接的,如果想要两个客户端相互发送消息,还需要有服务端的支持。消息是通过服务端进行转发的。但是DataChannel是P2P的模式,建立连接成功后,就不再需要服务端。

  1. 隐蔽性

WebSocket的连接在浏览器的调试工具的network中是可见的,包括发送和接收的数据。但是DataChannel在network中是不可见的。

代码

可以直接从gittee获取源代码,也可以直接使用下面的代码

服务端

var ws = require("nodejs-websocket");


var pub_ws = null;
var sub_ws = null;


function start() {
  var msg = JSON.stringify({ type: "start" });
  pub_ws.send(msg);
}

var server = ws.createServer(function (conn) {
  // 收到websocket连接
  conn.on("text", function (str) {
    if (pub_ws === conn) {
      if (sub_ws) {
        sub_ws.send(str);
      }
    } else if (sub_ws === conn) {
      if (pub_ws) {
        pub_ws.send(str);
      }
    } else {
      let obj = JSON.parse(str);
      if (obj.type === 'publish') {
        pub_ws = conn;
        if (sub_ws) {
          start();
        }
      } else if (obj.type === 'subscribe') {
        sub_ws = conn;
        if (pub_ws) {
          start();
        }
      }
    }
  })

  conn.on("error", function (event) {

  });

  conn.on("close", function (code, reason) {
    if (conn === pub_ws) {
      console.log("remove pub")
      pub_ws = null;
    } else if (conn === sub_ws) {
      console.log("remove sub")
      sub_ws = null;
    }
  })
}).listen(9000);

发起端页面

<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>推流页面</title>
</head>

<body>
  <div>

  </div>
  <script>
    // datachannel对象
    let datachannel = null;

    // 推流用的MediaStream
    let pc_pub = null;

    let ws = new WebSocket('ws://127.0.0.1:9000');
    ws.addEventListener('open', () => {
      // 通知server pub已经上线
      ws.send(JSON.stringify({
        type: "publish"
      }))
    })

    ws.addEventListener('message', (event) => {
      let msg = JSON.parse(event.data);
      switch (msg.type) {
        case "start":
          start();
          break;

        case "answer":
          pc_pub.setRemoteDescription(msg).then(() => {

          }).catch((err) => {

          })
          break;

        default:
          pc_pub.addIceCandidate(msg);
          break;
      }
    })



    function start() {
      pc_pub = new RTCPeerConnection();
      pc_pub.addEventListener('icecandidate', (event) => {
        if (event.candidate) {
          ws.send(JSON.stringify(event.candidate));
        }
      })
      addDataChannel();
      pc_pub.createOffer().then((offer) => {
        pc_pub.setLocalDescription(offer).then(() => {
          ws.send(JSON.stringify(offer));
        }).catch((err) => {
          console.error('setLocalDescription error', err);
        })
      }).catch((err) => {
        console.error("create offer error", err);
      })
    }


    function addDataChannel() {
      datachannel = pc_pub.createDataChannel('dc', {
        ordered: true,
        id: 1,
      });
      datachannel.addEventListener('open', onDataChannelOpen);
      datachannel.addEventListener('close', onDataChannelClose);
      datachannel.addEventListener('error', onDataChannelError);
      datachannel.addEventListener('message', onDataChannelMessage);
    }


    function onDataChannelOpen() {
      console.log("pub datachannel open")
    }

    function onDataChannelClose() {
      console.log("pub datachannel close")
    }

    function onDataChannelError() {
      console.log("pub datachannel error")
    }

    function onDataChannelMessage(event) {
      console.log("pub datachannel messae:", event.data)
    }



  </script>
</body>

</html>

接收端页面

<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>推流页面</title>
</head>

<body>
  <div>

  </div>
  <script>
    // datachannel对象
    let datachannel = null;

    // 订阅流用的Peerconnection
    let pc_sub = new RTCPeerConnection();

    let ws = new WebSocket('ws://127.0.0.1:9000');
    ws.addEventListener('open', () => {
      // 通知server pub已经上线
      ws.send(JSON.stringify({
        type: "subscribe"
      }))
    })

    ws.addEventListener('message', (event) => {
      let msg = JSON.parse(event.data);
      switch (msg.type) {
        case "start":
          break;

        case "offer":
          pc_sub.setRemoteDescription(msg).then(() => {
            pc_sub.createAnswer().then((answer) => {
              pc_sub.setLocalDescription(answer).then(() => {
                ws.send(JSON.stringify(answer));
              }).catch((err) => {

              })
            }).catch((err) => {
              console.error('create answer error', err);
            })
          }).catch((err) => {
            console.error('setRemoteDescription error', err);
          })
          break;

        default:
          pc_sub.addIceCandidate(msg);
          break;
      }
    })

    pc_sub.addEventListener('icecandidate', (event) => {
      if (event.candidate) {
        ws.send(JSON.stringify(event.candidate));
      }
    })

    pc_sub.addEventListener('datachannel', (event) => {
      console.log('recv datachannel', event.channel);
      datachannel = event.channel;
      datachannel.addEventListener('open', onDataChannelOpen);
      datachannel.addEventListener('close', onDataChannelClose);
      datachannel.addEventListener('error', onDataChannelError);
      datachannel.addEventListener('message', onDataChannelMessage);
    })

    function onDataChannelOpen() {
      console.log("sub datachannel open")
    }

    function onDataChannelClose() {
      console.log("sub datachannel close")
    }

    function onDataChannelError() {
      console.log("sub datachannel error")
    }

    function onDataChannelMessage(event) {
      console.log("sub datachannel messae:", event.data)
    }


  </script>
</body>

</html>

其他

如果你也是专注前端多媒体或者对前端多媒体感兴趣,可以关注前端多媒体公众号

image.png