更加通用的实现文章看这个就行了: https://blog.csdn.net/qq_39540028/article/details/104493049
以下是我们项目中的内容和补充细节
1. C++需要实现WebSocket功能
太古2总算活下去了,小程序也开始流行起来。要实现小程序端,分一杯羹。由于小程序用到了wss,所以要在这方面提供支持。也就是需要实现websocket和SSL的支持。
首先,我们的网络底层是非常过时的ACE,所以要改动很难,因为有很多不相干的代码在干扰这,而且写法非常不人道。
第二,不适合大规模修改和推倒重做,最后决定在原来的基础上,套了一层WebSocket。客户端使用Unity,是有现成的接口直接调用的。他们在原来的处理后,发包之前直接调了Unity的WebSocket接口。服务端收到后,需要做的是解除这一层WebSocket外衣,然后走原来的流程把数据给解析出来。
第三,SSL的支持,现在想到的是用Nginx来实现转发就可以了,问了一些其他同行,都是这样处理的。
2. WebSocket协议解析
WebSocket实现了客户端与服务器之间的全双工通讯。本质是前端与服务器端建立一条TCP长连接,服务端可以随时向前端推送数据,前端也可以随时向服务端发送数据,实现了两者间双向数据实时传输。在WebSocket出现之前,Web前端只能采用传统轮询和长轮询的方式与服务端通讯,这里不展开讲了。
WebSocket协议的官方文档是RFC6455文件,下面对协议的核心部分做一个讲解,每一部分会列出关键注意点。
协议分为:连接握手和数据传输
2.1 连接握手
2.1.1 客户端握手连接格式如下:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
客户端连接格式下面几个关键点需要注意:
请求行: 请求方法必须是GET, HTTP版本至少是1.1
请求必须含有Host
如果请求来自浏览器客户端, 必须包含Origin
请求必须含有Connection, 其值必须含有"Upgrade"记号
请求必须含有Upgrade, 其值必须含有"websocket"关键字
请求必须含有Sec-Websocket-Version, 其值必须是13
请求必须含有Sec-Websocket-Key, 用于提供基本的防护, 比如无意的连接
2.1.2 服务端收到客户端连接后,回复格式如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
响应行: HTTP/1.1 101 Switching Protocols
响应必须含有Upgrade, 其值为"weboscket"
响应必须含有Connection, 其值为"Upgrade"
响应必须含有Sec-Websocket-Accept, 根据请求首部的Sec-Websocket-key计算出来
服务端回复中关键点在于Sec-Websocket-Accept值的计算,具体计算方式如下:
将客户端送来的Sec-Websocket-Key的值和258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接
258EAFA5-E914-47DA-95CA-C5AB0DC85B11是一个magic key,是RFC6455 Page24页中定义的一个固定值,直接用即可。
通过SHA1计算出摘要, 并转成base64字符串
至此,一来一回,客户端和服务器端已经完成WebSocket 握手,连接建立,下一步就是传输数据了。
2.2 数据传输
RFC6455中定义了数据帧的格式,如下:
数据帧的组成结构和其他协议类似,归纳起来:数据头+载荷
WebSocket的数据头长度是可变的,有两个因素影响:
载荷长度的数值大小,Payload length: 占7或7+16或7+64bit,具体看下面详解。
是否有maks key,有的话头部多4个字节
数据帧格式如下:
FIN: 占1bit
0表示不是消息的最后一个分片
1表示是消息的最后一个分片
RSV1, RSV2, RSV3: 各占1bit, 一般情况下全为0, 与Websocket拓展有关, 如果出现非零的值且没有采用WebSocket拓展, 连接出错
Opcode: 占4bit
%x0: 表示本次数据传输采用了数据分片, 当前数据帧为其中一个数据分片
%x1: 表示这是一个文本帧
%x2: 表示这是一个二进制帧
%x3-7: 保留的操作代码, 用于后续定义的非控制帧
%x8: 表示连接断开
%x9: 表示这是一个心跳请求(ping)
%xA: 表示这是一个心跳响应(pong)
%xB-F: 保留的操作代码, 用于后续定义的非控制帧
Mask: 占1bit
0表示不对数据载荷进行掩码异或操作
1表示对数据载荷进行掩码异或操作
Payload length: 占7或7+16或7+64bit
0~125: 数据长度等于该值
126: 后续的2个字节代表一个16位的无符号整数, 值为数据的长度
127: 后续的8个字节代表一个64位的无符号整数, 值为数据的长度
Masking-key: 占0或4bytes
1: 携带了4字节的Masking-key
0: 没有Masking-key
payload data: 载荷数据
贝松君划重点,这里有几个关键点需要注意:
Fin为0,表示一个完整的消息被分片成多个数据帧中传输的,需要一直等待接到Fin为1的数据帧之后,才算收到一个完整的消息。websocketfiles 中的recv_dataframe已经考虑到这一点,因此为此返回给上层的数据都是一个完整的消息包。
只有客户端给服务器端发送数据时才会有masking key,服务器端给客户端发送数据不需要masking key mask掩码
opercode目前只收到过2、8、9。
2是正常的操作,解析后继续按原来的流程走就行了
8是断开连接,收到后服务端也断开连接了
9是WebSocket自带的心跳包,需要把内容给清掉,否则会一直残留在缓冲池中。