WebSocket 通信过程与实现

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 什么是 WebSocket ? WebSocket 是一种标准协议,用于在客户端和服务端之间进行双向数据传输。但它跟 HTTP 没什么关系,它是基于 TCP 的一种独立实现。 以前客户端想知道服务端的处理进度,要不停地使用 Ajax 进行轮询,让浏览器隔个几秒就向服务器发一次请求,这对服务器压力较大。

什么是 WebSocket ?

WebSocket 是一种标准协议,用于在客户端和服务端之间进行双向数据传输。但它跟 HTTP 没什么关系,它是基于 TCP 的一种独立实现。

以前客户端想知道服务端的处理进度,要不停地使用 Ajax 进行轮询,让浏览器隔个几秒就向服务器发一次请求,这对服务器压力较大。另外一种轮询就是采用 long poll 的方式,这就跟打电话差不多,没收到消息就一直不挂电话,也就是说,客户端发起连接后,如果没消息,就一直不返回 Response 给客户端,连接阶段一直是阻塞的。

而 WebSocket 解决了 HTTP 的这几个难题。当服务器完成协议升级后( HTTP -> WebSocket ),服务端可以主动推送信息给客户端,解决了轮询造成的同步延迟问题。由于 WebSocket 只需要一次 HTTP 握手,服务端就能一直与客户端保持通信,直到关闭连接,这样就解决了服务器需要反复解析 HTTP 协议,减少了资源的开销。

f9aa30e75c8896256ea119d068c47b47131ce2f9

随着新标准的推进,WebSocket 已经比较成熟了,并且各个主流浏览器对 WebSocket 的支持情况比较好(不兼容低版本 IE,IE 10 以下),有空可以看看。

b86b0c4c571b9b7e2a79b25d0874262455165b43

使用 WebSocket 的时候,前端使用是比较规范的,js 支持 ws 协议,感觉类似于一个轻度封装的 Socket 协议,只是以前需要自己维护 Socket 的连接,现在能够以比较标准的方法来进行。

下面我们就结合上图具体来聊一下 WebSocket 的通信过程。

建立连接

客户端请求报文 Header

客户端请求报文:

 
  1. GET / HTTP/1.1

  2. Upgrade: websocket

  3. Connection: Upgrade

  4. Host: example.com

  5. Origin: http://example.com

  6. Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==

  7. Sec-WebSocket-Version: 13

与传统 HTTP 报文不同的地方:

 
  1. Upgrade: websocket

  2. Connection: Upgrade

这两行表示发起的是 WebSocket 协议。

 
  1. Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==

  2. Sec-WebSocket-Version: 13

Sec-WebSocket-Key 是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。

Sec-WebSocket-Version 表示 WebSocket 的版本,最初 WebSocket 协议太多,不同厂商都有自己的协议版本,不过现在已经定下来了。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。

创建 WebSocket 对象:

 
  1. var ws = new websocket("ws://127.0.0.1:8001");

ws 表示使用 WebSocket 协议,后面接地址及端口

完整的客户端代码:

 
  1. <script type="text/javascript">

  2. var ws;

  3. var box = document.getElementById('box');

  4. function startWS() {

  5. ws = new WebSocket('ws://127.0.0.1:8001');

  6. ws.onopen = function (msg) {

  7. console.log('WebSocket opened!');

  8. };

  9. ws.onmessage = function (message) {

  10. console.log('receive message: ' + message.data);

  11. box.insertAdjacentHTML('beforeend', '<p>' + message.data + '</p>');

  12. };

  13. ws.onerror = function (error) {

  14. console.log('Error: ' + error.name + error.number);

  15. };

  16. ws.onclose = function () {

  17. console.log('WebSocket closed!');

  18. };

  19. }

  20. function sendMessage() {

  21. console.log('Sending a message...');

  22. var text = document.getElementById('text');

  23. ws.send(text.value);

  24. }

  25. window.onbeforeunload = function () {

  26. ws.onclose = function () {}; // 首先关闭 WebSocket

  27. ws.close()

  28. };

  29. </script>

服务端响应报文 Header
首先我们来看看服务端的响应报文:
 
  1. HTTP/1.1 101 Switching Protocols

  2. Upgrade: websocket

  3. Connection: Upgrade

  4. Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

  5. Sec-WebSocket-Protocol: chat

我们一行行来解释:

首先,101 状态码表示服务器已经理解了客户端的请求,并将通过 Upgrade 消息头通知客户端采用不同的协议来完成这个请求;
然后, Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key;
最后, Sec-WebSocket-Protocol 则是表示最终使用的协议。

Sec-WebSocket-Accept 的计算方法:

将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
通过 SHA1 计算出摘要,并转成 base64 字符串。

注意: Sec-WebSocket-Key/ Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端 / 服务端是否合法的 ws 客户端、ws 服务端,其实并没有实际性的保证。

创建主线程,用于实现接受 WebSocket 建立请求:

 
  1. def create_socket():

  2. # 启动 Socket 并监听连接

  3. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

  4. try:

  5. sock.bind(('127.0.0.1', 8001))

  6. # 操作系统会在服务器 Socket 被关闭或服务器进程终止后马上释放该服务器的端口,否则操作系统会保留几分钟该端口。

  7. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

  8. sock.listen(5)

  9. except Exception as e:

  10. logging.error(e)

  11. return

  12. else:

  13. logging.info('Server running...')

  14. # 等待访问

  15. while True:

  16. conn, addr = sock.accept() # 此时会进入 waiting 状态

  17. data = str(conn.recv(1024))

  18. logging.debug(data)

  19. header_dict = {}

  20. header, _ = data.split(r'\r\n\r\n', 1)

  21. for line in header.split(r'\r\n')[1:]:

  22. key, val = line.split(': ', 1)

  23. header_dict[key] = val

  24. if 'Sec-WebSocket-Key' not in header_dict:

  25. logging.error('This socket is not websocket, client close.')

  26. conn.close()

  27. return

  28. magic_key = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'

  29. sec_key = header_dict['Sec-WebSocket-Key'] + magic_key

  30. key = base64.b64encode(hashlib.sha1(bytes(sec_key, encoding='utf-8')).digest())

  31. key_str = str(key)[2:30]

  32. logging.debug(key_str)

  33. response = 'HTTP/1.1 101 Switching Protocols\r\n' \

  34. 'Connection: Upgrade\r\n' \

  35. 'Upgrade: websocket\r\n' \

  36. 'Sec-WebSocket-Accept: {0}\r\n' \

  37. 'WebSocket-Protocol: chat\r\n\r\n'.format(key_str)

  38. conn.send(bytes(response, encoding='utf-8'))

  39. logging.debug('Send the handshake data')

  40. WebSocketThread(conn).start()

进行通信

服务端解析 WebSocket 报文
Server 端接收到 Client 发来的报文需要进行解析

Client 包格式

b370979206987ecff4512ed8f73e5140474bdf5c


1.FIN: 占 1bit

● 0:不是消息的最后一个分片
● 1:是消息的最后一个分片

2.RSV1, RSV2, RSV3:各占 1bit

一般情况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位可以非 0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用 WebSocket 扩展,连接出错。

3.Opcode: 4bit

● %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片;
● %x1:表示这是一个文本帧(text frame);
● %x2:表示这是一个二进制帧(binary frame);
● %x3-7:保留的操作代码,用于后续定义的非控制帧;
● %x8:表示连接断开;
● %x9:表示这是一个心跳请求(ping);
● %xA:表示这是一个心跳响应(pong);
● %xB-F:保留的操作代码,用于后续定义的控制帧。

4.Mask: 1bit

表示是否要对数据载荷进行掩码异或操作。

● 0:否
● 1:是

5.Payload length: 7bit or (7 + 16)bit or (7 + 64)bit

表示数据载荷的长度。

● 0~126:数据的长度等于该值;
● 126:后续 2 个字节代表一个 16 位的无符号整数,该无符号整数的值为数据的长度;
● 127:后续 8 个字节代表一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度。

6.Masking-key: 0 or 4bytes

● 当 Mask 为 1,则携带了 4 字节的 Masking-key;
● 当 Mask 为 0,则没有 Masking-key。
● 掩码算法:按位做循环异或运算,先对该位的索引取模来获得 Masking-key 中对应的值 x,然后对该位与 x 做异或,从而得到真实的 byte 数据。

注意:掩码的作用并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。

7.Payload Data: 载荷数据

解析 WebSocket 报文代码如下:

 
  1. def read_msg(data):

  2. logging.debug(data)

  3. msg_len = data[1] & 127 # 数据载荷的长度

  4. if msg_len == 126:

  5. mask = data[4:8] # Mask 掩码

  6. content = data[8:] # 消息内容

  7. elif msg_len == 127:

  8. mask = data[10:14]

  9. content = data[14:]

  10. else:

  11. mask = data[2:6]

  12. content = data[6:]

  13. raw_str = '' # 解码后的内容

  14. for i, d in enumerate(content):

  15. raw_str += chr(d ^ mask[i % 4])

  16. return raw_str

服务端发送 WebSocket 报文

返回时不携带掩码,所以 Mask 位为 0,再按载荷数据的大小写入长度,最后写入载荷数据。

struct 模块解析

 
  1. struct.pack(fmt, v1, v2, ...)

按照给定的格式 fmt,把数据封装成字符串 ( 实际上是类似于 C 结构体的字节流 )

struct 中支持的格式如下表:

Format C Type Python type Standard size
x pad byte no value
c char bytes of length 1 1
b signed char integer 1
B unsigned char integer 1
? _Bool bool 1
h short integer 2
H unsigned short integer 2
i int integer 4
I unsigned int integer 4
l long integer 4
L unsigned long integer 4
q long long integer 8
Q unsigned long long integer 8
n ssize_t integer
N size_t integer
e -7 float 2
f float float 4
d double float 8
s char[] bytes
p char[] bytes
P void * integer

为了同 C 语言中的结构体交换数据,还要考虑有的 C 或 C++ 编译器使用了字节对齐,通常是以 4 个字节为单位的 32 位系统,故而 struct 根据本地机器字节顺序转换。可以用格式中的第一个字符来改变对齐方式,定义如下:

Character Byte order Size Alignment
@ native native native
= native standard none
< little-endian standard none
> big-endian standard none
! network (= big-endian) standard none

发送 WebSocket 报文代码如下:

 
  1. def write_msg(message):

  2. data = struct.pack('B', 129) # 写入第一个字节,10000001

  3. # 写入包长度

  4. msg_len = len(message)

  5. if msg_len <= 125:

  6. data += struct.pack('B', msg_len)

  7. elif msg_len <= (2 ** 16 - 1):

  8. data += struct.pack('!BH', 126, msg_len)

  9. elif msg_len <= (2 ** 64 - 1):

  10. data += struct.pack('!BQ', 127, msg_len)

  11. else:

  12. logging.error('Message is too long!')

  13. return

  14. data += bytes(message, encoding='utf-8') # 写入消息内容

  15. logging.debug(data)

  16. return data

总结

没有其他能像 WebSocket 一样实现全双工传输的技术了,迄今为止,大部分开发者还是使用 Ajax 轮询来实现,但这是个不太优雅的解决办法,WebSocket 虽然用的人不多,可能是因为协议刚出来的时候有安全性的问题以及兼容的浏览器比较少,但现在都有解决。如果你有这些需求可以考虑使用 WebSocket:

多个用户之间进行交互;

需要频繁地向服务端请求更新数据。

比如弹幕、消息订阅、多玩家游戏、协同编辑、股票基金实时报价、视频会议、在线教育等需要高实时的场景。


原文发布时间为:2018-09-10

本文来自云栖社区合作伙伴“前端大学”,了解相关信息可以关注“前端大学”。

相关文章
|
4月前
|
前端开发 网络协议 JavaScript
在Spring Boot中实现基于WebSocket的实时通信
在Spring Boot中实现基于WebSocket的实时通信
|
27天前
|
Kubernetes Cloud Native JavaScript
为使用WebSocket构建的双向通信应用带来基于服务网格的全链路灰度
介绍如何使用为基于WebSocket的云原生应用构建全链路灰度方案。
|
6月前
|
网络协议 JavaScript 前端开发
WebSocket:实现客户端与服务器实时通信的技术
WebSocket:实现客户端与服务器实时通信的技术
|
6月前
|
网络协议 Java Go
【Go语言专栏】Go语言中的WebSocket实时通信应用
【4月更文挑战第30天】Go语言(Golang)是Google开发的编程语言,适用于云计算、微服务等领域。本文介绍了WebSocket,一种实现浏览器与服务器全双工通信的协议,其特点是实时性、全双工和轻量级。在Go中实现WebSocket,可以使用gorilla/websocket库。示例展示了如何创建服务器端和客户端,实现消息的收发。WebSocket广泛应用于聊天、游戏、通知推送和实时数据同步等场景。学习Go语言中的WebSocket对于开发实时通信应用至关重要。
239 0
|
4月前
|
前端开发 JavaScript API
探索Python Django中的WebSocket集成:为前后端分离应用添加实时通信功能
【7月更文挑战第17天】现代Web开发趋势中,前后端分离配合WebSocket满足实时通信需求。Django Channels扩展了Django,支持WebSocket连接和异步功能。通过安装Channels、配置设置、定义路由和消费者,能在Django中实现WebSocket交互。前端使用WebSocket API连接后端,实现双向数据流,如在线聊天功能。集成Channels提升Web应用的实时性和用户体验,适应实时交互场景的需求。**
193 6
|
4月前
|
安全 数据安全/隐私保护 UED
优化用户体验:前后端分离架构下Python WebSocket实时通信的性能考量
【7月更文挑战第17天】前后端分离趋势下,WebSocket成为实时通信的关键,Python有`websockets`等库支持WebSocket服务。与HTTP轮询相比,WebSocket减少延迟,提高响应。连接管理、消息传输效率、并发处理及安全性是性能考量重点。使用WebSocket能优化用户体验,尤其适合社交、游戏等实时场景。开发应考虑场景需求,充分利用WebSocket优势。
151 3
|
4月前
|
前端开发 Python
前后端分离的进化:Python Web项目中的WebSocket实时通信解决方案
【7月更文挑战第18天】在Python的Flask框架中,结合Flask-SocketIO库可轻松实现WebSocket实时通信,促进前后端分离项目中的高效交互。示例展示了一个简单的聊天应用:Flask路由渲染HTML,客户端通过Socket.IO库连接服务器,发送消息并监听广播。此方法支持多种实时通信协议,适应不同环境,提供流畅的实时体验。
92 3
|
4月前
|
JavaScript 前端开发 UED
WebSocket在Python Web开发中的革新应用:解锁实时通信的新可能
【7月更文挑战第16天】WebSocket是实现Web实时通信的协议,与HTTP不同,它提供持久双向连接,允许服务器主动推送数据。Python有多种库如websockets和Flask-SocketIO支持WebSocket开发。使用Flask-SocketIO的简单示例包括定义路由、监听消息事件,并在HTML中用JavaScript建立连接。WebSocket提高了实时性、减少了服务器压力,广泛应用于聊天、游戏等场景。
47 1
|
4月前
|
移动开发 前端开发 网络协议
Python Web实时通信新纪元:基于WebSocket的前后端分离技术探索
【7月更文挑战第16天】WebSocket增强Web实时性,Python借助Flask-SocketIO简化实现。安装`flask`和`flask-socketio`,示例展示服务器端接收连接及消息并广播响应,前端HTML用Socket.IO库连接并监听事件。WebSocket开启双向通信新时代,助力动态Web应用开发。
53 1
|
3月前
|
网络协议 Go
[golang]gin框架接收websocket通信
[golang]gin框架接收websocket通信
105 0