抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它使得客户端和服务器之间的数据交换变得更加简单高效。本文将详细介绍 WebSocket 的握手过程、使用方式以及与 HTTP 的比较。

一、WebSocket 简介

1.1 为什么需要 WebSocket

在传统的 HTTP 协议中,通信只能由客户端发起。如果服务器有数据变化,客户端只能通过”轮询”的方式不断请求服务器来获取最新数据。这种方式存在明显的缺点:

  • 效率低下:客户端需要不断发送请求,即使服务器没有新数据
  • 延迟高:数据更新后需要等到下一次轮询才能获取
  • 资源浪费:每次请求都要建立 TCP 连接、发送完整的 HTTP 头

WebSocket 协议正是为了解决这些问题而诞生的。它允许服务器主动向客户端推送数据,实现真正的双向通信。

1.2 WebSocket 的特点

  • 全双工通信:客户端和服务器可以同时发送和接收数据
  • 持久连接:一次握手后,连接保持打开状态
  • 低开销:数据帧头部只有 2-10 字节,相比 HTTP 头部大大减少
  • 实时性强:服务器可以随时主动推送数据

二、WebSocket 握手过程

WebSocket 使用 HTTP 协议进行握手,握手成功后切换到 WebSocket 协议进行数据传输。

2.1 客户端握手请求

客户端发送一个特殊的 HTTP 请求来发起 WebSocket 连接:

1
2
3
4
5
6
7
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com

关键字段说明:

字段 说明
Upgrade: websocket 表示要升级到 WebSocket 协议
Connection: Upgrade 表示连接需要升级
Sec-WebSocket-Key 客户端生成的随机 Base64 编码字符串,用于安全校验
Sec-WebSocket-Version WebSocket 协议版本,当前为 13
Origin 请求来源,用于防止跨站攻击

2.2 服务器握手响应

服务器收到请求后,返回如下响应表示同意建立 WebSocket 连接:

1
2
3
4
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

关键字段说明:

字段 说明
101 Switching Protocols 状态码表示协议切换成功
Sec-WebSocket-Accept 服务器根据客户端的 Key 计算得出的值

2.3 Sec-WebSocket-Accept 的计算

Sec-WebSocket-Accept 的计算过程如下:

  1. 将客户端发送的 Sec-WebSocket-Key 与固定的 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接
  2. 对拼接后的字符串进行 SHA-1 哈希
  3. 将哈希结果进行 Base64 编码
1
2
3
4
5
6
7
8
9
import hashlib
import base64

key = "dGhlIHNhbXBsZSBub25jZQ=="
guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

sha1 = hashlib.sha1((key + guid).encode()).digest()
accept = base64.b64encode(sha1).decode()
print(accept) # s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

2.4 握手流程图

1
2
3
4
5
6
7
8
9
10
11
客户端                                    服务器
| |
| 1. HTTP GET (Upgrade: websocket) |
|--------------------------------------->|
| |
| 2. HTTP 101 Switching Protocols |
|<---------------------------------------|
| |
| 3. WebSocket 连接建立,双向通信开始 |
|<======================================>|
| |

三、WebSocket 数据帧格式

握手成功后,数据以帧(Frame)的形式传输。WebSocket 帧的基本格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 0               1               2               3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Extended payload length continued, if payload len == 127 |
+-------------------------------+-------------------------------+
| | Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------+-------------------------------+
| Payload Data continued ... |
+---------------------------------------------------------------+

主要字段说明:

  • FIN:1 bit,表示这是消息的最后一个帧
  • opcode:4 bits,表示帧类型
    • 0x0:继续帧
    • 0x1:文本帧
    • 0x2:二进制帧
    • 0x8:关闭连接
    • 0x9:Ping
    • 0xA:Pong
  • MASK:1 bit,表示数据是否经过掩码处理(客户端发送的数据必须掩码)
  • Payload length:数据长度

四、WebSocket 使用方式

4.1 浏览器端(JavaScript)

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
// 创建 WebSocket 连接
const ws = new WebSocket('ws://example.com/socket');

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

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

// 连接关闭时触发
ws.onclose = function(event) {
console.log('连接已关闭');
};

// 发生错误时触发
ws.onerror = function(error) {
console.error('WebSocket 错误:', error);
};

// 发送消息
ws.send('Hello!');

// 发送 JSON 数据
ws.send(JSON.stringify({ type: 'message', content: 'Hello!' }));

// 关闭连接
ws.close();

4.2 Node.js 服务端

使用 ws 库实现 WebSocket 服务器:

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
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
console.log('新客户端连接');

ws.on('message', function incoming(message) {
console.log('收到消息:', message.toString());

// 向客户端发送消息
ws.send('收到你的消息: ' + message);
});

ws.on('close', function close() {
console.log('客户端断开连接');
});

// 主动发送消息
ws.send('欢迎连接到服务器!');
});

// 广播消息给所有客户端
function broadcast(message) {
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}

4.3 Python 服务端

使用 websockets 库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import asyncio
import websockets

async def handler(websocket, path):
print("新客户端连接")

try:
async for message in websocket:
print(f"收到消息: {message}")
await websocket.send(f"收到: {message}")
except websockets.exceptions.ConnectionClosed:
print("客户端断开连接")

async def main():
async with websockets.serve(handler, "localhost", 8080):
await asyncio.Future() # 永久运行

asyncio.run(main())

4.4 Java Spring Boot 服务端

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
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
public class MyWebSocketHandler extends TextWebSocketHandler {

@Override
public void afterConnectionEstablished(WebSocketSession session) {
System.out.println("新连接: " + session.getId());
}

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
String payload = message.getPayload();
System.out.println("收到消息: " + payload);
session.sendMessage(new TextMessage("收到: " + payload));
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
System.out.println("连接关闭: " + session.getId());
}
}

配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.*;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

private final MyWebSocketHandler handler;

public WebSocketConfig(MyWebSocketHandler handler) {
this.handler = handler;
}

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(handler, "/ws")
.setAllowedOrigins("*");
}
}

五、WebSocket 与 HTTP 的比较

5.1 通信模式

特性 HTTP WebSocket
通信方向 单向(请求-响应) 双向(全双工)
发起方 只能客户端发起 双方都可以发起
连接模式 短连接(HTTP/1.0)或复用连接 长连接
服务器推送 不支持(需轮询或 SSE) 原生支持

5.2 性能对比

特性 HTTP WebSocket
头部开销 每次请求都携带完整头部(数百字节) 数据帧头部仅 2-10 字节
建立连接 每次请求可能需要三次握手 只需一次握手
实时性 依赖轮询间隔 毫秒级实时
服务器负载 轮询产生大量无效请求 按需发送,负载低

5.3 协议对比

1
2
3
4
5
6
7
8
9
HTTP 请求流程:
客户端 --请求1--> 服务器 --响应1--> 客户端
客户端 --请求2--> 服务器 --响应2--> 客户端
客户端 --请求3--> 服务器 --响应3--> 客户端

WebSocket 流程:
客户端 <====== 建立连接 ======> 服务器
<------ 数据双向流动 ----->
<========================>

5.4 使用场景

适合使用 HTTP 的场景:

  • RESTful API
  • 获取静态资源
  • 表单提交
  • 无需实时性的数据请求

适合使用 WebSocket 的场景:

  • 即时通讯(聊天应用)
  • 实时协作(在线文档编辑)
  • 实时数据推送(股票行情、体育比分)
  • 在线游戏
  • 物联网设备通信

5.5 代码对比

HTTP 轮询方式获取实时数据:

1
2
3
4
5
6
// HTTP 轮询 - 效率低
setInterval(async () => {
const response = await fetch('/api/messages');
const messages = await response.json();
updateUI(messages);
}, 1000); // 每秒请求一次

WebSocket 方式获取实时数据:

1
2
3
4
5
6
7
// WebSocket - 高效实时
const ws = new WebSocket('ws://example.com/messages');

ws.onmessage = (event) => {
const messages = JSON.parse(event.data);
updateUI(messages);
};

六、WebSocket 安全性

6.1 WSS(WebSocket Secure)

类似于 HTTPS,WebSocket 也有加密版本 WSS:

1
2
// 使用加密连接
const ws = new WebSocket('wss://example.com/socket');

WSS 使用 TLS 加密,连接 URL 以 wss:// 开头。

6.2 安全建议

  1. 使用 WSS:在生产环境始终使用加密连接
  2. 验证 Origin:检查请求来源,防止跨站 WebSocket 劫持
  3. 身份认证:在握手阶段进行身份验证
  4. 输入验证:对接收的数据进行验证和过滤
  5. 限流:防止恶意客户端发送大量消息

七、总结

WebSocket 是一种强大的实时通信协议,它的主要优势在于:

  1. 低延迟:建立连接后数据可以立即双向传输
  2. 高效率:减少了 HTTP 头部开销和连接建立开销
  3. 真正的双向通信:服务器可以主动推送数据

在选择使用 HTTP 还是 WebSocket 时,需要根据具体场景来决定。对于需要实时双向通信的应用,WebSocket 是更好的选择;而对于传统的请求-响应模式,HTTP 仍然是首选。