WebRTC切换Socket.io信令后仅重启服务器首次可用问题排查
1对1 WebRTC视频连接切换Socket.io信令后异常问题
搭建1对1 Peer-to-Peer(P2P)视频连接,双方可互看本地流(localStream)与远程流(remoteStream)。原使用AgoraRTM作为信令服务时运行正常,切换为Socket.io后出现异常:仅在重启WebSocket服务器后第一次运行正常,后续无法建立连接。已排查房间内用户数量、WebSocket连接ID、SDP Offer/Answer收发(确认正常),需定位问题根源并解决。
代码示例
main.js(WebRTC与客户端Socket.io逻辑)
let token = null; let uid = String(Math.floor(Math.random() * 10000)); let queryString = window.location.search; const urlSearch = new URLSearchParams(queryString); const room = urlSearch.get("room"); if (!room) { window.location = "lobby.html"; } let client; let channel; let socket; const constraints = { video: { width: { min: 640, ideal: 1920, max: 1920 }, height: { min: 480, ideal: 1080, max: 1920 }, aspectRatio: 1.777777778, }, audio: false, }; const servers = { iceServers: [ { urls: [ "stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302", "stun:stun3.l.google.com:19302", "stun:stun4.l.google.com:19302", ], }, ], }; const localVideoRef = document.getElementById("localVideo"); const remoteVideoRef = document.getElementById("remoteVideo"); let localStream; let remoteStream; let peerConnection; const configureSignaling = async () => { socket = await io.connect("http://localhost:4000"); socket.emit("join", { room, uid }); socket.on("MemberJoined", handleMemberJoined); socket.on("MessageFromPeer", handleMessageFromPeer); socket.on("MemberLeft", handleMemberLeft) }; const handleMemberLeft = async () => { remoteVideoRef.style.display = "none"; }; const handleMessageFromPeer = (m, uid) => { const message = JSON.parse(m.text); if(message.type !== "candidate") { console.log('handleMessageFromPeer: ', message, uid) } if (message.type === "offer") { createAnswer(uid, message.offer); } if (message.type === "answer") { addAnswer(message.answer); } if (message.type === "candidate") { if (peerConnection && peerConnection.currentRemoteDescription) { peerConnection.addIceCandidate(message.candidate); } } }; const createLocalStream = async () => { localStream = await navigator.mediaDevices.getUserMedia(constraints); localVideoRef.srcObject = localStream; }; const init = async () => { await configureSignaling(); await createLocalStream(); }; const handleMemberJoined = async (uid) => { createOffer(uid); }; let createOffer = async (uid) => { await createPeerConnection(uid); let offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); socket.emit( "sendMessageToPeer", { text: JSON.stringify({ type: "offer", offer: offer }) }, uid ); }; let createPeerConnection = async (uid) => { peerConnection = new RTCPeerConnection(servers); remoteStream = new MediaStream(); remoteVideoRef.srcObject = remoteStream; remoteVideoRef.style.display = "block"; remoteVideoRef.classList.add("remoteFrame"); if (!localStream) { await createLocalStream(); } localStream.getTracks().forEach((track) => { peerConnection.addTrack(track, localStream); }); peerConnection.ontrack = (event) => { event.streams[0].getTracks().forEach((track) => { remoteStream.addTrack(track); }); }; peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.emit( "sendMessageToPeer", { text: JSON.stringify({ type: "candidate", candidate: event.candidate, }), }, uid ); } }; }; let createAnswer = async (uid, offer) => { await createPeerConnection(uid); await peerConnection.setRemoteDescription(offer); const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); socket.emit( "sendMessageToPeer", { text: JSON.stringify({ type: "answer", answer: answer }) }, uid ); }; let addAnswer = async (answer) => { if (!peerConnection.currentRemoteDescription) { peerConnection.setRemoteDescription(answer); } }; let onLogout = async () => { remoteVideoRef.classList.remove("remoteFrame"); await socket.emit('onLeaveRoom', room) }; let onToggleCamera = async () => { const videoTrack = localStream .getTracks() .find((track) => track.kind === "video"); if (videoTrack.enabled) { videoTrack.enabled = false; document.getElementById("camera-btn").style.backgroundColor = "rgb(255, 80, 80)"; } else { videoTrack.enabled = true; document.getElementById("camera-btn").style.backgroundColor = "rgb(179, 102, 249, .9)"; } }; let onToggleMic = async () => { const audioTrack = localStream .getTracks() .find((track) => track.kind === "audio"); if (audioTrack.enabled) { audioTrack.enabled = false; document.getElementById("mic-btn").style.backgroundColor = "rgb(255, 80, 80)"; } else { audioTrack.enabled = true; document.getElementById("mic-btn").style.backgroundColor = "rgb(179, 102, 249, .9)"; } }; window.addEventListener("beforeunload", onLogout); init();
index.js(WebSocket服务端)
const express = require("express"); const app = express(); const PORT = 4000; const http = require("http").Server(app); const cors = require("cors"); app.use(cors()); const socketIO = require("socket.io")(http, { cors: { origin: "http://127.0.0.1:5501", }, }); //Add this before the app.get() block socketIO.on("connection", (socket) => { console.log(`⚡: ${socket.id} user just connected!`); console.log('CONNECTED') socket.on('join', async ({room, uid}) => { await socket.join(room); const connectedUsers = await socket.in(room).fetchSockets() console.log('room:', connectedUsers.length); if(connectedUsers.length) { console.log('PUMBA') socket.broadcast.emit('MemberJoined', uid) } }) socket.on("onLeaveRoom", async (room) => { await socket.leave(room) socket.broadcast.emit('MemberLeft') }) socket.on("disconnect", async (room) => { socket.broadcast.emit('MemberLeft') }) socket.on('sendMessageToPeer', (data, uid) => { console.log('sendMessageTOPEER', uid) socket.broadcast.emit('MessageFromPeer', data, uid ) }); }); app.get("/api", (req, res) => { res.json({ message: "Hello world", }); }); http.listen(PORT, () => { console.log(`Server listening on ${PORT}`); });
问题根源分析
- Socket.io消息广播范围错误:使用
socket.broadcast.emit会向所有连接的客户端广播消息,而非仅目标房间内的用户。后续连接时,无关客户端会收到错误的信令消息,打乱WebRTC协商流程。 - UID与Socket连接标识混淆:客户端自定义的
uid与Socket.io的socket.id(唯一连接标识)混用,导致信令消息无法精准发送到目标客户端。 - 未清理旧PeerConnection实例:用户离开或重新连接时,未关闭并销毁旧的
peerConnection和remoteStream,新连接复用旧实例引发协商冲突。
解决方案与代码修改
1. 服务端(index.js)修改
核心修改点:
- 维护房间用户映射表,关联
socket.id与客户端uid - 所有房间内消息使用
socket.to(room).emit或socketIO.to(targetSocketId).emit精准发送 - 修正成员加入/离开的通知范围
修改后的服务端代码:
const express = require("express"); const app = express(); const PORT = 4000; const http = require("http").Server(app); const cors = require("cors"); app.use(cors()); const socketIO = require("socket.io")(http, { cors: { origin: "http://127.0.0.1:5501", }, }); // 维护房间用户映射:roomId -> Map(socketId => uid) const roomUsers = new Map(); socketIO.on("connection", (socket) => { console.log(`⚡: ${socket.id} user just connected!`); socket.on('join', async ({room, uid}) => { await socket.join(room); // 初始化房间用户表 if (!roomUsers.has(room)) { roomUsers.set(room, new Map()); } roomUsers.get(room).set(socket.id, uid); const connectedUsers = await socket.in(room).fetchSockets(); // 房间内已有其他用户时,通知旧用户新成员加入,同时通知新用户旧成员存在 if (connectedUsers.length > 1) { // 向房间内除自己外的用户发送新成员信息 socket.to(room).emit('MemberJoined', uid, socket.id); // 向新用户发送房间内已存在的用户信息 const existingUsers = Array.from(roomUsers.get(room).entries()) .filter(([sid]) => sid !== socket.id) .map(([sid, userUid]) => ({uid: userUid, socketId: sid})); socket.emit('ExistingMembers', existingUsers); } }) socket.on("onLeaveRoom", async (room) => { await socket.leave(room); roomUsers.get(room)?.delete(socket.id); // 仅通知房间内用户 socket.to(room).emit('MemberLeft'); }) socket.on("disconnect", async () => { // 遍历所有房间,移除当前socket的记录 for (const [room, users] of roomUsers.entries()) { if (users.has(socket.id)) { users.delete(socket.id); socket.to(room).emit('MemberLeft'); // 房间空了就删除 if (users.size === 0) { roomUsers.delete(room); } break; } } }) socket.on('sendMessageToPeer', (data, targetSocketId) => { // 精准发送给目标socket socketIO.to(targetSocketId).emit('MessageFromPeer', data); }); }); app.get("/api", (req, res) => { res.json({ message: "Hello world", }); }); http.listen(PORT, () => { console.log(`Server listening on ${PORT}`); });
2. 客户端(main.js)修改
核心修改点:
- 添加资源清理函数,关闭旧PeerConnection并释放媒体流
- 修正信令消息发送目标为
socket.id而非自定义uid - 处理
ExistingMembers事件,支持房间内已有用户时主动发起连接
修改后的客户端代码关键部分:
// 添加资源清理函数 const cleanupPeerConnection = () => { if (peerConnection) { peerConnection.close(); peerConnection = null; } if (remoteStream) { remoteStream.getTracks().forEach(track => track.stop()); remoteStream = null; remoteVideoRef.srcObject = null; remoteVideoRef.style.display = "none"; remoteVideoRef.classList.remove("remoteFrame"); } }; let remoteSocketId = null; // 存储对端socketId const configureSignaling = async () => { socket = await io.connect("http://localhost:4000"); socket.emit("join", { room, uid }); socket.on("MemberJoined", handleMemberJoined); socket.on("MessageFromPeer", handleMessageFromPeer); socket.on("MemberLeft", handleMemberLeft); // 新增:处理房间内已有用户的情况 socket.on("ExistingMembers", handleExistingMembers); // 新增:socket断开时清理资源 socket.on("disconnect", cleanupPeerConnection); }; const handleMemberLeft = async () => { cleanupPeerConnection(); remoteSocketId = null; }; // 新增:处理房间内已有用户 const handleExistingMembers = (members) => { if (members.length > 0) { // 向第一个已存在用户发起offer remoteSocketId = members[0].socketId; createOffer(remoteSocketId); } }; const handleMemberJoined = async (uid, socketId) => { remoteSocketId = socketId; createOffer(socketId); }; const handleMessageFromPeer = (m) => { const message = JSON.parse(m.text); if(message.type !== "candidate") { console.log('handleMessageFromPeer: ', message) } if (message.type === "offer") { createAnswer(message.offer); } if (message.type === "answer") { addAnswer(message.answer); } if (message.type === "candidate") { if (peerConnection && peerConnection.currentRemoteDescription) { peerConnection.addIceCandidate(message.candidate); } } }; let createOffer = async (targetSocketId) => { await createPeerConnection(targetSocketId); let offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); socket.emit( "sendMessageToPeer", { text: JSON.stringify({ type: "offer", offer: offer }) }, targetSocketId ); }; let createPeerConnection = async (targetSocketId) => { // 创建前先清理旧连接 cleanupPeerConnection(); peerConnection = new RTCPeerConnection(servers); remoteStream = new MediaStream(); remoteVideoRef.srcObject = remoteStream; remoteVideoRef.style.display = "block"; remoteVideoRef.classList.add("remoteFrame"); if (!localStream) { await createLocalStream(); } localStream.getTracks().forEach((track) => { peerConnection.addTrack(track, localStream); }); peerConnection.ontrack = (event) => { event.streams[0].getTracks().forEach((track) => { remoteStream.addTrack(track); }); }; peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.emit( "sendMessageToPeer", { text: JSON.stringify({ type: "candidate", candidate: event.candidate, }), }, targetSocketId ); } }; }; let createAnswer = async (offer) => { await createPeerConnection(); await peerConnection.setRemoteDescription(offer); const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); socket.emit( "sendMessageToPeer", { text: JSON.stringify({ type: "answer", answer: answer }) }, remoteSocketId ); }; // 修正addAnswer逻辑,去掉不必要的判断 let addAnswer = async (answer) => { try { await peerConnection.setRemoteDescription(answer); } catch (err) { console.error('设置远程描述失败:', err); } }; let onLogout = async () => { cleanupPeerConnection(); remoteSocketId = null; await socket.emit('onLeaveRoom', room); }; window.addEventListener("beforeunload", () => { cleanupPeerConnection();
相关产品推荐
相关产品推荐

