背景:
平时我们很少会需要使用到点对点单独的通讯,即p2p,一般都是点对服务端通讯,但p2p也有自己的好处,即通讯不经过服务端,从服务端角度这个省了带宽和压力,从客户端角度,通讯是安全,且快速的,当然有些情况下可能速度并不一定快。那么如何实现p2p呢?
解决办法:
webrtc的RTCPeerConnection就实现了p2p的功能,使用RTCPeerConnection需要理解一些概念,什么是信令,信令交换的过程,信令服务器。
信令
2个设备需要通讯,就需要知道对方的在互联网上的公开地址,一般情况下2个设备都是不会直接拥有一个公网的ip地址,所以他们之间的通讯,就需要如何在公网找到自己的方式,路由信息告诉对方,通常这个信息都是临时的,并非永久,当对方获取到这个信息后,就可以通过网络找到彼此的实际路由路径,从而进行通讯,这个信息就是信令(位置信息)。
信令的交换过程:
假设2个设备,p1要和p2进行通讯
1.p1发起邀请阶段
javascript
const offer = await p1.createOffer();//创建邀请信令
await p1.setLocalDescription(offer);//设置为本地信令
send(JSON.stringify(offer));//把邀请信令发送给对方,至于怎么发送,一般是需要一个第3方的信令服务器来转发这个信息
2.p2收到邀请阶段
当收到p1发起的有邀请信令offer后
javascript
await p2.setRemoteDescription(new RTCSessionDescription(JSON.parse(offer)));//设置为远端的信令
const answer = await p2.createAnswer();//创新一个应答信令,告诉p1我的位置
await pc.setLocalDescription(answer);//设置我的位置
send(JSON.stringify(answer ));将位置信息发送给p1
3.p1收到应答信息阶段
javascript
await p2.setRemoteDescription(new RTCSessionDescription(JSON.parse(answer )));//设置为远端的信令
4.处理onicecandidate事件,确认要不要通讯
javascript
await p2.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate)));
完成上述几个阶段,正常来说就能开始通讯了
数据通讯DataChannel的使用
发送端
javascript
// 创建PeerConnection对象
const pc = new RTCPeerConnection();
// 创建DataChannel
const dataChannel = pc.createDataChannel('myDataChannel');
// 监听DataChannel的open事件
dataChannel.onopen = () => {
console.log('DataChannel已打开');
};
// 监听DataChannel的error事件
dataChannel.onerror = (error) => {
console.error('DataChannel错误:', error);
};
// 监听DataChannel的close事件
dataChannel.onclose = () => {
console.log('DataChannel已关闭');
};
// 发送文本消息
function sendMessage(message) {
dataChannel.send(message);
}
// 发起PeerConnection连接
// ...
// 在某个事件触发时调用sendMessage()发送消息
// sendMessage('Hello, world!');
接收端:
javascript
// 创建PeerConnection对象
const pc = new RTCPeerConnection();
// 监听DataChannel的open事件
pc.ondatachannel = (event) => {
const dataChannel = event.channel;
// 监听DataChannel的message事件
dataChannel.onmessage = (event) => {
const message = event.data;
console.log('接收到消息:', message);
};
// 监听DataChannel的error事件
dataChannel.onerror = (error) => {
console.error('DataChannel错误:', error);
};
// 监听DataChannel的close事件
dataChannel.onclose = () => {
console.log('DataChannel已关闭');
};
};
datachannel的用法发送端和接收端用法是一样的,只是接收端,需要通过 onicecandidate的事件才能获取到。
单页面完整demo
html
<!DOCTYPE html>
<html>
<head>
<title>WebRTC Demo</title>
</head>
<body>
<h1>WebRTC Demo</h1>
<button onclick="start()">Start</button>
<button onclick="call()">Call</button>
<button onclick="hangup()">Hang Up</button>
<button onclick="sendMessage()">send</button>
<br><br>
<textarea id="localDesc"></textarea>
<br><br>
<textarea id="remoteDesc"></textarea>
<br><br>
<textarea id="message"></textarea>
<br><br>
<textarea id="received"></textarea>
<script>
let localConnection, remoteConnection,dataChannel,receiveChannel;
function start() {
localConnection = new RTCPeerConnection();
remoteConnection = new RTCPeerConnection();
localConnection.onicecandidate = e => {
if (e.candidate) {
console.log("localConnection.onicecandidate")
remoteConnection.addIceCandidate(e.candidate);
}
};
remoteConnection.onicecandidate = e => {
if (e.candidate) {
console.log("remoteConnection.onicecandidate")
localConnection.addIceCandidate(e.candidate);
}
};
localConnection.oniceconnectionstatechange = e => {
console.log('Local ICE connection state change:', localConnection.iceConnectionState);
};
remoteConnection.oniceconnectionstatechange = e => {
console.log('Remote ICE connection state change:', remoteConnection.iceConnectionState);
};
remoteConnection.ondatachannel = e => {
console.log("ondatachannel",e)
receiveChannel = e.channel;
receiveChannel.onmessage = e => {
console.log("onmessage",e.data)
document.getElementById('received').value += e.data + '\n';
};
};
dataChannel = localConnection.createDataChannel('dataChannel');
dataChannel.onopen = e => {
console.log("onopen")
console.log('Data channel opened');
};
dataChannel.onclose = e => {
console.log("onclose")
console.log('Data channel closed');
};
dataChannel.onmessage = event => {
console.log("onmessage",event.data)
};
}
async function call() {
console.log("createOffer")
const offer = await localConnection.createOffer();
await localConnection.setLocalDescription(offer);
await remoteConnection.setRemoteDescription(offer);
console.log("createAnswer")
const answer = await remoteConnection.createAnswer();
await remoteConnection.setLocalDescription(answer);
await localConnection.setRemoteDescription(answer);
document.getElementById('localDesc').value = localConnection.localDescription.sdp;
document.getElementById('remoteDesc').value = remoteConnection.localDescription.sdp;
}
async function hangup() {
await localConnection.close();
await remoteConnection.close();
localConnection = null;
remoteConnection = null;
}
function sendMessage() {
const message = document.getElementById('message').value;
//const dataChannel = localConnection.createDataChannel('dataChannel');
dataChannel.send(message);
console.log("send",message)
}
</script>
</body>
</html>
不同页面demo,信令交换过程手动操作
javascript
<!DOCTYPE html>
<html>
<head>
<title>WebRTC 文本消息发送</title>
</head>
<body>
<textarea id="xx"></textarea>
<textarea id="xx2"></textarea>
<textarea id="xx3"></textarea>
<div>
<label for="message">消息:</label>
<input type="text" id="message" />
<button onclick="sendMessage()">发送</button>
<button onclick="sendOffer()">邀请</button>
<button onclick="handleAnswer()">接收远程信令</button>
<button onclick="handleCandidate()">接收candidate</button>
<br>
<button onclick="handleOffer()">接收邀请</button>
</div>
<div id="chat"></div>
<script>
let pc;
let dataChannel;
// 创建本地PeerConnection对象
function createPeerConnection() {
pc = new RTCPeerConnection();
// 创建数据通道
dataChannel = pc.createDataChannel('chat');
// 监听收到消息事件
dataChannel.onmessage = event => {
console.log(event.data)
const message = event.data;
displayMessage(message);
};
// 监听连接状态变化事件
dataChannel.onopen = () => {
displayMessage('连接已建立');
};
dataChannel.onclose = () => {
displayMessage('连接已关闭');
};
pc.ondatachannel = (e)=>{
console.log("ondatachannel",e)
};
// 监听ICE候选事件
pc.onicecandidate = e => {
if (e.candidate) {
document.getElementById("xx3").value = JSON.stringify(e.candidate);
}
};
pc.oniceconnectionstatechange = e => {
console.log('Local ICE connection state change:', pc.iceConnectionState);
};
}
createPeerConnection()
// 处理信令
function handleSignal(signal) {
switch (signal.type) {
case 'offer':
handleOffer(signal.offer);
break;
case 'answer':
handleAnswer(signal.answer);
break;
case 'candidate':
handleCandidate(signal.candidate);
break;
}
}
async function sendOffer(){
let desc = await pc.createOffer()
pc.setLocalDescription(desc);
document.getElementById("xx").value = JSON.stringify(desc)
console.log(desc)
}
// 处理Offer信令
async function handleOffer() {
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(document.getElementById("xx2").value)));
let answer = await pc.createAnswer()
await pc.setLocalDescription(answer);
document.getElementById("xx").value = JSON.stringify(answer)
}
// 处理Answer信令
async function handleAnswer() {
// 设置远端描述
let answer = new RTCSessionDescription(JSON.parse(document.getElementById("xx2").value))
await pc.setRemoteDescription(answer);
}
// 处理ICE候选信令
async function handleCandidate() {
try {
await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(document.getElementById("xx2").value)));
} catch (error) {
console.error('添加ICE候选失败:', error);
}
}
// 发送消息
function sendMessage() {
const messageInput = document.getElementById('message');
const message = messageInput.value;
dataChannel.send(message);
displayMessage('我:' + message);
messageInput.value = '';
}
// 显示消息
function displayMessage(message) {
const chatDiv = document.getElementById('chat');
const messageP = document.createElement('p');
messageP.textContent = message;
chatDiv.appendChild(messageP);
}
</script>
</body>
</html>