什么是 WebSocket ?

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

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

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

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

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

客户端请求报文 Header

客户端请求报文:

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 () {
        ws.onclose = function () {}; // 首先关闭 WebSocket
        ws.close()
</script>
 

服务端响应报文 Header

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

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 字符串。

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

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

def create_socket():
 # 启动 Socket 并监听连接
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        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'rnrn', 1)
 for line in header.split(r'rn')[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 Protocolsrn' 
 'Connection: Upgradern' 
 'Upgrade: websocketrn' 
 'Sec-WebSocket-Accept: {0}rn' 
 'WebSocket-Protocol: chatrnrn'.format(key_str)
        conn.send(bytes(response, encoding='utf-8'))
        logging.debug('Send the handshake data')
 WebSocketThread(conn).start()
 

服务端解析 WebSocket 报文

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

Client 包格式

  • 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 报文代码如下:

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 中支持的格式如下表:

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
  1. 多个用户之间进行交互;
  2. 需要频繁地向服务端请求更新数据。

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

以上内容希望帮助到大家,很多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家 ,需要请戳这里链接 或 者关注咱们下面的知乎专栏 PHP架构师圈子​zhuanlan.zhihu.com 什么是 WebSocket ?WebSocket 是一种标准协议,用于在客户端和服务端之间进行双向数据传输。但它跟 HTTP 没什么关系,它是基于 TCP 的一种独立实现。以前客户端想知道服务端的处理进度,要不停地使用 Ajax 进行轮询,让浏览器隔个几秒就向服务器发一次请求,这对服务器压力较大。另外一种轮询就是采用 long poll 的方式,这就跟打电话差不多,没收到消息就一直不挂电话,也就是... JS通过WebSocket实现双屏信息同步显示 最近项目里有个新的小需求,就是主机打开一个网页Demo,连接的安卓设备打开同一个网页Demo,两个页面是一样的,简单来说就是一个网页Demo打开两个页面A和B,在页面A中操作什么页面B中就显示什么,反之亦然。 JS代码 <!DOCTYPE html> <title>测试网页</title> <meta http-equiv="Content-
开始学习nodejs,并不懂很多东西的使用,尤其是websocket这个东西。 通过网上搜索发现websocket是异步并且没有同步接口。这个就让人很头疼,因为我做的项目需求必须整个流程达到同步效果。 经过长时间的查找,居然发现了一个类似于c++里面signal的库,真的是让我欣喜若狂,而且使用很方便,下面介绍一下这个库和使用方法。 库路径:https://github.com/akira-...
1.TCP协议的三次握手与四次挥手 (1)TCP连接建立过程中采用“三次握手机制” 第一次握手:客户端发出连接请求报文段,其中将SYN标志位置为1表示要建立连接,选择一个初始序列号seq=x,不携带数据但消耗一个序号。之后TCP客户进程进入SYN-SENT(同步已发送)状态。 第二次握手:服务器收到连接请求报文段后,如同意建立连接,向客户端发送确认。在确认报文段中将SYN位和ACK位都置1,确认号是ack=x+1,选择一个初始序号seq=y,不携带数据但消耗一个序号。TCP服务器进程进入SYN-RCV 什么是实时有序数据同步? 基本上,当您同时从多个位置发送数据时,每个人都需要以相同的顺序接收和处理数据。 当事情在远程和同时完成时,顺序就不会得到保证,Homesynck会尝试强制执行自己的顺序,以便每个人都可以同意。 编写软件时,订购问题可能很关键。 以下是一些具体示例: 远程文件同步 协作应用(例如Google Docs克隆) 顺序敏感的消息交换(例如消息传递应用程序) 回合制视频游戏 它是如何工作的? 客户端将消息发送到Homesynck服务器上托管的目录。 Homesynck会记录每个目录接收消息的顺序。 然后,它将消息发送回连接到目录的客户端,并指示应按什么顺序处理它 background-image: url(); background-repeat: no-repeat; animation: play02 1.4s steps(8) infinite; width:200px; height:200px; </style> <style> @keyframes play02{ background-position: 0 0; 100% { background-position: -1600px 0; </style> 当页面数据修改时,会通过后端保存方法存进数据库,这样我们就要一个入口,当数据保存方法被调用执行完后(AOP后置通知),触发webSocket消息机制,向前端发送更新提示,前端调用更新方法进行页面更新. 实现过程: