什么是 WebSocket ?

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

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

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

随着新标准的推进,WebSocket 已经比较成熟了,并且各个浏览器对 WebSocket 的支持情况比较好,有空可以看看。

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

客户端请求报文及实现

客户端请求报文:

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

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

Upgrade: websocket Connection: Upgrade

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

Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

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

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

创建 WebSocket 对象:

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

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

完整的客户端代码:

<script type="text/javascript">
   var ws;
   var box = document.getElementById('box');

function startWS() {
       ws = new WebSocket('ws://127.0.0.1:8001');
       ws.onopen = function (msg) {
           console.log('WebSocket opened!');
       };
       ws.onmessage = function (message) {
           console.log('receive message: ' + message.data);
           box.insertAdjacentHTML('beforeend', '<p>' + message.data + '</p>');
       };
       ws.onerror = function (error) {
           console.log('Error: ' + error.name + error.number);
       };
       ws.onclose = function () {
           console.log('WebSocket closed!');
       };
   }

function sendMessage() {
       console.log('Sending a message...');
       var text = document.getElementById('text');
       ws.send(text.value);
   }

window.onbeforeunload = function () {
       websocket.onclose = function () {};  // 首先关闭 WebSocket
       websocket.close()
   };
</script>

服务端响应报文及实现

首先我们来看看服务端的响应报文

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

我们一行行来解释

1、首先,101 状态码表示服务器已经理解了客户端的请求,并将通过 Upgrade 消息头通知客户端采用不同的协议来完成这个请求;

2、然后,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key

3、最后,Sec-WebSocket-Protocol 则是表示最终使用的协议。

Sec-WebSocket-Accept 的计算方法:

1、将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;

2、通过 SHA1 计算出摘要,并转成 base64 字符串。

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

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

def create_socket():
   # 启动 Socket 并监听连接
   sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   try:
       sock.bind(('127.0.0.1', 8001))

# 操作系统会在服务器 Socket 被关闭或服务器进程终止后马上释放该服务器的端口,否则操作系统会保留几分钟该端口。
       sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
       sock.listen(5)
   except Exception as e:
       logging.error(e)
       return
   else:
       logging.info('Server running...')

# 等待访问
   while True:
       conn, addr = sock.accept()  # 此时会进入 waiting 状态

data = str(conn.recv(1024))
       logging.debug(data)

header_dict = {}
       header, _ = data.split(r'\r\n\r\n', 1)
       for line in header.split(r'\r\n')[1:]:
           key, val = line.split(': ', 1)
           header_dict[key] = val

if 'Sec-WebSocket-Key' not in header_dict:
           logging.error('This socket is not websocket, client close.')
           conn.close()
           return

magic_key = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
       sec_key = header_dict['Sec-WebSocket-Key'] + magic_key
       key = base64.b64encode(hashlib.sha1(bytes(sec_key, encoding='utf-8')).digest())
       key_str = str(key)[2:30]
       logging.debug(key_str)

response = 'HTTP/1.1 101 Switching Protocols\r\n' \
                  'Connection: Upgrade\r\n' \
                  'Upgrade: websocket\r\n' \
                  'Sec-WebSocket-Accept: {0}\r\n' \
                  'WebSocket-Protocol: chat\r\n\r\n'.format(key_str)
       conn.send(bytes(response, encoding='utf-8'))

logging.debug('Send the handshake data')

WebSocketThread(conn).start()

进行通信解析 WebSocket 报文及实现

Server 端接收到浏览器发来的报文需要进行解析

浏览器包格式

1、FIN: 占 1 个 bit

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

2、RSV1, RSV2, RSV3:各占 1 个 bit

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

3、Opcode: 4 个 bit

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

4、Mask: 1 个 bit

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

5、Payload length: 7bit or 7 + 16bit or 7 + 64bit

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

6、Masking-key: 0 or 4bytes

当 Mask 为 1,则携带了 4 字节的 Masking-key;
当 Mask 为 0,则没有 Masking-key。
PS:掩码的作用并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。

7、Payload Data: 载荷数据

解析 WebSocket 报文代码如下:

def read_msg(data):
   logging.debug(data)

msg_len = data[1] & 127  # 数据载荷的长度
   if msg_len == 126:
       mask = data[4:8]  # Mask 掩码
       content = data[8:]  # 消息内容
   elif msg_len == 127:
       mask = data[10:14]
       content = data[14:]
   else:
       mask = data[2:6]
       content = data[6:]

raw_str = ''  # 解码后的内容
   for i, d in enumerate(content):
       raw_str += chr(d ^ mask[i % 4])
   return raw_str

服务端发送 WebSocket 报文

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

struct 模块解析

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

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

struct 中支持的格式如下表:

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

发送 WebSocket 报文代码如下:

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

# 写入包长度
   msg_len = len(message)
   if msg_len <= 125:
       data += struct.pack('B', msg_len)
   elif msg_len <= (2 ** 16 - 1):
       data += struct.pack('!BH', 126, msg_len)
   elif msg_len <= (2 ** 64 - 1):
       data += struct.pack('!BQ', 127, msg_len)
   else:
       logging.error('Message is too long!')
       return

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

总结

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

1 、多个用户之间进行交互;

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

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

参考文章

https://www.zhihu.com/question/20215561/answer/40316953

http://www.52im.net/thread-1341-1-1.html

http://fullstackpython.atjiang.com/websockets.html

来源: wzhvictor     链接:

https://segmentfault.com/a/1190000014643900

一文读懂 WebSocket 通信过程与实现相关推荐

  1. 一文读懂PFMEA(过程失效模式及后果分析)

    PFMEA是过程失效模式及后果分析(Process Failure Mode andEffects Analysis)的英文简称,是由负责制造/装配的工程师/小组主要采用的一种分析技术,用以最大限度地 ...

  2. 计算机网络——一文读懂DNS域名解析过程

    一.域名与IP地址 互联网的每一台主机都是由IP地址来标识位置的,IP地址要么是纯数字,要么就是数字加字母.要记下来实在是太痛苦了,于是就有了将IP地址就和域名关联起来,我们只需要记下域名就可以了.在 ...

  3. 串口通信 校验码_一文读懂S7-200 SMART自由口通信!

    学习S7-200 SMART时了解到,基于RS485接口可实现一下几种通信: 1)modbus RTU通信 2)PPI协议通信 3)USS协议通信 4)自由口通信 何为自由口通信呢? 前三种通信必须要 ...

  4. 量子计算机芯片的制造过程,全干货!一文读懂芯片制造及量子芯片!

    原标题:全干货!一文读懂芯片制造及量子芯片! 最近两个月,因为一系列事情,大家对国内芯片产业的关注度日益增加. 那么,什么是芯片?如何制造芯片?涉及到多少高科技?目前的芯片产业将会有哪些挑战? 在这里 ...

  5. 一文读懂序列建模(deeplearning.ai)之序列模型与注意力机制

    https://www.toutiao.com/a6663809864260649485/ 作者:Pulkit Sharma,2019年1月21日 翻译:陈之炎 校对:丁楠雅 本文约11000字,建议 ...

  6. 一文读懂你该了解的5G知识:现在别买5G手机

    来源: 腾讯科技 2019年是中国全力布局5G的一年:三大运营商纷纷搭建基站,手机厂商发布5G手机,部分城市已经开启了5G测试--在电信日这天,腾讯科技联合知乎推出重磅策划,聚焦和5G相关的小知识,精 ...

  7. 一文读懂HTTP/2及HTTP/3特性

    前言 HTTP/2 相比于 HTTP/1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作,当然兼容问题以及如何优雅降级应该是国内还不普遍使用的原因之一. 虽然 ...

  8. 区块链产业生态、存在问题及政策建议|一文读懂新趋势

    区块链产业生态.存在问题及政策建议|一文读懂新趋势 2017-03-03 09:47:50  来源: 腾讯研究院抢沙发 摘要:从技术上来讲,区块链是一种分布式的记账方法.说到记账,我们经历了从实物记账 ...

  9. 从根上理解高性能、高并发(七):深入操作系统,一文读懂进程、线程、协程

    本文引用了"一文读懂什么是进程.线程.协程"一文的主要内容,感谢原作者的无私分享. 1.系列文章引言 1.1 文章目的 作为即时通讯技术的开发者来说,高性能.高并发相关的技术概念早 ...

最新文章

  1. 用意念就能控制的操作系统?这家华人团队做的脑机接口,3个电极实现脑波成像...
  2. jQuery AJAX 网页无刷新上传示例
  3. python 预编译命令_Python子进程调用预编译j
  4. 三菱伺服自动调谐_三菱伺服在执行离线自动调整之前
  5. python中属于有序序列的有_Python中的有序序列有哪些
  6. 字符串指针的地址的传递
  7. 经验原石_经验分享:挑战翡翠原石,新手实战前应做哪些准备
  8. 关于ipcam的UPnP或NAT的知识
  9. MySQL从入门到精通详细教程
  10. win7计算机桌面快捷键显示桌面,win7系统右击桌面快捷小工具使用介绍
  11. java 集合底层_java集合底层实现总结
  12. Kernel:CC_HAVE_ASM_GOTO 、 Compiler lacks asm-goto support
  13. java doc 转 pdf_java doc转pdf(示例代码)
  14. 2020年6月电子学会Python等级考试试卷(三级)考题解析
  15. 为什么前端UI设计师给750PX的2倍设计稿?
  16. 零基础到入门React SSR
  17. 国内首届中文人机对话技术评测赛果出炉,两项任务冠军团队都分享了哪些技术细节?...
  18. mac tcp调试命令
  19. windows 10必装的五款神器
  20. 国庆长假大事记:百度地图、先锋系、火箭队

热门文章

  1. 猿团宣布加入中国信息无障碍产品联盟,致力信息无障碍化公益事业
  2. 完美云主机购买小技巧分享
  3. Ubuntu 16.04安装有道词典
  4. 工程升级技术的学习和使用
  5. 基于STM32设计的智能家居控制系统设计_语音+环境检测(OneNet)_2022
  6. evo测试工具错误: evo module evo.main_traj crashed - no logfile written (disabled)
  7. com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value '�й���׼ʱ��'
  8. 安全教育APP开发应该具备哪些功能
  9. 用Spring Security做分布式权限管理 - 卷一基本功
  10. 各地相继设立交易所,抢滩大数据交易市场