概述
Websocket 特点
WebSockets是一种在浏览器和服务器之间建立持久连接的协议,它允许服务器主动推送数据给客户端,并且在客户端和服务器之间实现双向通信。
-
建立连接:客户端使用WebSocket对象来建立WebSocket连接。例如:
const socket = new WebSocket('ws://example.com/socket');
-
协议:WebSocket使用标准的WebSocket协议来进行通信。相对于HTTP协议,WebSocket协议提供了更高效率和更低的延迟。ws和wss协议正对应了http和https。
-
事件:WebSocket对象提供了一些事件,用于处理连接的不同阶段和接收到的数据。常见的事件有
onopen
(连接建立)、onmessage
(接收到消息)、onclose
(连接关闭)等。 -
数据传输:WebSocket可以传输文本和二进制数据。文本数据是以UTF-8编码发送和接收的,而二进制数据使用ArrayBuffer对象。
-
安全性:与HTTP协议通过TLS/SSL协议进行加密一样,WebSockets也可以通过wss协议来提供安全的通信。wss://表示通过TLS/SSL加密的WebSockets连接。
-
跨域策略:WebSockets默认不受同源策略的限制,可以跨域使用,但仍然会受到服务器的安全策略限制。
可以在浏览器和服务器之间建立稳定的连接,实际应用如聊天应用、实时数据监控等。
Websocket 握手
当客户端需要和服务器使用WebSocket进行通信时,首先会使用HTTP协议完成一次特殊的请求响应,这一次请求-响应就是WebSocket握手。完成握手之后,后续才会使用 websocket 协议。在握手阶段,首先由客户端向服务器发送一个请求,请求地址格式如下:
请求头:
服务器如果同意,就响应:
基本使用
使用 WebSocket 进行连接和通信:
首先,在客户端使用 WebSocket 对象来建立连接:
const socket = new WebSocket('ws://example.com/socket');
// 监听连接建立事件
socket.onopen = function() {
console.log('WebSocket连接已建立');
// 向服务器发送消息
socket.send('Hello Server!');
};
// 监听接收到消息事件
socket.onmessage = function(event) {
const message = event.data;
console.log('接收到服务器消息:' + message);
};
// 监听连接关闭事件
socket.onclose = function() {
console.log('WebSocket连接已关闭');
};
在上面的代码中,我们创建了一个WebSocket对象并指定连接的URL。然后我们监听了三个事件:onopen
表示连接建立时触发的事件,onmessage
表示接收到消息时触发的事件,onclose
表示连接关闭时触发的事件。
在连接建立后,我们向服务器发送了一条消息Hello Server!
。当服务器向客户端发送消息时,onmessage
事件将被触发,并且我们可以在回调函数中处理接收到的消息。
接下来,让我们看一下服务器端的示例代码(以Node.js和Express框架为例):
const express = require('express');
const WebSocket = require('ws');
const app = express();
// 创建WebSocket服务器
const server = require('http').createServer(app);
const wss = new WebSocket.Server({ server });
wss.on('connection', function(socket) {
console.log('WebSocket连接已建立');
// 监听客户端发送的消息
socket.on('message', function(message) {
console.log('接收到客户端消息:' + message);
// 向客户端发送消息
socket.send('Hello Client!');
});
// 监听连接关闭事件
socket.on('close', function() {
console.log('WebSocket连接已关闭');
});
});
server.listen(3000, function() {
console.log('服务器已启动,监听端口3000');
});
在服务器端的代码中,我们使用了ws库创建了一个WebSocket服务器,并监听了connection
事件。当有客户端连接到服务器时,connection
事件将被触发,并在该回调函数中处理连接过程。
在服务器接收到客户端的消息时,我们输出消息到控制台,并向客户端发送一条回复消息。当连接关闭时,close
事件将被触发。
最后,我们在Express应用中创建了一个HTTP服务器,将WebSocket服务器绑定到服务器上,然后启动服务器监听端口3000。
通信小实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
const ws = new WebSocket('ws://localhost:3000')
ws.addEventListener('open',()=> {
console.log('已连接服务器')
ws.send('我是前端的数据')
})
ws.addEventListener('message', ({data})=> {
console.log(data)
})
</script>
</body>
</html>
const WebSocket = require('ws')
const socket = new WebSocket.Server({port: 3000});
socket.on('connection',ws=> {
console.log('我进来了')
ws.on('message',(data)=> {
console.log('前端数据:' + data + "。后端返回数据:"+ '我是后端的数据')
})
ws.on('close',()=> {
console.log('溜了溜了')
})
})
Socket.io
原生 websocket 有一个问题,连接双方可以在任何时候发送任何类型的数据,但是另一方必须清楚这个数据的含义,比如是谁发送过来的。
http 协议通过 path 请求路径来区分这个问题。Socket.io 为了解决这个问题,将不同的消息放入到不同的事件中,这些事件的类型是由我们自己定义的。
Socket.io 具有跨平台,实时性,WebSocket 不可连接时自动降级到其他传播机制(如 http 长轮询)等优点。由于 socket.io 对消息格式进行了特殊处理,所以如果一方使用了 socket.io 那么另一方也需要使用 这个库。
socket.io 有一些预定义的事件名,比如 message, connect 等,不可以作为我们自定义的事件名。
除此之外,Socket.io对低版本浏览器还进行了兼容处理。如果浏览器不支持WebSocket,Socket.io将使用长轮询(long polling)处理。另外,Socket.io还支持使用命名空间来进一步隔离业务。
官方文档:(服务器初始化 | Socket.IO)
npm install socket.io
安装。
聊天室案例
在使用框架的时候,我们可以使用 socket.io-client 这个库来简化实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}
html,
body,
.room {
height: 100%;
width: 100%;
}
.room {
display: flex;
}
.left {
width: 300px;
border-right: 0.5px solid #2b2d30;
background: #333;
}
.right {
background: #1c1c1c;
flex: 1;
display: flex;
flex-direction: column;
}
.header {
background: rgb(60, 63, 65);
color: white;
padding: 10px;
box-sizing: border-box;
font-size: 20px;
}
.main {
flex: 1;
padding: 10px;
box-sizing: border-box;
font-size: 20px;
overflow: auto;
}
.main-chat {
color: #bcbec4;
}
.footer {
min-height: 200px;
border-top: 1px solid #4e5157;
}
.footer .ipt {
width: 100%;
height: 100%;
color: #bcbec4;
outline: none;
font-size: 20px;
padding: 10px;
box-sizing: border-box;
}
.groupList {
height: 100%;
overflow: auto;
}
.groupList-items {
height: 50px;
width: 100%;
background: rgb(67, 69, 74);
border-bottom: 1px solid #2b2d30;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
</style>
</head>
<div class="room">
<div class="left">
<div class="groupList">
</div>
</div>
<div class="right">
<header class="header">聊天室</header>
<main class="main">
</main>
<footer class="footer">
<div class="ipt" contenteditable></div>
</footer>
</div>
</div>
<body>
<script type="module">
import {io} from "https://cdn.socket.io/4.7.4/socket.io.esm.min.js";
let name = prompt('请输入你的名字')
let room = prompt('请输入你要进入的房间')
const sendMessage = (message) => {
const div = document.createElement('div');
div.className = 'main-chat';
div.innerText = `${message.user}:${message.text}`;
main.appendChild(div)
}
const group = document.querySelector('.groupList'); // 左侧列表的父级
const main = document.querySelector('.main'); // 消息内容的父级
const ipt = document.querySelector('.footer .ipt')
// 希望自己也能收到信息
const addChart = (msg)=> {
let item = document.createElement('div')
item.className = 'main-chat'
item.innerHTML = `${msg.name}: ${msg.message}`
main.appendChild(item)
}
const socket = io('ws://localhost:3000') // ws 的地址
socket.on('connect', () => {
// 监听回车事件
document.addEventListener('keydown',e=> {
if (e.key === 'Enter') {
const message = ipt.innerText
socket.emit('message', {name, message, room})
addChart({name, message})
ipt.innerText = ''
}
})
// 加入房间
socket.emit('join', {name, room})
//监听左侧列表
socket.on('groupMap', (data) => {
group.innerHTML = ``
Object.keys(data).forEach(key=> {
const item = document.createElement('div')
item.className = 'groupList-items'
item.innerHTML = `房间号:${key} 房间人数:${data[key].length}`
group.appendChild(item)
})
})
//监听发送的消息
socket.on('message', (data) => {
addChart(data)
})
})
</script>
</body>
</html>
import {createServer} from "http";
import {Server} from "socket.io";
const server = createServer();
const io = new Server(server, {
cors: true // 允许跨域
});
/**
* [{name, room, id}, {name, room, id}]
* @type {{}}
*/
const groupMap = {}
io.on("connection", (socket) => {
// 前端传过来 name 和 room
socket.on('join', ({name, room}) => {
socket.join(room) // 创建一个房间
if (groupMap[room]) {
groupMap[room].push({name, room, id: socket.id}) // 已有就 push
} else {
groupMap[room] = [{name, room, id: socket.id}] // 没有就创建
}
socket.emit('groupMap', groupMap)
// 所有人都可以看到
socket.broadcast.emit('groupMap', groupMap)
// 管理员发送信息
socket.broadcast.to(room).emit('message', {
name: '管理员',
message: `欢迎${name}进入聊天室`
})
})
// 向前端发消息
socket.on('message', ({name, message, room}) => {
// 虽然给别人发送了消息,但是自己收不到
socket.broadcast.to(room).emit('message', {name, message})
})
});
server.listen(3000);
面试题
1. webSocket 协议是什么,能简述一下吗?
websocket 协议是 HTML5 带来的新协议,相对于http,它是一个持久连接的协议,它利用 http 协议完成握手,然后通过 TCP 连接通道发送消息,允许客户端和服务端进行全双工通信,使用 websocket 协议可以实现服务器主动推送消息。默认端口也是 80 和 443 。
首先,客户端若要发起websocket连接,首先必须向服务器发送http 请求以完成握手,请求行中的path需要使用ws:
开头的地址,请求头中要分别加入 upgrade.connection、Sec-WebSocket-Key、Sec-WebSocket-Version 等请求头标记。
然后,服务器收到请求后,发现这是一个 websocket 协议的握手请求,于是响应行中包含 Switching Protocols ,同时响应头中包含upgrade、connection、.Sec-WebSocket-Accept 等响应头标记。
当客户端收到响应后即可完成握手,随后使用建立的 TCP 连接直接发送和接收消息。
其他特点有数据格式比较轻量,性能开销小,通信高效,可以发送文本,也可以发送二进制数据。没有同源策略的限制,完全可以取代 ajax,协议标识符是 ws 和 wss,服务求网址就是 url。
2. webSocket 与传统的 http 有什么优势?
当页面中需要观察实时数据的变化(比如聊天、k 线图)时,过去我们往往使用两种方式完成:
第一种是短轮询,即客户端每隔一段时间就向服务器发送消息,询问有没有新的数据。
第二种是长轮询,发起一次请求询问服务器,服务器可以将该请求挂起,等到有新消息时再进行响应。响应后,客户端立即又发起一次请求,重复整个流程。
无论是哪一种方式,都暴露了 http 协议的弱点,即响应必须在请求之后发生,服务器是被动的,无法主动推送消息。而让客户端不断的发起请求又白白的占用了资源。
websocket 的出现就是为了解决这个问题,它利用 http 协议完成握手之后,就可以与服务器建立持久的连接,服务器可以在任何需要的时候,主动推送消息给客户端,这样占用的资源最少,同时实时性也最高。
3. 前端如何实现即时通讯?
- 短轮询。即客户端每隔一段时间就向服务器发送消息,询问有没有新的数据
- 长轮询,发起一次请求询问服务器,服务器可以将该请求挂起,等到有新消息时再进行响应。响应后,客户端立即又发起一次请求,重复整个流程。
- websocket,握手完毕后会建立持久性的连接通道,随后服务器可以在任何时候推送新消息给客户端
4. Socket 和 WebSocket 的区别?
TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,Socket是对TCP/IP协议的封装和应用,在软件上的体现可以理解为nodejs的内置模块 socket,可以去操作TCP。
当两台主机通信时,必须通过Socket连接,Socket则利用TCP/IP协议建立TCP连接。TCP连接则更依靠于底层的IP协议,IP协议的连接则依赖于链路层等更低层次。
Socket(套接字)并不是一个独立的协议,而是位于应用层和传输层之间的一组编程接口(API)。它为应用程序提供了访问底层网络通信能力的方式,通常用于基于 TCP 或 UDP 协议的网络通信。(腾讯云, IBM)
在 OSI 七层模型中,Socket 并不直接对应某一层,而是作为应用层与传输层之间的抽象层。它封装了复杂的 TCP/IP 协议操作,提供了统一的接口,使得开发者可以通过调用 Socket 接口来实现网络通信,而无需深入了解底层协议的细节。(goeasy.io)
因此,Socket 主要被视为应用层与传输层之间的桥梁,属于应用层编程范畴。它使得应用程序能够利用传输层的服务(如 TCP 或 UDP)进行数据传输。(腾讯云)
5. WebSocket协议与HTTP长连接的核心区别
-
通信模式差异
• HTTP长连接:基于"请求-响应"模型,客户端必须主动发起请求,服务端才能响应,无法实现服务端主动推送数据。• WebSocket:全双工通信,双方均可主动发送数据,适合实时交互场景(如IM通讯场景)。
-
协议升级机制
• HTTP长连接:直接复用TCP连接传输多个HTTP请求,无需协议升级(HTTP/1.1默认支持持久连接)。• WebSocket:通过HTTP协议握手(状态码101)升级为WebSocket协议,后续通信脱离HTTP规范。
-
数据头部开销
• HTTP长连接:每次请求需携带完整HTTP头部(如Cookie、User-Agent等),导致带宽浪费(HTTP头部冗余问题)。• WebSocket:建立连接后仅需2-10字节的轻量级帧头(二进制帧结构)。
-
连接生命周期
• HTTP长连接:空闲超时自动断开(例如Apache默认5-15秒),需客户端定期轮询。• WebSocket:持久连接直到显式关闭。
-
适用场景
• HTTP长连接:适合减少重复建立连接的资源消耗,但无法突破单向通信限制(HTTP/2多路复用场景)。• WebSocket:专为实时双向通信设计(在线游戏、聊天室等场景)。
6. WebSocket 开发中的常见问题与解决方案
一、心跳机制失效
问题表现:
连接因网络波动或服务器超时自动断开,但客户端未及时感知,导致消息丢失或通信中断。
解决方案:
-
双向心跳检测:
-
客户端与服务端约定固定间隔(如30秒)发送
ping
消息,对方回复pong
确认存活。若连续多次未收到响应,则触发重连。 -
示例代码(客户端):
let heartbeatTimer; function startHeartbeat() { heartbeatTimer = setInterval(() => { if (socket.readyState === WebSocket.OPEN) { socket.send('ping'); } }, 30000); } socket.addEventListener('open', startHeartbeat); socket.addEventListener('message', (event) => { if (event.data === 'pong') resetHeartbeat(); // 收到响应后重置计时 });
-
服务端逻辑:若超时未收到心跳,主动断开连接并释放资源。
-
-
动态调整心跳间隔:
根据网络质量动态调整心跳频率(如网络差时延长间隔),减少无效流量消耗。
二、重连机制不稳定
问题表现:
连接断开后重连失败或频繁重试导致服务器压力激增。
解决方案:
-
指数退避策略:
-
每次重连间隔按指数增长(如1s → 2s → 4s),直至达到最大间隔(如30秒)或最大尝试次数(如10次)。
-
示例代码:
let retryCount = 0, maxRetries = 10, baseDelay = 1000; function reconnect() { if (retryCount >= maxRetries) return; const delay = Math.min(baseDelay * 2 ** retryCount, 30000); setTimeout(() => { createWebSocket(); // 创建新连接 retryCount++; }, delay); }
-
-
网络状态感知:
- 监听浏览器
online/offline
事件,网络恢复后立即触发重连。 - 结合心跳检测区分“短暂断开”与“永久故障”,避免无效重试。
- 监听浏览器
三、会话恢复与消息丢失
问题表现:
重连后历史会话状态丢失,或未确认消息未能重传。
解决方案:
-
消息缓冲队列:
-
客户端在本地缓存未确认的消息,重连成功后按顺序重发。
-
示例:
let messageQueue = []; socket.onclose = () => { messageQueue = [...pendingMessages]; // 保存未发送消息 reconnect(); }; socket.onopen = () => { messageQueue.forEach(msg => socket.send(msg)); // 重连后重发 };
-
-
服务端状态同步:
- 客户端在重连时携带会话ID(如JWT),服务端恢复上下文状态。
- 使用唯一标识(如用户ID+设备ID)确保消息路由正确。