WebRTC场景WebSocket调用send方法报CONNECTING状态报错排查
问题诊断
控制台抛出的Uncaught DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state错误触发逻辑非常明确:
WebSocket实例初始化后,需要和服务端完成TCP握手、TLS校验、WebSocket协议升级全流程,这个过程中连接处于CONNECTING状态,只有连接正式建立、触发onopen事件后,连接状态切换为OPEN,才能正常调用send()方法传输数据。
现有代码在页面加载时就初始化WebSocket,但用户点击按钮触发sendUsername发送信令时,WebSocket连接还没完成建立,直接调用send就会抛出上述错误,导致后续房间创建、信令交互全流程中断,和端口、地址配置没有关系。
另外代码里还存在几处WebRTC旧API兼容问题、信令发送顺序问题,会在后续运行中触发其他异常。
修复步骤
- 为WebSocket增加待发送消息队列,连接未就绪时先把消息存入队列,等连接成功后批量发送,从逻辑上杜绝CONNECTING状态下调用send的问题
- 补全WebSocket的
onopen/onerror/onclose事件监听,方便排查连接异常 - 修正WebRTC旧API写法,调整SDP offer发送顺序,适配现代浏览器标准
- 可在交互层增加控制:WebSocket连接就绪前禁用发送、呼叫按钮,避免用户操作过早触发异常
修复后完整代码
// 初始化WebSocket实例与待发消息队列 const webSocket = new WebSocket("wss://domain:443") const messageQueue = [] let username let localStream let peerConn let isVideo = true // WebSocket连接建立成功后,清空积压的待发消息 webSocket.onopen = () => { while (messageQueue.length) { const pendingData = messageQueue.shift() webSocket.send(JSON.stringify(pendingData)) } // 连接成功后再放开用户名提交、呼叫按钮的可点击状态 document.getElementById("join-btn").disabled = false } webSocket.onerror = (err) => { console.error("WebSocket连接异常:", err) } webSocket.onclose = () => { console.log("WebSocket连接已断开") } webSocket.onmessage = (event) => { handleSignallingData(JSON.parse(event.data)) } function handleSignallingData(data) { switch (data.type) { case "answer": peerConn.setRemoteDescription(new RTCSessionDescription(data.answer)) break case "candidate": peerConn.addIceCandidate(new RTCIceCandidate(data.candidate)) } } function sendUsername() { username = document.getElementById("username-input").value sendData({ type: "store_user" }) } function sendData(data) { data.username = username // 连接已就绪直接发送,未就绪存入队列 if (webSocket.readyState === WebSocket.OPEN) { webSocket.send(JSON.stringify(data)) } else { messageQueue.push(data) } } async function startCall() { document.getElementById("video-call-div").style.display = "inline" try { // 替换为标准Promise版本的媒体采集API localStream = await navigator.mediaDevices.getUserMedia({ video: { frameRate: 24, width: { min: 480, ideal: 720, max: 1280 }, aspectRatio: 1.33333 }, audio: true }) document.getElementById("local-video").srcObject = localStream const configuration = { iceServers: [ { urls: [ "stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302" ] } ] } peerConn = new RTCPeerConnection(configuration) // 替换已废弃的addStream方法,用addTrack添加媒体轨 localStream.getTracks().forEach(track => { peerConn.addTrack(track, localStream) }) // 替换已废弃的onaddstream事件,用ontrack监听远端媒体流 peerConn.ontrack = (e) => { document.getElementById("remote-video").srcObject = e.streams[0] } peerConn.onicecandidate = (e) => { if (!e.candidate) return sendData({ type: "store_candidate", candidate: e.candidate }) } await createAndSendOffer() } catch (error) { console.log("媒体采集/呼叫流程异常:", error) } } async function createAndSendOffer() { try { const offer = await peerConn.createOffer() // 先设置本地SDP,再发送offer信令,修正原有顺序问题 await peerConn.setLocalDescription(offer) sendData({ type: "store_offer", offer: offer }) } catch (error) { console.log("创建offer异常:", error) } } function muteVideo() { isVideo = !isVideo localStream.getVideoTracks()[0].enabled = isVideo }
额外说明
- 上述代码默认你在HTML里给加入房间/提交用户名的按钮加了
id="join-btn"、初始状态加了disabled属性,不需要的话可以删掉对应控制逻辑 - 如果要做断线重连,只需要在
onclose回调里加重新初始化WebSocket的逻辑即可,消息队列的逻辑可以直接复用 - 信令服务端如果有心跳要求,直接在
onopen回调里启动定时心跳发送即可,不会出现状态错误
内容的提问来源于stack exchange,提问作者MrCipher
相关产品推荐
相关产品推荐

