在上一篇中介绍了如何在局域网内使用浏览器进行1v1视频通话。现在我们介绍一下如何在前端使用WebRTC进行消息通信。
简单的Demo
通过PeerConnection让两个浏览器建立连接,并进行相互发送消息
效果
在控制台中,通过使用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在建立连接的部分是不同的,在建立连接之后的用法是基本一致的。他们之间还是有一些不同之处的。
- 传输协议
WebSocket是基于TCP协议的,传输数据都是有序的。而DataChannel是基于UDP的,并且可以在创建的时候通过ordered字段来控制是否是可靠传输。如果选择无序模式,在弱网条件下延迟相对更低,但是不会保证消息的可靠性和有序。
- 架构不同
WebSocket是客户端和服务端建立连接的,如果想要两个客户端相互发送消息,还需要有服务端的支持。消息是通过服务端进行转发的。但是DataChannel是P2P的模式,建立连接成功后,就不再需要服务端。
- 隐蔽性
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>
其他
如果你也是专注前端多媒体或者对前端多媒体感兴趣,可以关注前端多媒体公众号