WebSocket是HTML5提供的一种在单个TCP连接上进行全双工通信的协议。它允许服务器主动向客户端推送数据,实现了真正的实时通信。
东巴文(db-w.cn) 认为:WebSocket是现代实时Web应用的基石,让服务器推送成为可能。
| 特点 | 说明 |
|---|---|
| 全双工通信 | 客户端和服务器可以同时发送和接收数据 |
| 实时性强 | 服务器可以主动推送数据,无需客户端轮询 |
| 开销小 | 建立连接后,数据帧头部开销很小 |
| 保持连接 | 连接保持打开状态,避免频繁建立连接 |
| 跨域支持 | 支持跨域通信 |
// HTTP: 请求-响应模式
// 客户端 -> 服务器: 请求
// 服务器 -> 客户端: 响应
// 每次请求都需要建立新的连接
// WebSocket: 全双工通信
// 客户端 <-> 服务器: 双向通信
// 连接保持打开,双方都可以主动发送数据
const comparison = {
HTTP: {
通信模式: '半双工',
连接方式: '短连接',
实时性: '差(需要轮询)',
开销: '大(每次请求都有完整HTTP头)',
适用场景: '传统Web应用'
},
WebSocket: {
通信模式: '全双工',
连接方式: '长连接',
实时性: '好(服务器主动推送)',
开销: '小(数据帧头部仅2-10字节)',
适用场景: '实时应用、游戏、聊天'
}
};
<!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>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.status {
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
}
.status.connected {
background: #d4edda;
color: #155724;
}
.status.disconnected {
background: #f8d7da;
color: #721c24;
}
.status.connecting {
background: #fff3cd;
color: #856404;
}
.messages {
height: 400px;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
overflow-y: auto;
background: #f9f9f9;
}
.message {
padding: 8px;
margin: 5px 0;
border-radius: 5px;
}
.message.sent {
background: #007bff;
color: white;
text-align: right;
}
.message.received {
background: #e9ecef;
text-align: left;
}
.message.system {
background: #ffc107;
text-align: center;
font-weight: bold;
}
.input-area {
display: flex;
gap: 10px;
margin-top: 20px;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>WebSocket基本连接</h1>
<div class="status disconnected" id="status">
未连接
</div>
<div class="messages" id="messages"></div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="输入消息..." disabled>
<button id="sendBtn" disabled>发送</button>
<button id="connectBtn">连接</button>
<button id="disconnectBtn" disabled>断开</button>
</div>
<script>
let ws = null;
const status = document.getElementById('status');
const messages = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
// 连接WebSocket
function connect() {
updateStatus('connecting', '连接中...');
// 创建WebSocket连接
// 使用公共WebSocket测试服务器
ws = new WebSocket('wss://echo.websocket.org');
// 连接打开
ws.addEventListener('open', function(event) {
updateStatus('connected', '已连接');
addMessage('系统', '连接成功!');
messageInput.disabled = false;
sendBtn.disabled = false;
connectBtn.disabled = true;
disconnectBtn.disabled = false;
});
// 接收消息
ws.addEventListener('message', function(event) {
addMessage('服务器', event.data);
});
// 连接关闭
ws.addEventListener('close', function(event) {
updateStatus('disconnected', '已断开');
addMessage('系统', '连接已关闭');
messageInput.disabled = true;
sendBtn.disabled = true;
connectBtn.disabled = false;
disconnectBtn.disabled = true;
});
// 连接错误
ws.addEventListener('error', function(event) {
updateStatus('disconnected', '连接错误');
addMessage('系统', '连接发生错误');
});
}
// 断开连接
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
}
// 发送消息
function sendMessage() {
const message = messageInput.value.trim();
if (message && ws && ws.readyState === WebSocket.OPEN) {
ws.send(message);
addMessage('我', message);
messageInput.value = '';
}
}
// 更新状态
function updateStatus(state, text) {
status.className = 'status ' + state;
status.textContent = text;
}
// 添加消息
function addMessage(sender, text) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
if (sender === '系统') {
messageDiv.classList.add('system');
messageDiv.textContent = text;
} else if (sender === '我') {
messageDiv.classList.add('sent');
messageDiv.textContent = text;
} else {
messageDiv.classList.add('received');
messageDiv.textContent = `${sender}: ${text}`;
}
messages.appendChild(messageDiv);
messages.scrollTop = messages.scrollHeight;
}
// 事件绑定
connectBtn.addEventListener('click', connect);
disconnectBtn.addEventListener('click', disconnect);
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>
// WebSocket连接状态
const WebSocketState = {
CONNECTING: 0, // 正在连接
OPEN: 1, // 已连接
CLOSING: 2, // 正在关闭
CLOSED: 3 // 已关闭
};
// 检查连接状态
function checkConnection(ws) {
switch (ws.readyState) {
case WebSocket.CONNECTING:
console.log('正在连接...');
break;
case WebSocket.OPEN:
console.log('连接已打开');
break;
case WebSocket.CLOSING:
console.log('连接正在关闭...');
break;
case WebSocket.CLOSED:
console.log('连接已关闭');
break;
}
}
// WebSocket其他属性
const wsProperties = {
url: 'WebSocket服务器的URL',
protocol: '服务器选择的子协议',
bufferedAmount: '未发送到服务器的字节数',
binaryType: '二进制数据类型(blob|arraybuffer)'
};
<!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>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.section {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
h3 {
margin-top: 0;
}
textarea {
width: 100%;
height: 100px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-family: monospace;
}
button {
padding: 10px 20px;
margin: 10px 5px 10px 0;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
.log {
background: #f9f9f9;
padding: 10px;
border-radius: 5px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 14px;
}
.log-entry {
padding: 5px 0;
border-bottom: 1px solid #eee;
}
.log-entry.sent {
color: #007bff;
}
.log-entry.received {
color: #28a745;
}
.log-entry.error {
color: #dc3545;
}
</style>
</head>
<body>
<h1>WebSocket发送消息</h1>
<div class="section">
<h3>发送文本消息</h3>
<textarea id="textInput" placeholder="输入文本消息..."></textarea>
<button onclick="sendText()">发送文本</button>
</div>
<div class="section">
<h3>发送JSON数据</h3>
<textarea id="jsonInput">{
"type": "message",
"content": "Hello WebSocket!",
"timestamp": "2024-01-01T00:00:00Z"
}</textarea>
<button onclick="sendJSON()">发送JSON</button>
</div>
<div class="section">
<h3>发送二进制数据</h3>
<input type="file" id="fileInput">
<button onclick="sendBinary()">发送文件</button>
<button onclick="sendArrayBuffer()">发送ArrayBuffer</button>
</div>
<div class="section">
<h3>消息日志</h3>
<div class="log" id="log"></div>
</div>
<script>
let ws = null;
// 连接WebSocket
function connect() {
ws = new WebSocket('wss://echo.websocket.org');
ws.binaryType = 'arraybuffer'; // 设置二进制类型
ws.addEventListener('open', function() {
addLog('系统', '连接成功');
});
ws.addEventListener('message', function(event) {
if (event.data instanceof ArrayBuffer) {
addLog('接收', `二进制数据: ${event.data.byteLength} 字节`);
} else {
addLog('接收', event.data);
}
});
ws.addEventListener('error', function(error) {
addLog('错误', '连接错误', 'error');
});
ws.addEventListener('close', function() {
addLog('系统', '连接关闭');
});
}
// 发送文本
function sendText() {
const text = document.getElementById('textInput').value;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(text);
addLog('发送', text, 'sent');
}
}
// 发送JSON
function sendJSON() {
const json = document.getElementById('jsonInput').value;
if (ws && ws.readyState === WebSocket.OPEN) {
try {
const data = JSON.parse(json);
ws.send(JSON.stringify(data));
addLog('发送', JSON.stringify(data), 'sent');
} catch (e) {
addLog('错误', 'JSON格式错误', 'error');
}
}
}
// 发送二进制文件
function sendBinary() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (file && ws && ws.readyState === WebSocket.OPEN) {
const reader = new FileReader();
reader.onload = function(e) {
ws.send(e.target.result);
addLog('发送', `文件: ${file.name} (${file.size} 字节)`, 'sent');
};
reader.readAsArrayBuffer(file);
}
}
// 发送ArrayBuffer
function sendArrayBuffer() {
if (ws && ws.readyState === WebSocket.OPEN) {
const buffer = new ArrayBuffer(10);
const view = new Uint8Array(buffer);
for (let i = 0; i < 10; i++) {
view[i] = i;
}
ws.send(buffer);
addLog('发送', 'ArrayBuffer: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', 'sent');
}
}
// 添加日志
function addLog(type, message, className = '') {
const log = document.getElementById('log');
const entry = document.createElement('div');
entry.className = 'log-entry ' + className;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${type}: ${message}`;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
// 初始化连接
connect();
</script>
</body>
</html>
// WebSocket消息处理
ws.addEventListener('message', function(event) {
// 文本消息
if (typeof event.data === 'string') {
console.log('文本消息:', event.data);
// 尝试解析JSON
try {
const data = JSON.parse(event.data);
console.log('JSON数据:', data);
} catch (e) {
// 不是JSON格式
}
}
// 二进制消息
else if (event.data instanceof Blob) {
console.log('Blob数据:', event.data);
// 转换为文本
const reader = new FileReader();
reader.onload = function(e) {
console.log('Blob转文本:', e.target.result);
};
reader.readAsText(event.data);
}
// ArrayBuffer消息
else if (event.data instanceof ArrayBuffer) {
console.log('ArrayBuffer数据:', event.data);
// 解析数据
const view = new Uint8Array(event.data);
console.log('字节数组:', view);
}
});
class WebSocketManager {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.heartbeatInterval = null;
this.listeners = {};
}
// 连接
connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.url);
this.ws.addEventListener('open', () => {
console.log('连接成功');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.emit('open');
resolve();
});
this.ws.addEventListener('message', (event) => {
this.handleMessage(event.data);
});
this.ws.addEventListener('close', (event) => {
console.log('连接关闭', event.code, event.reason);
this.stopHeartbeat();
this.emit('close', event);
// 自动重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnect();
}
});
this.ws.addEventListener('error', (error) => {
console.error('连接错误', error);
this.emit('error', error);
reject(error);
});
});
}
// 重连
reconnect() {
this.reconnectAttempts++;
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => {
this.connect().catch(err => {
console.error('重连失败', err);
});
}, this.reconnectDelay * this.reconnectAttempts);
}
// 发送消息
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const message = typeof data === 'string' ? data : JSON.stringify(data);
this.ws.send(message);
return true;
}
return false;
}
// 处理消息
handleMessage(data) {
try {
const message = JSON.parse(data);
this.emit('message', message);
} catch (e) {
this.emit('message', data);
}
}
// 心跳机制
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // 每30秒发送一次心跳
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
// 关闭连接
close() {
this.stopHeartbeat();
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// 事件监听
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
// 触发事件
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
// 获取状态
getState() {
if (!this.ws) return 'CLOSED';
const states = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
return states[this.ws.readyState];
}
}
// 使用示例
const wsManager = new WebSocketManager('wss://echo.websocket.org');
wsManager.on('open', () => {
console.log('WebSocket已连接');
});
wsManager.on('message', (data) => {
console.log('收到消息:', data);
});
wsManager.on('close', () => {
console.log('WebSocket已关闭');
});
wsManager.on('error', (error) => {
console.error('WebSocket错误:', error);
});
wsManager.connect();
<!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>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.status-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin: 20px 0;
}
.status-item {
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
text-align: center;
}
.status-value {
font-size: 32px;
font-weight: bold;
color: #007bff;
}
.status-label {
color: #666;
margin-top: 5px;
}
.log {
background: #f9f9f9;
padding: 15px;
border-radius: 8px;
max-height: 400px;
overflow-y: auto;
font-family: monospace;
}
.log-entry {
padding: 5px 0;
border-bottom: 1px solid #eee;
}
.ping {
color: #ffc107;
}
.pong {
color: #28a745;
}
.message {
color: #007bff;
}
button {
padding: 10px 20px;
margin: 10px 5px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<h1>WebSocket心跳机制</h1>
<div class="status-panel">
<div class="status-item">
<div class="status-value" id="connectionStatus">未连接</div>
<div class="status-label">连接状态</div>
</div>
<div class="status-item">
<div class="status-value" id="pingCount">0</div>
<div class="status-label">发送心跳</div>
</div>
<div class="status-item">
<div class="status-value" id="pongCount">0</div>
<div class="status-label">收到响应</div>
</div>
</div>
<div>
<button onclick="connect()">连接</button>
<button onclick="disconnect()">断开</button>
<button onclick="sendMessage()">发送消息</button>
</div>
<h3>心跳日志</h3>
<div class="log" id="log"></div>
<script>
let ws = null;
let heartbeatTimer = null;
let pingCount = 0;
let pongCount = 0;
// 连接
function connect() {
ws = new WebSocket('wss://echo.websocket.org');
ws.addEventListener('open', function() {
updateStatus('已连接');
addLog('系统', '连接成功');
startHeartbeat();
});
ws.addEventListener('message', function(event) {
const data = event.data;
// 检查是否是心跳响应
if (data === 'pong' || data === 'ping') {
pongCount++;
document.getElementById('pongCount').textContent = pongCount;
addLog('PONG', '收到心跳响应', 'pong');
} else {
addLog('消息', data, 'message');
}
});
ws.addEventListener('close', function() {
updateStatus('已断开');
addLog('系统', '连接关闭');
stopHeartbeat();
});
ws.addEventListener('error', function() {
updateStatus('错误');
addLog('错误', '连接错误');
});
}
// 断开
function disconnect() {
stopHeartbeat();
if (ws) {
ws.close();
ws = null;
}
}
// 发送消息
function sendMessage() {
if (ws && ws.readyState === WebSocket.OPEN) {
const message = 'Hello WebSocket!';
ws.send(message);
addLog('发送', message, 'message');
}
}
// 启动心跳
function startHeartbeat() {
// 每30秒发送一次心跳
heartbeatTimer = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send('ping');
pingCount++;
document.getElementById('pingCount').textContent = pingCount;
addLog('PING', '发送心跳检测', 'ping');
}
}, 30000);
}
// 停止心跳
function stopHeartbeat() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
// 更新状态
function updateStatus(status) {
document.getElementById('connectionStatus').textContent = status;
}
// 添加日志
function addLog(type, message, className = '') {
const log = document.getElementById('log');
const entry = document.createElement('div');
entry.className = 'log-entry ' + className;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${type}: ${message}`;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
</script>
</body>
</html>
<!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', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.chat-container {
width: 100%;
max-width: 900px;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h1 {
font-size: 24px;
}
.connection-status {
display: flex;
align-items: center;
gap: 10px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #dc3545;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #28a745;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.chat-body {
display: flex;
height: 600px;
}
.chat-sidebar {
width: 250px;
background: #f8f9fa;
border-right: 1px solid #dee2e6;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 15px;
background: #e9ecef;
font-weight: bold;
border-bottom: 1px solid #dee2e6;
}
.user-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.user-item {
display: flex;
align-items: center;
padding: 10px;
border-radius: 8px;
margin-bottom: 5px;
transition: background 0.3s;
}
.user-item:hover {
background: #e9ecef;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
}
.user-name {
flex: 1;
}
.user-status {
width: 8px;
height: 8px;
border-radius: 50%;
background: #28a745;
}
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.message.sent {
align-items: flex-end;
}
.message.received {
align-items: flex-start;
}
.message-sender {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.message-bubble {
max-width: 70%;
padding: 12px 16px;
border-radius: 15px;
word-wrap: break-word;
}
.message.sent .message-bubble {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.message.received .message-bubble {
background: #e9ecef;
color: #333;
}
.message-time {
font-size: 11px;
color: #999;
margin-top: 5px;
}
.system-message {
text-align: center;
color: #666;
font-size: 14px;
margin: 10px 0;
}
.chat-input {
padding: 15px;
background: #f8f9fa;
border-top: 1px solid #dee2e6;
display: flex;
gap: 10px;
}
.chat-input input {
flex: 1;
padding: 12px 15px;
border: 1px solid #dee2e6;
border-radius: 25px;
outline: none;
font-size: 14px;
}
.chat-input input:focus {
border-color: #667eea;
}
.chat-input button {
padding: 12px 25px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-weight: bold;
transition: transform 0.3s;
}
.chat-input button:hover {
transform: scale(1.05);
}
.chat-input button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.typing-indicator {
padding: 10px 20px;
font-size: 14px;
color: #666;
font-style: italic;
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<h1>💬 WebSocket聊天室</h1>
<div class="connection-status">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">未连接</span>
</div>
</div>
<div class="chat-body">
<div class="chat-sidebar">
<div class="sidebar-header">在线用户 (<span id="userCount">0</span>)</div>
<div class="user-list" id="userList"></div>
</div>
<div class="chat-main">
<div class="chat-messages" id="chatMessages"></div>
<div class="typing-indicator" id="typingIndicator" style="display: none;"></div>
<div class="chat-input">
<input type="text" id="messageInput" placeholder="输入消息..." disabled>
<button id="sendBtn" disabled>发送</button>
<button id="connectBtn">连接</button>
</div>
</div>
</div>
</div>
<script>
// 聊天室管理器
class ChatRoom {
constructor() {
this.ws = null;
this.username = '用户' + Math.floor(Math.random() * 10000);
this.users = [];
this.typingUsers = new Set();
this.typingTimer = null;
this.initElements();
this.initEventListeners();
}
initElements() {
this.statusDot = document.getElementById('statusDot');
this.statusText = document.getElementById('statusText');
this.userCount = document.getElementById('userCount');
this.userList = document.getElementById('userList');
this.chatMessages = document.getElementById('chatMessages');
this.typingIndicator = document.getElementById('typingIndicator');
this.messageInput = document.getElementById('messageInput');
this.sendBtn = document.getElementById('sendBtn');
this.connectBtn = document.getElementById('connectBtn');
}
initEventListeners() {
this.connectBtn.addEventListener('click', () => this.connect());
this.sendBtn.addEventListener('click', () => this.sendMessage());
this.messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
this.messageInput.addEventListener('input', () => this.handleTyping());
}
connect() {
this.updateStatus('connecting');
// 使用模拟的WebSocket服务器
// 实际应用中替换为真实的WebSocket服务器地址
this.ws = new WebSocket('wss://echo.websocket.org');
this.ws.addEventListener('open', () => {
this.updateStatus('connected');
this.addSystemMessage('连接成功!欢迎来到聊天室');
// 发送用户加入消息
this.send({
type: 'join',
username: this.username
});
this.messageInput.disabled = false;
this.sendBtn.disabled = false;
this.connectBtn.textContent = '断开';
this.connectBtn.onclick = () => this.disconnect();
});
this.ws.addEventListener('message', (event) => {
this.handleMessage(event.data);
});
this.ws.addEventListener('close', () => {
this.updateStatus('disconnected');
this.addSystemMessage('已断开连接');
this.messageInput.disabled = true;
this.sendBtn.disabled = true;
this.connectBtn.textContent = '连接';
this.connectBtn.onclick = () => this.connect();
});
this.ws.addEventListener('error', () => {
this.updateStatus('error');
this.addSystemMessage('连接错误');
});
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
sendMessage() {
const message = this.messageInput.value.trim();
if (message && this.ws && this.ws.readyState === WebSocket.OPEN) {
this.send({
type: 'message',
username: this.username,
content: message,
timestamp: new Date().toISOString()
});
this.addMessage(this.username, message, true);
this.messageInput.value = '';
}
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
handleMessage(data) {
try {
const message = JSON.parse(data);
switch (message.type) {
case 'join':
this.addSystemMessage(`${message.username} 加入了聊天室`);
this.addUser(message.username);
break;
case 'leave':
this.addSystemMessage(`${message.username} 离开了聊天室`);
this.removeUser(message.username);
break;
case 'message':
this.addMessage(message.username, message.content, false);
break;
case 'typing':
this.handleTypingIndicator(message.username, message.isTyping);
break;
case 'userList':
this.updateUserList(message.users);
break;
}
} catch (e) {
// 如果不是JSON,直接显示文本
this.addMessage('服务器', data, false);
}
}
addMessage(sender, content, isSent) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isSent ? 'sent' : 'received'}`;
const time = new Date().toLocaleTimeString();
messageDiv.innerHTML = `
<div class="message-sender">${sender}</div>
<div class="message-bubble">${this.escapeHtml(content)}</div>
<div class="message-time">${time}</div>
`;
this.chatMessages.appendChild(messageDiv);
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
}
addSystemMessage(content) {
const messageDiv = document.createElement('div');
messageDiv.className = 'system-message';
messageDiv.textContent = content;
this.chatMessages.appendChild(messageDiv);
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
}
handleTyping() {
// 发送正在输入状态
this.send({
type: 'typing',
username: this.username,
isTyping: true
});
// 清除之前的定时器
clearTimeout(this.typingTimer);
// 3秒后发送停止输入状态
this.typingTimer = setTimeout(() => {
this.send({
type: 'typing',
username: this.username,
isTyping: false
});
}, 3000);
}
handleTypingIndicator(username, isTyping) {
if (isTyping) {
this.typingUsers.add(username);
} else {
this.typingUsers.delete(username);
}
if (this.typingUsers.size > 0) {
const users = Array.from(this.typingUsers);
const text = users.length === 1
? `${users[0]} 正在输入...`
: `${users.join(', ')} 正在输入...`;
this.typingIndicator.textContent = text;
this.typingIndicator.style.display = 'block';
} else {
this.typingIndicator.style.display = 'none';
}
}
addUser(username) {
if (!this.users.includes(username)) {
this.users.push(username);
this.updateUserList(this.users);
}
}
removeUser(username) {
this.users = this.users.filter(u => u !== username);
this.updateUserList(this.users);
}
updateUserList(users) {
this.users = users;
this.userCount.textContent = users.length;
this.userList.innerHTML = users.map(user => `
<div class="user-item">
<div class="user-avatar">${user.charAt(0).toUpperCase()}</div>
<div class="user-name">${user}</div>
<div class="user-status"></div>
</div>
`).join('');
}
updateStatus(status) {
const statusMap = {
connecting: { text: '连接中...', dot: '' },
connected: { text: '已连接', dot: 'connected' },
disconnected: { text: '未连接', dot: '' },
error: { text: '连接错误', dot: '' }
};
const { text, dot } = statusMap[status];
this.statusText.textContent = text;
this.statusDot.className = 'status-dot ' + dot;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// 初始化聊天室
const chatRoom = new ChatRoom();
</script>
</body>
</html>
// 推荐:完善的错误处理
ws.addEventListener('error', function(error) {
console.error('WebSocket错误:', error);
// 显示错误信息给用户
showErrorMessage('连接发生错误,请稍后重试');
});
ws.addEventListener('close', function(event) {
if (event.code !== 1000) {
console.error('异常关闭:', event.code, event.reason);
// 尝试重连
reconnect();
}
});
// 不推荐:没有错误处理
ws = new WebSocket(url);
// 没有任何错误处理
// 推荐:合理的连接管理
class WebSocketConnection {
constructor(url) {
this.url = url;
this.ws = null;
this.shouldReconnect = true;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.addEventListener('close', () => {
if (this.shouldReconnect) {
setTimeout(() => this.connect(), 3000);
}
});
}
disconnect() {
this.shouldReconnect = false;
if (this.ws) {
this.ws.close();
}
}
}
// 不推荐:没有连接管理
ws = new WebSocket(url);
// 连接断开后不会重连
// 推荐:使用消息队列
class MessageQueue {
constructor() {
this.queue = [];
this.ws = null;
}
send(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(message);
} else {
// 连接未打开,加入队列
this.queue.push(message);
}
}
onOpen() {
// 发送队列中的消息
while (this.queue.length > 0) {
const message = this.queue.shift();
this.ws.send(message);
}
}
}
东巴文点评:WebSocket开发中,错误处理、连接管理和消息队列是三个关键点,务必重视。
问题1:WebSocket的readyState值为1表示什么状态?
A. 正在连接 B. 已连接 C. 正在关闭 D. 已关闭
<details> <summary>点击查看答案</summary>答案:B
东巴文解释:WebSocket的readyState值:0-CONNECTING(正在连接)、1-OPEN(已连接)、2-CLOSING(正在关闭)、3-CLOSED(已关闭)。值为1表示连接已打开,可以发送和接收数据。
</details>问题2:以下哪个方法用于关闭WebSocket连接?
A. ws.disconnect() B. ws.close() C. ws.end() D. ws.terminate()
<details> <summary>点击查看答案</summary>答案:B
东巴文解释:WebSocket使用close()方法关闭连接。可以传入可选的code和reason参数:ws.close(1000, '正常关闭')。
任务:创建一个简单的实时股票价格监控应用,使用WebSocket接收股票价格更新并显示。
<details> <summary>点击查看参考答案</summary><!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>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
h1 {
text-align: center;
color: #333;
}
.stock-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 30px;
}
.stock-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.stock-card:hover {
transform: translateY(-5px);
}
.stock-symbol {
font-size: 24px;
font-weight: bold;
color: #333;
}
.stock-name {
color: #666;
margin-bottom: 15px;
}
.stock-price {
font-size: 32px;
font-weight: bold;
margin: 10px 0;
}
.stock-change {
font-size: 18px;
font-weight: bold;
}
.stock-change.up {
color: #28a745;
}
.stock-change.down {
color: #dc3545;
}
.stock-chart {
height: 80px;
margin-top: 15px;
background: #f9f9f9;
border-radius: 5px;
overflow: hidden;
}
.connection-status {
text-align: center;
padding: 10px;
margin-bottom: 20px;
border-radius: 5px;
}
.connection-status.connected {
background: #d4edda;
color: #155724;
}
.connection-status.disconnected {
background: #f8d7da;
color: #721c24;
}
.update-time {
text-align: center;
color: #666;
margin-top: 20px;
}
</style>
</head>
<body>
<h1>📈 实时股票价格监控</h1>
<div class="connection-status disconnected" id="connectionStatus">
未连接
</div>
<div class="stock-grid" id="stockGrid"></div>
<div class="update-time" id="updateTime"></div>
<script>
// 模拟股票数据
const stocks = [
{ symbol: 'AAPL', name: '苹果公司', price: 178.50 },
{ symbol: 'GOOGL', name: '谷歌', price: 141.80 },
{ symbol: 'MSFT', name: '微软', price: 378.90 },
{ symbol: 'AMZN', name: '亚马逊', price: 178.25 },
{ symbol: 'TSLA', name: '特斯拉', price: 248.50 },
{ symbol: 'META', name: 'Meta', price: 505.75 }
];
// 股票监控器
class StockMonitor {
constructor() {
this.ws = null;
this.stockData = {};
this.priceHistory = {};
this.initStockData();
this.renderStocks();
this.connect();
}
initStockData() {
stocks.forEach(stock => {
this.stockData[stock.symbol] = {
...stock,
change: 0,
changePercent: 0
};
this.priceHistory[stock.symbol] = [stock.price];
});
}
connect() {
this.updateConnectionStatus('connecting');
// 使用模拟数据(实际应用中连接真实WebSocket服务器)
this.ws = new WebSocket('wss://echo.websocket.org');
this.ws.addEventListener('open', () => {
this.updateConnectionStatus('connected');
this.startSimulation();
});
this.ws.addEventListener('message', (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'stockUpdate') {
this.updateStock(data);
}
} catch (e) {
// 忽略非JSON消息
}
});
this.ws.addEventListener('close', () => {
this.updateConnectionStatus('disconnected');
// 重连
setTimeout(() => this.connect(), 3000);
});
this.ws.addEventListener('error', () => {
this.updateConnectionStatus('error');
});
}
startSimulation() {
// 模拟股票价格更新
setInterval(() => {
const randomStock = stocks[Math.floor(Math.random() * stocks.length)];
const change = (Math.random() - 0.5) * 5; // -2.5 到 2.5
const newPrice = Math.max(0.01, this.stockData[randomStock.symbol].price + change);
const update = {
type: 'stockUpdate',
symbol: randomStock.symbol,
price: newPrice,
timestamp: new Date().toISOString()
};
// 模拟接收消息
this.updateStock(update);
}, 1000);
}
updateStock(data) {
const stock = this.stockData[data.symbol];
if (stock) {
const oldPrice = stock.price;
stock.price = data.price;
stock.change = data.price - oldPrice;
stock.changePercent = (stock.change / oldPrice) * 100;
// 更新价格历史
this.priceHistory[data.symbol].push(data.price);
if (this.priceHistory[data.symbol].length > 20) {
this.priceHistory[data.symbol].shift();
}
this.updateStockCard(data.symbol);
this.updateUpdateTime();
}
}
renderStocks() {
const grid = document.getElementById('stockGrid');
grid.innerHTML = Object.values(this.stockData).map(stock => `
<div class="stock-card" id="stock-${stock.symbol}">
<div class="stock-symbol">${stock.symbol}</div>
<div class="stock-name">${stock.name}</div>
<div class="stock-price">$${stock.price.toFixed(2)}</div>
<div class="stock-change ${stock.change >= 0 ? 'up' : 'down'}">
${stock.change >= 0 ? '▲' : '▼'}
${Math.abs(stock.change).toFixed(2)}
(${stock.changePercent >= 0 ? '+' : ''}${stock.changePercent.toFixed(2)}%)
</div>
<div class="stock-chart">
<canvas id="chart-${stock.symbol}" width="250" height="80"></canvas>
</div>
</div>
`).join('');
}
updateStockCard(symbol) {
const stock = this.stockData[symbol];
const card = document.getElementById(`stock-${symbol}`);
if (card) {
card.querySelector('.stock-price').textContent = `$${stock.price.toFixed(2)}`;
const changeEl = card.querySelector('.stock-change');
changeEl.className = `stock-change ${stock.change >= 0 ? 'up' : 'down'}`;
changeEl.innerHTML = `
${stock.change >= 0 ? '▲' : '▼'}
${Math.abs(stock.change).toFixed(2)}
(${stock.changePercent >= 0 ? '+' : ''}${stock.changePercent.toFixed(2)}%)
`;
// 更新图表
this.drawChart(symbol);
}
}
drawChart(symbol) {
const canvas = document.getElementById(`chart-${symbol}`);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const history = this.priceHistory[symbol];
if (history.length < 2) return;
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 计算范围
const min = Math.min(...history);
const max = Math.max(...history);
const range = max - min || 1;
// 绘制折线图
ctx.beginPath();
ctx.strokeStyle = history[history.length - 1] >= history[0] ? '#28a745' : '#dc3545';
ctx.lineWidth = 2;
history.forEach((price, i) => {
const x = (i / (history.length - 1)) * canvas.width;
const y = canvas.height - ((price - min) / range) * canvas.height * 0.8 - canvas.height * 0.1;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
}
updateConnectionStatus(status) {
const statusEl = document.getElementById('connectionStatus');
const statusMap = {
connecting: { text: '连接中...', className: 'disconnected' },
connected: { text: '已连接 - 实时更新中', className: 'connected' },
disconnected: { text: '未连接', className: 'disconnected' },
error: { text: '连接错误', className: 'disconnected' }
};
const { text, className } = statusMap[status];
statusEl.textContent = text;
statusEl.className = 'connection-status ' + className;
}
updateUpdateTime() {
document.getElementById('updateTime').textContent =
`最后更新: ${new Date().toLocaleTimeString()}`;
}
}
// 初始化监控器
const monitor = new StockMonitor();
</script>
</body>
</html>
</details>
东巴文(db-w.cn) - 让编程学习更有趣、更高效!