WebSocket数据传输机制与帧格式
2021-11-186.3k 阅读
WebSocket概述
在传统的HTTP协议中,通信一般是由客户端发起请求,服务器响应。这种模式在很多场景下能很好地工作,但对于一些实时性要求较高的应用,如在线聊天、实时游戏、股票行情推送等,就显得力不从心。因为HTTP协议是无状态的、单向的,服务器不能主动向客户端发送数据,除非客户端再次发起请求。
WebSocket应运而生,它是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间可以建立持久连接,双方都可以主动发送和接收数据,大大提高了实时性交互的能力。WebSocket协议在2011年被IETF(互联网工程任务组)标准化为RFC 6455。
WebSocket数据传输机制
- 握手过程
- WebSocket的通信建立始于握手过程。客户端首先发起一个HTTP请求,请求头中包含特殊的字段,用于告知服务器这是一个WebSocket连接请求。例如:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec - WebSocket - Key: dGhlIHNhbXBsZSBub25jZQ==
Sec - WebSocket - Version: 13
Upgrade: websocket
和Connection: Upgrade
字段表明客户端希望将连接升级为WebSocket协议。Sec - WebSocket - Key
是一个随机生成的Base64编码字符串,用于服务器验证请求的合法性。Sec - WebSocket - Version
指定客户端支持的WebSocket协议版本。- 服务器接收到请求后,如果支持WebSocket协议,会返回一个响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec - WebSocket - Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
HTTP/1.1 101 Switching Protocols
状态码表示服务器同意将连接升级为WebSocket协议。Sec - WebSocket - Accept
字段是根据客户端发送的Sec - WebSocket - Key
计算得出的,用于证明服务器能够正确处理WebSocket连接。计算方法是将Sec - WebSocket - Key
与一个固定字符串258EAFA5 - E914 - 47DA - 95CA - C5AB0DC85B11
拼接,然后进行SHA - 1哈希计算,最后进行Base64编码。
- 数据传输
- 一旦握手成功,客户端和服务器之间就建立了WebSocket连接,可以进行双向数据传输。WebSocket的数据传输是基于帧的,每个帧包含头部和数据部分。
- 在数据传输过程中,WebSocket会保持连接的活跃状态。如果一段时间内没有数据传输,客户端和服务器可以通过发送ping帧和pong帧来检测连接是否正常。客户端发送ping帧,服务器收到后应回复pong帧;反之,服务器发送ping帧,客户端也应回复pong帧。这有助于及时发现网络故障或连接异常,避免出现“假连接”的情况。
- 连接关闭
- 当一方想要关闭连接时,会发送一个关闭帧。关闭帧包含一个状态码和可选的关闭原因。例如,状态码1000表示正常关闭,1001表示端点正在离开(如服务器重启),1008表示违反了协议规范等。
- 另一方收到关闭帧后,应回复一个关闭帧,然后双方关闭连接。这样的过程确保了连接关闭的有序性,避免数据丢失或资源泄漏。
WebSocket帧格式
- 帧头结构
- WebSocket帧头由2个字节或更多字节组成,其结构如下:
- 第1个字节:
- FIN(1位):表示这是否是消息的最后一帧。如果FIN = 1,说明这是消息的最后一帧;如果FIN = 0,说明还有后续帧。
- RSV1 - RSV3(3位):保留位,必须设置为0,除非协商了扩展协议。
- Opcode(4位):表示帧的类型。常见的Opcode值有:
- 0x0:表示这是一个延续帧,用于将一个长消息分割成多个帧传输。
- 0x1:表示文本帧,数据是UTF - 8编码的文本。
- 0x2:表示二进制帧,数据是二进制格式。
- 0x8:表示关闭帧,用于关闭WebSocket连接。
- 0x9:表示ping帧。
- 0xA:表示pong帧。
- 第2个字节:
- MASK(1位):表示数据是否被掩码。如果MASK = 1,数据部分被掩码;如果MASK = 0,数据部分未被掩码。在客户端到服务器的通信中,MASK必须设置为1;在服务器到客户端的通信中,MASK必须设置为0。
- Payload length(7位):表示有效载荷(数据部分)的长度。如果Payload length的值小于126,那么它就是数据的实际长度。如果Payload length的值为126,那么接下来的2个字节表示数据的长度。如果Payload length的值为127,那么接下来的8个字节表示数据的长度。
- 掩码键和掩码操作
- 当MASK = 1时,接下来的4个字节是掩码键。掩码操作是对数据部分的每个字节与掩码键的相应字节进行异或运算。假设掩码键为
[m0, m1, m2, m3]
,数据部分为[d0, d1, d2, ...]
,则掩码后的字节为[d0 ^ m0, d1 ^ m1, d2 ^ m2, d3 ^ m3, d4 ^ m0, d5 ^ m1, ...]
。这样做是为了防止网络中间件(如代理)对WebSocket数据进行不适当的处理,因为掩码后的数据看起来是随机的,难以被识别和修改。
- 当MASK = 1时,接下来的4个字节是掩码键。掩码操作是对数据部分的每个字节与掩码键的相应字节进行异或运算。假设掩码键为
- 示例帧分析
- 假设我们有一个简单的文本帧,FIN = 1,Opcode = 0x1(文本帧),MASK = 1,Payload length = 5。
- 帧头的第1个字节为:
0x81
(二进制:10000001
,其中FIN = 1,RSV1 - RSV3 = 0,Opcode = 0x1)。 - 帧头的第2个字节为:
0x85
(二进制:10000101
,其中MASK = 1,Payload length = 5)。 - 接下来4个字节是掩码键,假设为
[0x12, 0x34, 0x56, 0x78]
。 - 然后是5个字节的数据,假设原始数据为
[0x48, 0x65, 0x6C, 0x6C, 0x6F]
(即“Hello”的ASCII码)。 - 经过掩码操作后的数据为:
0x48 ^ 0x12 = 0x5A
0x65 ^ 0x34 = 0x51
0x6C ^ 0x56 = 0x3A
0x6C ^ 0x78 = 0x14
0x6F ^ 0x12 = 0x7D
- 完整的帧数据就是:
0x81 0x85 0x12 0x34 0x56 0x78 0x5A 0x51 0x3A 0x14 0x7D
。
代码示例
- 使用Python和Tornado实现WebSocket服务器
- 首先,确保安装了Tornado库。可以使用
pip install tornado
命令进行安装。
- 首先,确保安装了Tornado库。可以使用
import tornado.ioloop
import tornado.web
import tornado.websocket
class WebSocketHandler(tornado.websocket.WebSocketHandler):
def open(self):
print('WebSocket opened')
def on_message(self, message):
self.write_message('You sent: {}'.format(message))
def on_close(self):
print('WebSocket closed')
def make_app():
return tornado.web.Application([
(r"/ws", WebSocketHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
print('Server is running on port 8888')
tornado.ioloop.IOLoop.current().start()
- 在上述代码中,我们定义了一个
WebSocketHandler
类,继承自tornado.websocket.WebSocketHandler
。open
方法在WebSocket连接建立时被调用,on_message
方法在接收到客户端消息时被调用,on_close
方法在连接关闭时被调用。make_app
函数创建了一个Tornado应用,并将/ws
路径映射到WebSocketHandler
。最后,应用在8888端口监听。
- 使用JavaScript实现WebSocket客户端
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF - 8">
<title>WebSocket Client</title>
</head>
<body>
<input type="text" id="messageInput">
<button onclick="sendMessage()">Send</button>
<div id="messageDisplay"></div>
<script>
const socket = new WebSocket('ws://localhost:8888/ws');
socket.onopen = function (event) {
document.getElementById('messageDisplay').innerHTML += 'Connected to server<br>';
};
socket.onmessage = function (event) {
document.getElementById('messageDisplay').innerHTML += 'Received: ' + event.data + '<br>';
};
socket.onclose = function (event) {
document.getElementById('messageDisplay').innerHTML += 'Connection closed<br>';
};
function sendMessage() {
const message = document.getElementById('messageInput').value;
socket.send(message);
document.getElementById('messageInput').value = '';
}
</script>
</body>
</html>
- 上述HTML代码创建了一个简单的WebSocket客户端。当页面加载时,会尝试连接到
ws://localhost:8888/ws
。用户可以在输入框中输入消息,点击“Send”按钮发送消息。接收到服务器的消息或连接状态发生变化时,会在页面上显示相应的信息。
WebSocket在实际项目中的应用场景
- 实时聊天应用
- 无论是即时通讯软件还是网页版的聊天工具,WebSocket都能提供高效的实时通信能力。通过WebSocket,用户发送的消息可以立即被服务器接收并转发给其他相关用户,实现近乎实时的消息传递。在实现过程中,服务器可以维护一个用户连接列表,当收到某个用户的消息时,遍历列表将消息发送给目标用户。例如,在一个多人在线聊天房间中,每个用户的连接都通过WebSocket保持,当一个用户发送消息后,服务器将该消息广播给房间内的所有其他用户。
- 在线游戏
- 对于实时对战类游戏,如多人在线竞技游戏(MOBA)或实时策略游戏(RTS),游戏状态的实时更新至关重要。玩家的操作(如移动角色、释放技能等)需要及时传递给服务器,服务器再将更新后的游戏状态广播给所有玩家。WebSocket的全双工通信特性可以满足这种高频率、低延迟的数据传输需求。以一款简单的在线棋类游戏为例,玩家下棋的动作通过WebSocket发送到服务器,服务器验证动作合法性后,将新的棋盘状态发送给双方玩家,使双方都能实时看到棋局变化。
- 股票行情推送
- 在金融领域,股票行情的实时更新对于投资者至关重要。证券公司的服务器可以通过WebSocket将最新的股票价格、成交量等数据推送给客户端。客户端无需频繁地向服务器发送请求获取数据,减少了网络流量和服务器负载。而且,WebSocket的持久连接特性可以保证数据的实时性和稳定性,投资者能够及时获取到最新的市场信息,做出更准确的投资决策。
WebSocket与其他实时通信技术的比较
- 与轮询技术的比较
- 轮询:轮询是一种简单的实时通信实现方式,客户端定时向服务器发送请求,获取最新数据。例如,在网页上显示实时新闻,客户端可能每隔几秒就向服务器请求一次新的新闻列表。这种方式的优点是实现简单,兼容性好,几乎所有的Web应用都可以使用。然而,它的缺点也很明显,频繁的请求会增加服务器负载和网络流量,尤其是在数据更新不频繁的情况下,很多请求可能获取到的是相同的数据,造成资源浪费。
- WebSocket:WebSocket建立的是持久连接,服务器可以主动推送数据给客户端,只有在有新数据时才进行传输,大大减少了不必要的请求和响应,降低了服务器负载和网络流量。同时,由于是全双工通信,实时性更强,能够满足对实时性要求较高的应用场景。
- 与长轮询技术的比较
- 长轮询:长轮询是对轮询的一种改进。客户端向服务器发送请求后,服务器如果没有新数据,不会立即响应,而是保持连接等待新数据到来,一旦有新数据,立即响应客户端。客户端收到响应后,再次发起请求。这种方式在一定程度上减少了无效请求,提高了实时性。但与WebSocket相比,长轮询仍然是基于HTTP协议的,每次请求和响应都需要携带完整的HTTP头信息,增加了额外的开销。而且,长轮询在服务器端需要为每个请求维护一个连接,当并发用户数较多时,会消耗大量的服务器资源。
- WebSocket:WebSocket使用单一的TCP连接进行全双工通信,在连接建立后,通信双方可以直接发送和接收数据,无需每次都携带HTTP头信息,大大提高了通信效率。同时,WebSocket对并发连接的管理更加高效,能够支持更多的并发用户。
WebSocket性能优化
- 连接管理优化
- 减少连接创建和关闭次数:频繁地创建和关闭WebSocket连接会带来额外的开销,包括握手过程的时间消耗和资源占用。在应用设计中,尽量复用已有的连接。例如,在一个单页应用中,如果有多个模块需要与服务器进行实时通信,可以使用同一个WebSocket连接,而不是为每个模块单独创建连接。
- 合理设置连接超时:设置适当的连接超时时间可以避免长时间占用无效连接。如果一个连接在一段时间内没有数据传输,可以主动关闭连接,释放资源。同时,在客户端和服务器端都要处理好连接超时后的重连机制,确保应用的稳定性。
- 数据传输优化
- 压缩数据:对于较大的数据传输,可以使用压缩算法对数据进行压缩,减少数据在网络上传输的大小。例如,在传输文本数据时,可以使用gzip压缩算法。在WebSocket协议中,虽然没有内置压缩功能,但可以通过在应用层添加压缩和解压缩逻辑来实现。
- 优化帧的使用:根据数据的大小和类型,合理选择帧的类型和分割方式。对于较长的消息,可以分割成多个延续帧进行传输,避免单个帧过大导致网络拥塞。同时,尽量减少不必要的帧(如空帧)的发送,提高数据传输效率。
- 服务器端优化
- 负载均衡:当有大量客户端连接到服务器时,使用负载均衡器可以将请求均匀分配到多个服务器实例上,避免单个服务器过载。常见的负载均衡算法有轮询、加权轮询、最少连接数等。可以根据实际应用场景选择合适的负载均衡算法。
- 使用高性能的服务器框架:选择性能优良的服务器框架对于提升WebSocket服务的性能至关重要。例如,在Python中,除了Tornado,还有Django Channels等框架也支持WebSocket。不同的框架在性能、功能和易用性方面各有特点,需要根据项目需求进行选择和优化。
WebSocket安全性考虑
- 防止中间人攻击
- 使用TLS/SSL加密:通过使用TLS/SSL协议对WebSocket连接进行加密,可以防止中间人在传输过程中窃取或篡改数据。在建立WebSocket连接时,可以使用
wss://
协议代替ws://
协议,这样浏览器会自动进行TLS/SSL握手,确保数据传输的安全性。例如,在部署WebSocket服务器时,可以配置SSL证书,使得客户端与服务器之间的通信通过加密通道进行。 - 验证服务器身份:客户端在连接服务器时,应验证服务器的身份,防止连接到伪造的服务器。可以通过检查服务器的SSL证书来验证服务器身份。在JavaScript中,当使用
new WebSocket('wss://...')
时,浏览器会自动验证服务器的SSL证书,如果证书无效,会抛出安全错误。
- 使用TLS/SSL加密:通过使用TLS/SSL协议对WebSocket连接进行加密,可以防止中间人在传输过程中窃取或篡改数据。在建立WebSocket连接时,可以使用
- 防范恶意连接和数据攻击
- 限制连接数量:服务器应设置最大连接数限制,防止恶意用户通过大量连接耗尽服务器资源。可以在服务器配置中设置允许的最大WebSocket连接数,当连接数达到上限时,拒绝新的连接请求。
- 验证和过滤输入数据:对于客户端发送到服务器的数据,服务器应进行严格的验证和过滤,防止恶意数据注入。例如,在处理文本帧数据时,要检查数据是否符合预期的格式和长度,防止SQL注入、XSS攻击等。
- 安全的关闭连接
- 按照WebSocket协议规范进行连接关闭,确保双方都能有序地关闭连接,避免数据丢失或资源泄漏。在发送关闭帧时,应包含正确的状态码和关闭原因,让对方能够清楚了解关闭的原因。同时,在接收到关闭帧后,要及时回复关闭帧并关闭连接。
WebSocket的扩展与未来发展
- 协议扩展
- 虽然WebSocket协议已经提供了基本的全双工通信功能,但在一些特定场景下,可能需要对协议进行扩展。例如,为了支持更高效的数据压缩、多路复用(在一个WebSocket连接上同时传输多个数据流)等功能,可以通过自定义扩展协议来实现。目前已经有一些关于WebSocket扩展的研究和实践,如WebSocket Compression Extension用于数据压缩,WebSocket Multiplexing Extension用于多路复用。
- 与其他技术的融合
- 与HTTP/3的结合:随着HTTP/3的发展,WebSocket有可能与HTTP/3更好地结合。HTTP/3基于UDP协议,具有更低的延迟和更高的传输效率。WebSocket可以利用HTTP/3的优势,进一步提升性能,特别是在网络条件较差的情况下。
- 与边缘计算的融合:边缘计算将计算和存储资源推向网络边缘,靠近用户设备。WebSocket可以与边缘计算技术结合,在边缘节点处理实时数据,减少数据传输到中心服务器的延迟,提高实时应用的响应速度。例如,在物联网场景中,设备产生的大量实时数据可以在边缘节点通过WebSocket进行初步处理和转发,只有关键数据才传输到中心服务器。
- 应用场景的拓展
- 虚拟现实(VR)和增强现实(AR):在VR和AR应用中,实时数据传输对于提供沉浸式体验至关重要。WebSocket可以用于传输用户的动作数据、场景更新数据等,实现多用户在虚拟环境中的实时交互。例如,在多人在线VR游戏中,玩家的位置、动作等信息可以通过WebSocket实时传输给其他玩家,使游戏体验更加真实和流畅。
- 工业物联网(IIoT):在工业领域,设备之间需要进行实时的状态监测和控制。WebSocket可以用于连接工业设备与监控系统,实现设备状态的实时上传和控制指令的实时下发。例如,工厂中的机器设备可以通过WebSocket将运行数据(如温度、压力等)实时发送给监控中心,监控中心也可以通过WebSocket向设备发送调整参数或维修指令。