WebSocket协议

Ajax

Ajax 是通过 JavaScript 在页面内发起异步 HTTP 请求(通常使用 XMLHttpRequest 或 fetch),服务端返回响应数据(现代多为 JSON,早期为 XML),前端根据返回内容更新页面的局部 DOM,从而实现无需整页刷新的动态交互效果。

1
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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Ajax JSON 示例</title>
</head>
<body>
<h2>🧾 服务器数据:</h2>
<div id="result">等待数据中...</div>

<script>
function getData() {
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('网络响应失败');
}
return response.json(); // 解析 JSON 数据
})
.then(data => {
document.getElementById('result').innerText = `消息:${data.message}`;
})
.catch(error => {
document.getElementById('result').innerText = '请求失败:' + error.message;
});
}
// 初次加载 + 定时轮询
getData();
setInterval(getData, 5000);
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require('express');
const app = express();

// 简单的 JSON API
app.get('/api/data', (req, res) => {
const result = {
message: '你好,这是来自服务器的 JSON 数据!时间:' + new Date().toLocaleTimeString()
};
res.json(result);
});

app.listen(3000, () => {
console.log('服务运行在 <http://localhost:3000>');
});

Long Poll

Long Poll 是由客户端发起一个请求,当服务端没有信息更新时,会处于阻塞状态,直到有更新后,由服务器响应给客户端

1
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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Long Polling 示例</title>
</head>
<body>
<h2>📡 消息推送:</h2>
<div id="box">等待服务器推送消息...</div>

<script>
function startLongPoll() {
fetch('/longpoll')
.then(res => res.json())
.then(data => {
if (data.message) {
document.getElementById('box').innerText = data.message;
}
// 不论有没有消息,立即再次发起请求
startLongPoll();
})
.catch(err => {
document.getElementById('box').innerText = '连接失败,重试中...';
setTimeout(startLongPoll, 3000); // 等待再重试
});
}

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

服务端

1
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
// server.js
const express = require('express');
const app = express();

let latestMessage = '暂无新消息';
let waitClients = []; // 挂起请求的客户端列表

// 模拟服务端推送(每隔 10 秒更新一条消息)
setInterval(() => {
latestMessage = '新消息时间:' + new Date().toLocaleTimeString();

// 所有挂起的请求立即响应
waitClients.forEach(res => res.json({ message: latestMessage }));
waitClients = []; // 清空
}, 10000);

// Long Poll 接口
app.get('/longpoll', (req, res) => {
// 将响应挂起,等待服务端有更新后再响应
waitClients.push(res);

// 超时保护:如果30秒后还没有消息,则返回空数据
setTimeout(() => {
const index = waitClients.indexOf(res);
if (index !== -1) {
waitClients.splice(index, 1);
res.json({ message: null }); // 表示无更新
}
}, 30000);
});

app.listen(3000, () => {
console.log('服务运行在 <http://localhost:3000>');
});

WebSocket

WebSocket 是通过 HTTP 请求建立的连接,在成功升级协议后切换为全双工通信,允许客户端和服务端彼此主动发送消息。

1
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
39
40
41
42
43
44
45
46
47
48
49
50
// JS 客户端
socket = new WebSocket("ws://192.168.0.5:18899/ws","echo");
// 创建websocket
socket.onopen = function(event){
var target = document.getElementById("responseText");
target.value = "Web Socket 连接已经开启";
};
// 监听websocket连接的open事件

socket.onmessage = function(event){
var ta = document.getElementById("responseText");
ta.value = ta.value + '\\n' + event.data
};
// 获取服务端数据

<script type="text/javascript">
var socket ;
if(!window.WebSocket){
window.WebSocket = window.MozWebSocket;
}
var domain = window.location.host;
if(window.WebSocket){
socket = new WebSocket("ws://" + domain +"/ws" ,"echo");
socket.onmessage = function (event){
var ta = document.getElementById("responseText");
ta.value = ta.value + '\\n' + event.data
};
socket.onopen = function(event){
var target = document.getElementById("responseText");
target.value = "Web Socket 连接已开启!";
};
socket.onclose = function(event){
var target = document.getElementById("responseText");
target.value = ta.vale + "WebSocket 已断开";
};
}else {
alert("Your browser does not support Web Socket");
}

function send(message){
if(!windows.WebSocket){
return ;
}
if (socket.readyState == WebSocket.OPEN){
socket.send(message);
}else {
alert("The Socket is not open");
}
}
</script>

服务器端

1
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// WebSocket数据帧类型
BinaryWebSocketFrame 封装二进制数据的WebSocketFrame数据帧
TextWebSocketFrame 封装文本数据的WebSocketFrame数据帧
CloseWebSocketFrame 表示一个Close结束请求
ContinuationWebSocketFrame 分拆为多个WebSocket数据帧后,用于发送后续内容的数据帧
PingWebSocketFrame 心跳帧,一般用于客户端发送
PongWebSocketFrame 心跳帧,一般用于服务端发送

// Netty 包装类

WebSocketServerProtocolHandler 负责开启处理握手过程,以及保活/关闭处理
WebSocketServerProtocolHandshakeHandler 负责进行协议升级握手处理
WebSocketSocketFrameEncoder 数据帧编码器,负责WebSocket数据帧编码。
WebSocketSocketFrameDecoder 数据帧解码器,负责WebSocket数据帧解码

// 服务端
@Slf4j
public final class WebSocketEchoServer{
static class EchoInitializer extends ChannelInitializer<SocketChannel>{
@Override
public void initChannel(SocketChannel ch){
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpRequestDecoder());
pipeline.addLast(new HttpResponseEncoder());
pipeline.addLast(new HttpObjectAggregator(65535));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws","echo",true,10*1024));
pipeline.addLast(new WebPageHandler());
pipeline.addLast(new TextWebSocketFrameHandler());
}
}
public static void start(String ip ) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new EchoInitializer());
Channel ch = b.bind(18899).sync().channel();
log.info("WebSocket服务已启动http://{}:{}",ip,18899);
ch.closeFutrue().sync();
}finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}

// 处理器
public class TextWebSocketFrameHandler extends
SimpleChannelInboundHandler<WebSocketFrame>
{
@Override
protected void channelRead(ChannelHandlerContext ctx , WebSocketFrame frame) throws Exception
{
if(frame instanceof TextWebSocektFrame){
String request = ((TextWebSocketFrame) frame).text();
log.debug("服务端收到:"+ request);
String echo = Dateutil.getTime() + ":" + request;
TextWebSocketFream echoFrame = new TextWebSocketFrame(echo);
ctx.channel().writeAndFlush(echoFrame);
} else {
String message = "unsupported frame type:" + frame.getClass().getName();
throw new UnsupportedOperationException(message);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception
{
if(evt instanceof WebSocketServerProtocolHandler.HandshakeComplete){
ctx.pipeline().remove(WebPageHandler.class);
log.debug("WebSock HandShakeComplete 握手成功");
log.debug("新的WebSocket客户端加入,通道为:"+ ctx.channel());
} else
{
super.userEventTriggered(ctx,evt);
}
}
}

WebSocket 升级过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
🧑 Client                   🌐 Network                   🖥️ Server

┌────────────┐ ┌─────────────┐
│JS 发起请求 │ ── GET / Upgrade: websocket ──▶ │HTTP Server │
└────────────┘ └─────────────┘
│验证 Sec-Key │
│返回 101 升级│
◀────────────── 101 Switching Protocols ─────────
▼ 升级完成

🚀 WebSocket 建立,双向通信开始
⇄ socket.send()
⇄ socket.onmessage()

❌ 发送 Close 帧
▼ TCP 连接关闭

客户端基于HTTP 发送升级请求,请求首部包含以下内容 :

1
2
3
4
5
6
7
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket // 指定升级的目标协议
Connection: Upgrade // 发出声明请求
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== ← 客户端随机Base64编码值
Sec-WebSocket-Version: 13 ← 使用的WebSocket协议版本
Sec-WebSocket-Protocol: echo\\r\\n ← 可以用于描述自定义的子协议

服务端响应

1
2
3
4
5
HTTP/1.1 101 Switching Protocols                    ← 响应码为101
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= ← 服务端计算后的 key 验证结果
Sec-WebSocket-Protocol: echo\\r\\n ← 用于描述自定义的子协议

报文格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0               1               2               3  
+---------------+---------------+---------------+---------------+
|FIN| RSV1|RSV2|RSV3| OPCODE | MASK | Payload length (7) |
+---------------+---------------+-------------------------------+
| Extended payload length (16/64 bits, optional) |
+---------------------------------------------------------------+
| Masking key (32 bits, optional) |
+---------------------------------------------------------------+
| Payload data (x bytes) |
+---------------------------------------------------------------+
FIN 1 bit 是否为消息的最后一帧(1 表示是)
RSV1~3 3 bit 通常为 0,保留扩展用
OPCODE 4 bit 表示帧类型(见下)
MASK 1 bit 表示 Payload 是否被掩码(客户端必须为 1
Payload length 7 bit 表示数据长度(0~125
特殊值:126 = 16bit 扩展,127 = 64bit 扩展
Extended Payload Length 0/2/8 字节 当长度为 126127 时才出现
Masking key 4 字节 仅客户端发送时存在,用于解码 payload
Payload data 任意 真正要传输的数据(文本/二进制)

OPCODE (报文类型)

OPCODE 类型 含义
0x0 Continuation 后续帧(用于分片)
0x1 Text Frame 文本帧(UTF-8 编码)
0x2 Binary Frame 二进制帧
0x8 Connection Close 关闭连接
0x9 Ping 心跳探测(客户端发送)
0xA Pong 心跳回应

WebSocket协议
http://gadoid.io/2025/05/13/WebSocket协议/
作者
Codfish
发布于
2025年5月13日
许可协议