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 的计算过程如下:
- 将客户端发送的
Sec-WebSocket-Key 与固定的 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接
- 对拼接后的字符串进行 SHA-1 哈希
- 将哈希结果进行 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)
|
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
| 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!');
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
| setInterval(async () => { const response = await fetch('/api/messages'); const messages = await response.json(); updateUI(messages); }, 1000);
|
WebSocket 方式获取实时数据:
1 2 3 4 5 6 7
| 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 安全建议
- 使用 WSS:在生产环境始终使用加密连接
- 验证 Origin:检查请求来源,防止跨站 WebSocket 劫持
- 身份认证:在握手阶段进行身份验证
- 输入验证:对接收的数据进行验证和过滤
- 限流:防止恶意客户端发送大量消息
七、总结
WebSocket 是一种强大的实时通信协议,它的主要优势在于:
- 低延迟:建立连接后数据可以立即双向传输
- 高效率:减少了 HTTP 头部开销和连接建立开销
- 真正的双向通信:服务器可以主动推送数据
在选择使用 HTTP 还是 WebSocket 时,需要根据具体场景来决定。对于需要实时双向通信的应用,WebSocket 是更好的选择;而对于传统的请求-响应模式,HTTP 仍然是首选。