pin_drop当前位置:知识文库 ❯ 图文

后端通信:WebSocket实时通信 - 从入门到实践详解

教程简介

WebSocket是HTML5引入的全双工通信协议,允许浏览器和服务器之间建立持久连接,实现实时双向数据传输。与HTTP的请求-响应模式不同,WebSocket连接建立后,服务器可以主动向客户端推送数据,无需客户端轮询。这使得WebSocket成为实时聊天、在线协作、实时数据监控、游戏等场景的理想选择。

本教程将全面讲解WebSocket协议原理、建立连接、发送/接收消息、连接管理、心跳检测、重连策略、二进制数据处理、WebSocket与HTTP/2对比,以及聊天室实现。

核心概念

1. WebSocket协议

WebSocket协议由两部分组成:握手和数据传输。

握手过程

代码示例

客户端请求:
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务器响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

握手成功后,协议从HTTP切换为WebSocket,开始全双工通信。

2. WebSocket API

代码示例

// 创建连接
const ws = new WebSocket('ws://example.com/ws');
// 或使用安全连接
const wss = new WebSocket('wss://example.com/ws');

// 连接打开
ws.onopen = function(event) {
    console.log('连接已建立');
    ws.send('Hello Server!');
};

// 接收消息
ws.onmessage = function(event) {
    console.log('收到消息:', event.data);
};

// 连接关闭
ws.onclose = function(event) {
    console.log('连接关闭:', event.code, event.reason);
};

// 连接错误
ws.onerror = function(event) {
    console.error('连接错误');
};

// 主动关闭
ws.close(1000, '正常关闭');

3. WebSocket属性

属性 说明
readyState 连接状态
bufferedAmount 未发送的数据量
protocol 服务器选择的子协议
url WebSocket URL
extensions 服务器选择的扩展

readyState状态值

常量 说明
0 CONNECTING 正在连接
1 OPEN 连接已建立
2 CLOSING 正在关闭
3 CLOSED 已关闭

4. 关闭代码

代码 说明
1000 正常关闭
1001 端点离开
1002 协议错误
1003 不支持的数据类型
1005 无状态码(保留)
1006 异常关闭(保留)
1007 无效消息
1008 策略违规
1009 消息过大
1010 需要扩展
1011 内部错误
1012 服务重启
1013 稍后重试
4000-4999 自定义代码

5. 心跳检测

代码示例

class WebSocketWithHeartbeat {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.heartbeatInterval = 30000; // 30秒
        this.heartbeatTimer = null;
        this.reconnectTimer = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
    }

    connect() {
        this.ws = new WebSocket(this.url);

        this.ws.onopen = () => {
            console.log('连接已建立');
            this.reconnectAttempts = 0;
            this.startHeartbeat();
        };

        this.ws.onmessage = (event) => {
            if (event.data === 'pong') {
                // 心跳响应,连接正常
                return;
            }
            this.onMessage(event.data);
        };

        this.ws.onclose = (event) => {
            this.stopHeartbeat();
            if (event.code !== 1000) {
                this.reconnect();
            }
        };

        this.ws.onerror = () => {
            this.stopHeartbeat();
        };
    }

    startHeartbeat() {
        this.heartbeatTimer = setInterval(() => {
            if (this.ws.readyState === WebSocket.OPEN) {
                this.ws.send('ping');
            }
        }, this.heartbeatInterval);
    }

    stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
    }

    reconnect() {
        if (this.reconnectAttempts >= this.maxReconnectAttempts) {
            console.error('重连次数已达上限');
            return;
        }
        const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
        this.reconnectAttempts++;
        console.log(`${delay}ms后尝试第${this.reconnectAttempts}次重连`);
        this.reconnectTimer = setTimeout(() => this.connect(), delay);
    }

    onMessage(data) {
        // 子类覆盖
    }

    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(data);
        }
    }

    close() {
        this.stopHeartbeat();
        if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
        if (this.ws) this.ws.close(1000, '正常关闭');
    }
}

6. 二进制数据

代码示例

// 发送Blob
const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
ws.send(blob);

// 发送ArrayBuffer
const buffer = new ArrayBuffer(8);
ws.send(buffer);

// 接收二进制数据
ws.binaryType = 'blob'; // 或 'arraybuffer'
ws.onmessage = function(event) {
    if (event.data instanceof Blob) {
        // 处理Blob
    } else if (event.data instanceof ArrayBuffer) {
        // 处理ArrayBuffer
    }
};

7. WebSocket vs HTTP/2 SSE

特性 WebSocket SSE HTTP/2
方向 全双工 服务器→客户端 请求-响应
协议 ws/wss HTTP/HTTPS HTTP/2
数据格式 文本/二进制 文本 任意
自动重连 需手动实现 内置
浏览器支持 全版本 全版本 现代浏览器
适用场景 聊天、游戏 通知、推送 普通请求

代码示例

示例1:WebSocket连接管理器

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket连接管理器</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 900px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
        .status-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; padding: 12px; background: #f8f9fa; border-radius: 8px; }
        .status-dot { width: 12px; height: 12px; border-radius: 50%; }
        .status-connecting { background: #ff9800; animation: pulse 1s infinite; }
        .status-open { background: #34a853; }
        .status-closing { background: #ff9800; }
        .status-closed { background: #ea4335; }
        @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
        .url-input { width: 100%; padding: 10px 14px; border: 1px solid #dadce0; border-radius: 8px; font-size: 14px; margin-bottom: 12px; }
        .btn-row { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
        button { padding: 8px 16px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 13px; }
        .btn-connect { background: #34a853; color: #fff; }
        .btn-disconnect { background: #ea4335; color: #fff; }
        .btn-send { background: #1a73e8; color: #fff; }
        .btn-ping { background: #ff9800; color: #fff; }
        .message-input { width: 100%; padding: 10px 14px; border: 1px solid #dadce0; border-radius: 8px; font-size: 14px; margin-bottom: 8px; }
        .log-area { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 12px; line-height: 1.8; max-height: 350px; overflow-y: auto; white-space: pre-wrap; }
        .log-sent { color: #64b5f6; }
        .log-received { color: #81c784; }
        .log-system { color: #ffb74d; }
        .log-error { color: #e57373; }
    </style>
</head>
<body>
    <div class="container">
        <h1>WebSocket连接管理器</h1>
        <div class="card">
            <h2>连接状态</h2>
            <div class="status-bar">
                <div class="status-dot status-closed" id="statusDot"></div>
                <span id="statusText">未连接</span>
                <span style="margin-left:auto;font-size:13px;color:#888;">readyState: <b id="readyState">3 (CLOSED)</b></span>
            </div>
            <input type="text" class="url-input" id="urlInput" value="wss://echo.websocket.org" placeholder="WebSocket URL">
            <div class="btn-row">
                <button class="btn-connect" onclick="connect()">连接</button>
                <button class="btn-disconnect" onclick="disconnect()">断开</button>
                <button class="btn-ping" onclick="sendPing()">发送Ping</button>
            </div>
        </div>
        <div class="card">
            <h2>发送消息</h2>
            <input type="text" class="message-input" id="msgInput" placeholder="输入消息内容" value="Hello WebSocket!">
            <div class="btn-row">
                <button class="btn-send" onclick="sendMessage()">发送文本</button>
                <button class="btn-send" onclick="sendJSON()">发送JSON</button>
            </div>
        </div>
        <div class="card">
            <h2>通信日志</h2>
            <div class="log-area" id="logArea">等待连接...</div>
        </div>
    </div>
    <script>
        let ws = null;
        const stateNames = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];

        function updateStatus() {
            const state = ws ? ws.readyState : 3;
            const dot = document.getElementById('statusDot');
            const classes = ['status-connecting', 'status-open', 'status-closing', 'status-closed'];
            dot.className = 'status-dot ' + classes[state];
            document.getElementById('statusText').textContent = stateNames[state];
            document.getElementById('readyState').textContent = `${state} (${stateNames[state]})`;
        }

        function log(msg, type = 'system') {
            const time = new Date().toLocaleTimeString();
            const area = document.getElementById('logArea');
            area.innerHTML += `<span class="log-${type}">[${time}] ${msg}</span>\n`;
            area.scrollTop = area.scrollHeight;
        }

        function connect() {
            if (ws && ws.readyState < 2) {
                log('已有活跃连接', 'error');
                return;
            }
            const url = document.getElementById('urlInput').value;
            log(`正在连接: ${url}`);
            ws = new WebSocket(url);

            ws.onopen = () => {
                log('连接已建立', 'system');
                updateStatus();
            };

            ws.onmessage = (event) => {
                log(`收到消息: ${event.data}`, 'received');
            };

            ws.onclose = (event) => {
                log(`连接关闭: code=${event.code}, reason=${event.reason || '无'}`, event.code === 1000 ? 'system' : 'error');
                updateStatus();
            };

            ws.onerror = () => {
                log('连接错误', 'error');
                updateStatus();
            };

            updateStatus();
        }

        function disconnect() {
            if (ws) {
                ws.close(1000, '用户主动关闭');
                log('正在关闭连接...', 'system');
            }
        }

        function sendMessage() {
            const msg = document.getElementById('msgInput').value;
            if (ws && ws.readyState === 1) {
                ws.send(msg);
                log(`发送消息: ${msg}`, 'sent');
            } else {
                log('未连接,无法发送', 'error');
            }
        }

        function sendJSON() {
            const data = { type: 'message', content: document.getElementById('msgInput').value, timestamp: Date.now() };
            if (ws && ws.readyState === 1) {
                ws.send(JSON.stringify(data));
                log(`发送JSON: ${JSON.stringify(data)}`, 'sent');
            } else {
                log('未连接,无法发送', 'error');
            }
        }

        function sendPing() {
            if (ws && ws.readyState === 1) {
                ws.send('ping');
                log('发送心跳: ping', 'sent');
            }
        }
    </script>
</body>
</html>

示例2:模拟聊天室

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>模拟聊天室</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 800px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .chat-container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .chat-header { padding: 16px 20px; background: #1a73e8; color: #fff; display: flex; align-items: center; justify-content: space-between; }
        .chat-header h2 { font-size: 18px; }
        .online-count { font-size: 13px; opacity: 0.8; }
        .chat-messages { height: 400px; overflow-y: auto; padding: 16px; background: #f8f9fa; }
        .message { margin-bottom: 12px; display: flex; gap: 10px; }
        .message.self { flex-direction: row-reverse; }
        .msg-avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 14px; flex-shrink: 0; }
        .msg-content { max-width: 70%; }
        .msg-name { font-size: 12px; color: #888; margin-bottom: 2px; }
        .msg-text { padding: 10px 14px; border-radius: 12px; font-size: 14px; line-height: 1.5; word-break: break-word; }
        .message:not(.self) .msg-text { background: #fff; border: 1px solid #e8eaed; border-top-left-radius: 4px; }
        .message.self .msg-text { background: #1a73e8; color: #fff; border-top-right-radius: 4px; }
        .message.self .msg-name { text-align: right; }
        .msg-time { font-size: 11px; color: #aaa; margin-top: 2px; }
        .system-msg { text-align: center; font-size: 12px; color: #888; margin: 8px 0; }
        .chat-input { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid #e8eaed; background: #fff; }
        .chat-input input { flex: 1; padding: 10px 14px; border: 1px solid #dadce0; border-radius: 8px; font-size: 14px; }
        .chat-input input:focus { outline: none; border-color: #1a73e8; }
        .chat-input button { padding: 10px 20px; background: #1a73e8; color: #fff; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
    </style>
</head>
<body>
    <div class="container">
        <h1>模拟聊天室</h1>
        <div class="chat-container">
            <div class="chat-header">
                <h2>WebSocket聊天室</h2>
                <span class="online-count">在线: <b id="onlineCount">3</b>人</span>
            </div>
            <div class="chat-messages" id="chatMessages">
                <div class="system-msg">欢迎来到聊天室!</div>
            </div>
            <div class="chat-input">
                <input type="text" id="msgInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendChat()">
                <button onclick="sendChat()">发送</button>
            </div>
        </div>
    </div>
    <script>
        const users = [
            { name: '我', color: '#1a73e8' },
            { name: '小明', color: '#34a853' },
            { name: '小红', color: '#ea4335' },
            { name: '小刚', color: '#ff9800' }
        ];

        const botMessages = [
            '你好!今天天气不错',
            '有人用过WebSocket吗?',
            '实时通信真的很方便',
            '我正在学习前端开发',
            '这个聊天室是用模拟数据实现的',
            'WebSocket比轮询效率高多了',
            '大家觉得GraphQL怎么样?',
            'HTTP/3什么时候能普及呢?',
            '前端技术发展真快',
            '推荐一下好用的代码编辑器'
        ];

        function addMessage(userIndex, text) {
            const user = users[userIndex];
            const time = new Date().toLocaleTimeString();
            const messages = document.getElementById('chatMessages');
            const isSelf = userIndex === 0;

            messages.innerHTML += `
                <div class="message ${isSelf ? 'self' : ''}">
                    <div class="msg-avatar" style="background:${user.color}">${user.name[0]}</div>
                    <div class="msg-content">
                        <div class="msg-name">${user.name} · ${time}</div>
                        <div class="msg-text">${text}</div>
                    </div>
                </div>
            `;
            messages.scrollTop = messages.scrollHeight;
        }

        function addSystemMsg(text) {
            document.getElementById('chatMessages').innerHTML += `<div class="system-msg">${text}</div>`;
        }

        function sendChat() {
            const input = document.getElementById('msgInput');
            const text = input.value.trim();
            if (!text) return;
            addMessage(0, text);
            input.value = '';

            // 模拟其他用户回复
            setTimeout(() => {
                const userIndex = 1 + Math.floor(Math.random() * 3);
                const reply = botMessages[Math.floor(Math.random() * botMessages.length)];
                addMessage(userIndex, reply);
            }, 1000 + Math.random() * 2000);
        }

        // 模拟其他用户的消息
        function simulateBot() {
            const userIndex = 1 + Math.floor(Math.random() * 3);
            const msg = botMessages[Math.floor(Math.random() * botMessages.length)];
            addMessage(userIndex, msg);
            setTimeout(simulateBot, 8000 + Math.random() * 15000);
        }

        setTimeout(simulateBot, 5000);

        // 模拟用户上下线
        setTimeout(() => addSystemMsg('小李 加入了聊天室'), 3000);
        setTimeout(() => addSystemMsg('小王 离开了聊天室'), 20000);
    </script>
</body>
</html>

示例3:重连策略演示

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>重连策略演示</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 900px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
        .strategy-btns { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
        .strategy-btn { padding: 10px 18px; border: 2px solid #e8eaed; border-radius: 8px; background: #fff; cursor: pointer; font-weight: 600; font-size: 13px; transition: all 0.2s; }
        .strategy-btn.active { background: #1a73e8; color: #fff; border-color: #1a73e8; }
        button { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
        .btn-primary { background: #1a73e8; color: #fff; }
        .btn-danger { background: #ea4335; color: #fff; }
        .timeline { padding: 16px; background: #f8f9fa; border-radius: 8px; min-height: 200px; }
        .timeline-item { padding: 8px 12px; margin-bottom: 6px; border-radius: 6px; font-size: 13px; font-family: 'Consolas', monospace; }
        .item-connect { background: #e3f2fd; color: #1565c0; }
        .item-disconnect { background: #ffebee; color: #c62828; }
        .item-reconnect { background: #fff3e0; color: #ef6c00; }
        .item-success { background: #e8f5e9; color: #2e7d32; }
        .delay-chart { display: flex; align-items: flex-end; gap: 4px; height: 100px; margin-top: 16px; }
        .delay-bar { width: 40px; background: #1a73e8; border-radius: 4px 4px 0 0; transition: height 0.3s; display: flex; align-items: flex-start; justify-content: center; font-size: 10px; color: #fff; padding-top: 4px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>重连策略演示</h1>
        <div class="card">
            <h2>选择重连策略</h2>
            <div class="strategy-btns">
                <div class="strategy-btn active" onclick="selectStrategy('fixed')">固定间隔</div>
                <div class="strategy-btn" onclick="selectStrategy('linear')">线性退避</div>
                <div class="strategy-btn" onclick="selectStrategy('exponential')">指数退避</div>
            </div>
            <div style="display:flex;gap:10px;margin-bottom:16px;">
                <button class="btn-primary" onclick="simulateDisconnect()">模拟断开</button>
                <button class="btn-danger" onclick="resetSimulation()">重置</button>
            </div>
        </div>
        <div class="card">
            <h2>重连时间线</h2>
            <div class="timeline" id="timeline"></div>
            <h3 style="font-size:14px;margin-top:16px;color:#555;">重连延迟图</h3>
            <div class="delay-chart" id="delayChart"></div>
        </div>
    </div>
    <script>
        let currentStrategy = 'fixed';
        let attempts = [];
        let maxAttempts = 8;

        function selectStrategy(strategy) {
            currentStrategy = strategy;
            document.querySelectorAll('.strategy-btn').forEach(b => b.classList.remove('active'));
            event.currentTarget.classList.add('active');
            resetSimulation();
        }

        function getDelay(attempt) {
            switch (currentStrategy) {
                case 'fixed': return 2000;
                case 'linear': return Math.min(2000 * attempt, 30000);
                case 'exponential': return Math.min(1000 * Math.pow(2, attempt - 1), 30000);
            }
        }

        function getStrategyName() {
            return { fixed: '固定间隔', linear: '线性退避', exponential: '指数退避' }[currentStrategy];
        }

        function addTimelineItem(text, type) {
            const time = new Date().toLocaleTimeString();
            document.getElementById('timeline').innerHTML += `<div class="timeline-item item-${type}">[${time}] ${text}</div>`;
        }

        function renderDelayChart() {
            document.getElementById('delayChart').innerHTML = attempts.map((delay, i) => {
                const height = Math.min((delay / 30000) * 100, 100);
                return `<div class="delay-bar" style="height:${Math.max(height, 5)}%">${(delay/1000).toFixed(1)}s</div>`;
            }).join('');
        }

        async function simulateDisconnect() {
            attempts = [];
            document.getElementById('timeline').innerHTML = '';
            addTimelineItem('连接断开!开始重连...', 'disconnect');
            addTimelineItem(`策略: ${getStrategyName()}`, 'reconnect');

            for (let i = 1; i <= maxAttempts; i++) {
                const delay = getDelay(i);
                attempts.push(delay);
                addTimelineItem(`第${i}次重连,等待 ${(delay/1000).toFixed(1)}s...`, 'reconnect');
                renderDelayChart();

                await new Promise(resolve => setTimeout(resolve, Math.min(delay, 3000))); // 加速演示

                if (i === maxAttempts || Math.random() > 0.6) {
                    addTimelineItem(`第${i}次重连成功!`, 'success');
                    break;
                } else {
                    addTimelineItem(`第${i}次重连失败`, 'disconnect');
                }
            }
        }

        function resetSimulation() {
            attempts = [];
            document.getElementById('timeline').innerHTML = '<div class="timeline-item item-reconnect">点击"模拟断开"开始演示</div>';
            document.getElementById('delayChart').innerHTML = '';
        }

        resetSimulation();
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge 说明
WebSocket 全版本 全版本 全版本 全版本 所有现代浏览器
二进制数据 全版本 全版本 全版本 全版本 Blob/ArrayBuffer
WebSocket API 全版本 全版本 全版本 全版本 标准API
子协议 全版本 全版本 全版本 全版本 protocol参数
bufferedAmount 全版本 全版本 全版本 全版本 流量控制

注意事项与最佳实践

1. 连接管理

  • 实现自动重连机制(指数退避)

  • 设置最大重连次数

  • 重连时使用新的WebSocket实例

  • 关闭时使用正确的关闭代码

2. 心跳检测

  • 定期发送ping消息

  • 超时未收到pong则认为连接断开

  • 心跳间隔建议30秒

3. 消息格式

  • 使用JSON格式传递结构化数据

  • 包含消息类型字段便于路由

  • 添加消息ID用于确认和去重

4. 安全考虑

  • 使用wss://而非ws://

  • 验证消息格式和内容

  • 实现消息频率限制

  • 使用Token认证

代码规范示例

完整的WebSocket管理类

代码示例

class WebSocketManager {
    constructor(url, options = {}) {
        this.url = url;
        this.options = {
            heartbeatInterval: 30000,
            maxReconnectAttempts: 5,
            reconnectBaseDelay: 1000,
            ...options
        };
        this.ws = null;
        this.reconnectAttempts = 0;
        this.heartbeatTimer = null;
        this.reconnectTimer = null;
        this.listeners = new Map();
    }

    connect() {
        this.ws = new WebSocket(this.url);
        this.ws.onopen = (e) => this._onOpen(e);
        this.ws.onmessage = (e) => this._onMessage(e);
        this.ws.onclose = (e) => this._onClose(e);
        this.ws.onerror = (e) => this._onError(e);
    }

    _onOpen(e) {
        this.reconnectAttempts = 0;
        this._startHeartbeat();
        this._emit('open', e);
    }

    _onMessage(e) {
        const data = e.data;
        if (data === 'pong') return;
        try {
            const parsed = JSON.parse(data);
            this._emit('message', parsed);
        } catch {
            this._emit('message', data);
        }
    }

    _onClose(e) {
        this._stopHeartbeat();
        this._emit('close', e);
        if (e.code !== 1000) this._reconnect();
    }

    _onError(e) {
        this._emit('error', e);
    }

    _startHeartbeat() {
        this.heartbeatTimer = setInterval(() => {
            if (this.ws?.readyState === WebSocket.OPEN) {
                this.ws.send('ping');
            }
        }, this.options.heartbeatInterval);
    }

    _stopHeartbeat() {
        clearInterval(this.heartbeatTimer);
    }

    _reconnect() {
        if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
            this._emit('reconnect_failed');
            return;
        }
        const delay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
        this.reconnectAttempts++;
        this.reconnectTimer = setTimeout(() => this.connect(), delay);
    }

    send(data) {
        if (this.ws?.readyState === WebSocket.OPEN) {
            this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
        }
    }

    on(event, callback) {
        if (!this.listeners.has(event)) this.listeners.set(event, []);
        this.listeners.get(event).push(callback);
    }

    _emit(event, data) {
        (this.listeners.get(event) || []).forEach(cb => cb(data));
    }

    close() {
        this._stopHeartbeat();
        clearTimeout(this.reconnectTimer);
        this.ws?.close(1000, 'Normal closure');
    }
}

常见问题与解决方案

问题1:连接频繁断开

解决方案:实现心跳检测和自动重连,使用指数退避策略。

问题2:消息丢失

解决方案:实现消息确认机制,为每条消息分配ID,未确认的消息重发。

问题3:跨域问题

解决方案:WebSocket不受同源策略限制,但服务器需要配置允许的Origin。

问题4:代理服务器不支持WebSocket

解决方案:使用HTTP长轮询作为降级方案,或使用Socket.io等库自动降级。

总结

WebSocket是实现实时通信的核心技术:

  1. 全双工通信:服务器可主动推送数据,无需轮询

  2. WebSocket API:onopen/onmessage/onclose/onerror事件驱动

  3. 连接管理:心跳检测、自动重连、指数退避策略

  4. 消息格式:文本和二进制数据,推荐JSON格式

  5. 关闭代码:使用正确的关闭代码表示关闭原因

  6. 安全考虑:使用wss://、消息验证、频率限制

  7. 降级方案:HTTP长轮询、SSE作为备选

掌握WebSocket的使用和最佳实践,能够让你构建高性能的实时Web应用。

常见问题

什么是WebSocket实时通信?

WebSocket实时通信是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习WebSocket实时通信的实际应用?

教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。

WebSocket实时通信有哪些注意事项?

常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。

WebSocket实时通信适合初学者吗?

本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。

WebSocket实时通信的核心要点是什么?

核心要点包括教程简介等内容,建议按教程顺序逐步学习。

标签: Promise OAuth Canvas 响应码 网络安全 键盘输入 async await 内存管理

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 后端通信:GraphQL基础 - 从入门到实践详解 下一篇: 后端通信:SSE服务器推送 - 从入门到实践详解

poll相关推荐