背景:
平時我們很少會需要使用到點對點單獨的通訊,即p2p,一般都是點對服務端通訊,但p2p也有自己的好處,即通訊不經過服務端,從服務端角度這個省了帶寬和壓力,從客戶端角度,通訊是安全,且快速的,當然有些情況下可能速度并不一定快。那么如何實現p2p呢?
解決辦法:
webrtc的RTCPeerConnection就實現了p2p的功能,使用RTCPeerConnection需要理解一些概念,什么是信令,信令交換的過程,信令服務器。
信令
2個設備需要通訊,就需要知道對方的在互聯網上的公開地址,一般情況下2個設備都是不會直接擁有一個公網的ip地址,所以他們之間的通訊,就需要如何在公網找到自己的方式,路由信息告訴對方,通常這個信息都是臨時的,并非永久,當對方獲取到這個信息后,就可以通過網絡找到彼此的實際路由路徑,從而進行通訊,這個信息就是信令(位置信息)。
信令的交換過程:
假設2個設備,p1要和p2進行通訊
1.p1發起邀請階段
const offer = await p1.createOffer();//創建邀請信令
await p1.setLocalDescription(offer);//設置為本地信令send(JSON.stringify(offer));//把邀請信令發送給對方,至于怎么發送,一般是需要一個第3方的信令服務器來轉發這個信息
2.p2收到邀請階段
?當收到p1發起的有邀請信令offer后
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收到應答信息階段
await p2.setRemoteDescription(new RTCSessionDescription(JSON.parse(answer )));//設置為遠端的信令
4.處理onicecandidate事件,確認要不要通訊
?await p2.addIceCandidate(new RTCIceCandidate(JSON.parse(candidate)));
完成上述幾個階段,正常來說就能開始通訊了
數據通訊DataChannel的使用
發送端
// 創建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!');
接收端:
// 創建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
<!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,信令交換過程手動操作
<!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>