WebSocket 原理篇
# WebSocket 原理篇
# 一、WebSocket 协议的原理详解
WebSocket 协议与 HTTP 协议类似,都是基于 TCP 连接传输数据的。然而,WebSocket 协议通过 HTTP 协议建立初始连接,然后在此基础上进行 WebSocket 协议通信,因此这两者在握手阶段有一定的交集。
# 1.1 WebSocket 与 HTTP 的关系
- HTTP 协议 是一种非持久化协议,通信只能由客户端发起,每次请求对应一次响应。即使在 HTTP/1.1 中引入了
keep-alive
,允许复用 TCP 连接,但仍然保持“请求-响应”模式。 - WebSocket 协议 是一种持久化协议,可以在一次连接建立后,持续进行双向通信。服务器可以主动向客户端推送数据,而无需等待客户端请求。
# 1.2 WebSocket 握手流程
WebSocket 的建立过程主要分为两个阶段:HTTP 握手和协议升级。
- 客户端发起 HTTP 请求:使用标准的 HTTP 请求建立初始连接,并表明要升级为 WebSocket 协议。
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
2
3
4
5
6
7
8
解析这段请求:
Upgrade: websocket
:告诉服务器请求协议升级为 WebSocket。Connection: Upgrade
:指示当前连接要进行协议升级。Sec-WebSocket-Key
:客户端随机生成的 Base64 编码值,服务器需要用此值验证 WebSocket 请求的合法性。Sec-WebSocket-Protocol
:指定客户端支持的子协议,用于区分不同服务。Sec-WebSocket-Version
:WebSocket 协议版本,这里为 13,表示使用的是最新的 WebSocket 协议版本。
- 服务器响应并确认协议升级:
服务器解析客户端的请求后,返回如下响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
2
3
4
5
解析服务器响应:
HTTP/1.1 101 Switching Protocols
:状态码 101 表示协议切换成功。Upgrade: websocket
:确认升级协议为 WebSocket。Connection: Upgrade
:保持连接升级。Sec-WebSocket-Accept
:由服务器生成的验证信息,通过对Sec-WebSocket-Key
进行加密计算得到。Sec-WebSocket-Protocol
:确认使用的子协议。
至此,WebSocket 连接建立成功,接下来所有通信将按照 WebSocket 协议进行,而不再使用 HTTP。
# 1.3 WebSocket 协议工作流程
- 握手阶段:客户端通过 HTTP 请求发起握手,服务器返回 101 状态码表示协议切换成功。
- 数据传输阶段:握手成功后,WebSocket 连接进入数据传输阶段,双方可以随时发送数据。通信是双向的,且无需重新建立连接。
- 连接关闭阶段:当客户端或服务器希望关闭连接时,可以发送关闭帧,另一方收到后也应发送关闭帧确认,随后连接断开。
# 1.4 WebSocket 的优缺点
优点:
- 持久化连接:建立连接后,通信可以持续进行,避免了频繁的握手过程。
- 双向通信:服务器可以主动推送消息,不需要等待客户端请求。
- 轻量化:相比 HTTP 请求的头部信息,WebSocket 消耗更少的带宽。
- 实时性:适用于需要低延迟、高实时性的应用场景。
缺点:
- 浏览器兼容性:部分旧版浏览器(如 IE 10 以下版本)不支持 WebSocket。
- 连接维护:持久化连接需要服务器进行更多资源管理,如连接超时、心跳检测等。
# 二、WebSocket 握手请求与响应详解
# 2.1 客户端握手请求示例
以下是客户端发送的握手请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
2
3
4
5
6
7
8
GET /chat HTTP/1.1
:发起对/chat
路径的 HTTP GET 请求。Upgrade: websocket
和Connection: Upgrade
:指示服务器切换协议。Sec-WebSocket-Key
:客户端生成的随机字符串,用于校验服务器响应。Sec-WebSocket-Protocol
:指定客户端支持的子协议。Sec-WebSocket-Version
:WebSocket 协议版本,通常为 13。
# 2.2 服务器握手响应示例
服务器响应如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
2
3
4
5
HTTP/1.1 101 Switching Protocols
:确认协议切换。Sec-WebSocket-Accept
:服务器返回的验证信息,是对客户端Sec-WebSocket-Key
的加密处理结果。Sec-WebSocket-Protocol
:确认最终采用的子协议。
总结
WebSocket 协议连接过程概述:
- 客户端发起 HTTP 请求,并携带 WebSocket 协议相关信息(如
Upgrade
、Sec-WebSocket-Key
等)。 - 服务器接收请求,验证后返回协议切换确认信息(如
Sec-WebSocket-Accept
),建立 WebSocket 连接。 - 连接建立后,客户端和服务器可以进行双向通信,直到一方主动关闭连接。
# 三、WebSocket 的应用场景
WebSocket 作为一种高效的双向通信协议,适用于多个实时性要求较高的场景。以下是 WebSocket 的典型应用场景及一些不能使用的场景:
# 3.1 WebSocket 适用场景
即时聊天通信
- 实现类似微信、Slack 的实时聊天应用,用户间消息可以立即发送和接收,避免传统 HTTP 长轮询带来的延迟和资源浪费。
多人在线游戏
- 在多人在线游戏中,服务器需要频繁向所有玩家广播游戏状态的更新,WebSocket 能实现低延迟的实时数据同步。
在线协同编辑
- 在文档或代码的协同编辑中,多个用户可以实时查看和编辑内容,通过 WebSocket 可以确保每个用户的界面保持同步。
实时数据流
- 比如股票行情、加密货币价格、体育比赛直播等需要频繁更新的场景,WebSocket 可以实时推送最新的数据到客户端。
地图位置共享
- 实时更新和共享用户或车辆的位置数据,适用于打车服务、物流跟踪等应用。
即时 Web 应用
- 比如在交易网站中,价格波动数据需要实时显示,WebSocket 能够连续推送更新的数据,提高用户体验。
# 3.2 不适用 WebSocket 的场景
静态或不频繁更新的数据
- 当数据变化较慢,且只需一次获取即可满足需求时,使用 HTTP 请求即可,比如静态页面或不需要频繁刷新数据的页面。
旧数据查询
- 如果仅需获取历史数据或一次性的查询,HTTP 协议更合适。
无需双向实时通信
- 如果只需单向请求并响应,且通信频率较低,HTTP 的请求-响应模式已经足够。
# 四、WebSocket 在线/离线状态的判断与断线问题的解决
在 WebSocket 通信中,判断客户端是否在线以及处理断线重连是两个重要的问题。特别是在 Nginx 代理 WebSocket 的场景下,如果连接长时间无消息,可能会被自动断开。下面详细总结这部分内容。
# 4.1 如何判断客户端在线/离线?
为了判断客户端是否在线,通常采用“心跳检测”的方式,结合数据库或缓存系统来记录客户端的状态。
工作流程:
第一次连接与状态记录:
- 当客户端第一次与服务器建立 WebSocket 连接时,会发送一个包含唯一标识(如用户 ID)和时间戳的请求。
- 服务器接收该请求后,将客户端的唯一标识和时间戳存入数据库或缓存中,标记为“在线”。
定时心跳检测:
- 客户端每隔一段时间(如 30 秒)发送一个心跳包,请求中仍包含唯一标识和时间戳。
- 服务器接收到心跳包后,查询数据库或缓存中该唯一标识对应的上一次时间戳,计算与当前时间戳的差值。
- 如果时间差在预定的范围内(如 60 秒内),则认为客户端仍然在线;如果超出范围,则标记为“离线”。
示例代码:
// 客户端定时发送心跳包
function sendHeartbeat() {
const uniqueId = "client_123"; // 客户端唯一标识
const timestamp = Date.now(); // 当前时间戳
ws.send(JSON.stringify({ id: uniqueId, timestamp: timestamp }));
}
// 每隔 30 秒发送一次心跳包
setInterval(sendHeartbeat, 30000);
2
3
4
5
6
7
8
9
10
服务器端逻辑:
// 伪代码:服务器端处理心跳请求
function handleHeartbeat(request) {
String clientId = request.getParameter("id");
long currentTimestamp = request.getParameter("timestamp");
// 从缓存中查询该客户端的上一次心跳时间
long lastTimestamp = cache.get(clientId);
if (lastTimestamp == null || (currentTimestamp - lastTimestamp > 60000)) {
// 超过 60 秒没有更新,标记为离线
markAsOffline(clientId);
} else {
// 更新客户端的最新心跳时间
cache.put(clientId, currentTimestamp);
markAsOnline(clientId);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4.2 解决 WebSocket 断线问题
在实际应用中,WebSocket 可能因为网络不稳定、Nginx 超时等原因断开连接。为了确保连接的持久性,可以采用以下两种方案:
# 方案一:修改 Nginx 配置
Nginx 作为反向代理时,如果 WebSocket 连接在一段时间内没有消息传输,可能会被自动断开。可以通过调整 Nginx 的配置来增加连接的持久性。
Nginx 配置示例:
http {
# 增加 WebSocket 的超时时间
upstream websocket {
server localhost:8080;
}
server {
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# 设置较长的超时时间,防止无消息时被断开
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 3600s;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 方案二:客户端发送心跳包
定期发送心跳包可以防止连接因长时间无消息而被 Nginx 或其他代理服务器断开。心跳包通常是一个简单的 ping 消息,服务器接收后返回一个 pong 响应。
心跳包实现示例:
// 心跳检测,防止连接超时断开
var heartCheck = {
timeout: 30000, // 每 30 秒发送一次心跳
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function () {
var self = this;
this.timeoutObj = setTimeout(function () {
ws.send('ping'); // 发送心跳包
console.log('发送心跳包: ping');
self.serverTimeoutObj = setTimeout(function () {
ws.close(); // 如果服务器未响应,主动断开连接
}, self.timeout);
}, this.timeout);
}
};
// 在 WebSocket 连接建立后启动心跳检测
ws.onopen = function () {
heartCheck.reset().start(); // 连接成功后启动心跳检测
};
// 当接收到服务器的 pong 消息时,重置心跳检测
ws.onmessage = function (event) {
if (event.data === 'pong') {
heartCheck.reset().start(); // 重置并重新启动心跳检测
} else {
console.log('收到消息:', event.data);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
总结
- 判断在线/离线的关键在于心跳检测,通过记录时间戳和唯一标识来确定客户端是否在线。
- WebSocket 断线问题主要由网络不稳定或代理超时引起,可以通过 Nginx 配置调整超时时间或定期发送心跳包来解决。
- 综合使用心跳包和重连机制,可以最大限度地确保 WebSocket 连接的稳定性和持续性。