目录
1. C++实现WebSocket功能的一些开源参考库和建议
1. C++实现WebSocket功能的一些开源参考库和建议
1.1 背景
项目中会遇到让已有的C++服务端增加WebSocket协议支持的情况,Github上有不少开源的C++代码可以参考,比较知名的如websocketcpp,beast, uWebSockets,restbed等等,不过即便有这些代码参考,也难以快速移植代码到你的C++服务器端程序里,原因大致有下面几点:
- websocketcpp,beast, uWebSockets等库相对比较重型,代码量较大,快速裁剪并移植到现有C++工程里比较耗时。大家都懂,一般开发时间是比较紧张的,花精力配置运行起来、再对比搞懂这些库没时间啊。
- 相当多Websocket开源库采用C++11规则编写,但现实是很多C++服务端程序只支持C++98,难以升级编译器。移植C++11代码得不偿失。
- WebSocket开源库等与底层网络库耦合的较多,相信各位的服务端都有自己定制的网络库,切换网络库,再考虑线程安全等因素,给修改移植工作带来不少工作量。
- 另外,Github上还有一些简单的、实验性的WebSocket程序。可惜其中很多写的太简单,协议解析和网络传输代码混杂在一起,散落各处,不便于封装,只能参考,难以用在实际中。
贝松君就遇到上面的尴尬,最后花时间写了一套“简单易用,刚刚合适“的WebSocket解析程序,倒不是我喜欢重新发明轮子,只是拆别人轮子的时间比造一个合适的轮子还长,我也是没办法啊!现在这部分WebSocket代码已经开源到Github上,希望能为大家节省时间,提高开发效率。项目暂且命名websocketfiles,欢迎使用,觉有用就Star一下吧。
websocketfiles 代码地址:https://github.com/beikesong/websocketfiles
1.2 Websocketfiles简介
先顺着上面简介下,想直接看WebSocket协议讲解的可跳到第二节。
Websocketfiles提供WebSocket协议的最基本功能,完成handshake后,只做协议解包封包,不绑定网络传输层,可以自由移植。特点如下 :
- 符合RFC6455协议
- 只专注于WebSocket解包封包,不绑定网络传输层,主体代码不足1000行,能快速熟悉,便于移植。
- 只包含两个关键类:WebsocketPacket:WebSocket协议包解包封包,WebsocketEndpoint:定义服务器/客户端行为,可直接修改、继承和扩展为服务端/客户端程序。
- 针对TCP协议做了粘包处理,读取完整的WebSocket packet后才做处理。你只需要把网络层读取的数据送入from_wire函数,通过to_wire函数发送回复。
- C++98实现,代码可以在多平台上编译通过(Linux/Windows/armLinux等)
- 提供丰富的调试信息,便于理解和修改程序。
- 作为演示,提供了一个基于libuv的异步WebSocket Server实现,便于调试和移植。
这里阅读代码不便捷,就不直接在文章中贴了,可以从上面的链接下载阅读,使用方法放在第三节再说。作为参考,先对WebSocket协议的主要内容做讲解。
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 握手,连接建立,下一步就是传输数据了。在websocketfiles,握手通过WebsocketEndpoint::parser_packet,WebsocketPacket::recv_handshake和pack_handshake完成。
2.2 数据传输
RFC6455中定义了数据帧的格式,如下:
数据帧的组成结构和其他协议类似,归纳起来:数据头+载荷
WebSocket的数据头长度是可变的,有两个因素影响:
- 载荷长度的数值大小,
Payload length
: 占7或7+16或7+64bit,具体看下面详解。 - 是否有maks key,有的话头部多4个字节
数据帧格式如下:
-
FIN
: 占1bit0
表示不是消息的最后一个分片1
表示是消息的最后一个分片
RSV1
,RSV2
,RSV3
: 各占1bit, 一般情况下全为0, 与Websocket拓展有关, 如果出现非零的值且没有采用WebSocket拓展, 连接出错-
Opcode
: 占4bit%x0
: 表示本次数据传输采用了数据分片, 当前数据帧为其中一个数据分片%x1
: 表示这是一个文本帧%x2
: 表示这是一个二进制帧%x3-7
: 保留的操作代码, 用于后续定义的非控制帧%x8
: 表示连接断开%x9
: 表示这是一个心跳请求(ping)%xA
: 表示这是一个心跳响应(pong)%xB-F
: 保留的操作代码, 用于后续定义的非控制帧
-
Mask
: 占1bit0
表示不对数据载荷进行掩码异或操作1
表示对数据载荷进行掩码异或操作
-
Payload length
: 占7或7+16或7+64bit0~125
: 数据长度等于该值126
: 后续的2个字节代表一个16位的无符号整数, 值为数据的长度127
: 后续的8个字节代表一个64位的无符号整数, 值为数据的长度
-
Masking-key
: 占0或4bytes1
: 携带了4字节的Masking-key0
: 没有Masking-key
payload data
: 载荷数据
贝松君划重点,这里有几个关键点需要注意:
- Fin为0,表示一个完整的消息被分片成多个数据帧中传输的,需要一直等待接到Fin为1的数据帧之后,才算收到一个完整的消息。websocketfiles 中的recv_dataframe已经考虑到这一点,因此为此返回给上层的数据都是一个完整的消息包。
- 只有客户端给服务器端发送数据时才会有masking key,服务器端给客户端发送数据不需要masking key
- mask掩码计算可以查看文档,或者直接阅读 pack_dataframe代码。
在websocketfiles,数据帧收发通过WebsocketEndpoint::parser_packet,WebsocketPacket::recv_dataframe和pack_dataframe完成。好了,够简洁吧,大家可以参考websocketfiles这个项目的代码,看看每一部分的实现,这样更能融会贯通,即便不使用别人的代码相信也能自己写出来。
3. Websocketfiles使用方法
3.1 源码文件介绍:
- WebsocketPacket.cpp: 定义WebSocket协议握手及数据包解包封包函数
- WebsocketEndpoint.cpp:定义WebSocket服务端客户端行为,可以修改或继承实现自己特有的服务端客户端行为。
- string_helper.cpp: 字符串操作函数
- sha1.cpp和base64.cpp:SHA1和Base64编码所需函数
- main.cpp和lib/include 文件夹仅仅用于demo所需要的liuv .so和.h文件,如果你自己的工程使用不同的网络库,可以不用包含
3.2 示例程序介绍
map.cpp中使用大名鼎鼎的libuv作为网络库(Node.js底层的跨平台C网络库),演示了一个异步WebSocket 服务端。完成握手后,将客户端发送过来的信息原样回复过去。该实例使用一个event loop线程和一个队列工作线程。网络库和多线程的专题不属于websocketfiles项目的重点,所以不在这里展开了。
编译运行:下载源码后,直接make,执行服务端程序后有详细的打印信息,便于大家跟踪调试和理解代码,如下:
set thread pool size:1
peer (xxx.xxx.xxx.xxx, 11464) connected
handshake element k:Host v: yyy.yyy.yyy.yyy:9050
handshake element k:Connection v:Upgrade
handshake element k:Pragma v:no-cache
handshake element k:Cache-Control v:no-cache
handshake element k:User-Agent v:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
handshake element k:Upgrade v:websocket
handshake element k:Origin v:http://www.bejson.com
handshake element k:Sec-WebSocket-Version v:13
handshake element k:Accept-Encoding v:gzip, deflate
handshake element k:Accept-Language v:zh-CN,zh;q=0.9
handshake element k:Sec-WebSocket-Key v:lEecdWuXh4ekgX/oWBSc8A==
handshake element k:Sec-WebSocket-Extensions v:permessage-deflate; client_max_window_bits
WebsocketEndpont - handshake successful!
WebSocketPacket: received data with header size: 6 payload size:28 input oft size:34
WebSocketEndpoint - recv a Text opcode.
WebSocketEndpoint - received data, length:28 ,content:first websocket test message
WebSocketPacket: send data with header size: 2 payload size:28
3.3 使用方法
如果要在自己的C++工程中使用websocketfiles,只需要把src目录的所有文件拷贝过来,然后:
- 将网络层得到的read buffer 送入 WebsocketEndpoint::from_wire函数,完成握手和数据帧接收工作
- 定义自己的 user_defined_process函数,根据Websocket消息种类,做出不同处理,
- 将处理后需要回复的write_buffer填入WebsocketEndpoint::from_wire函数,将数据发送给网络层。
- 目前to_wire函数使用回调方式,需要将网络层的发送函数指针传入。你可以可以采用其他方式,只需要修改WebsocketEndpoint类来定义发送和接收行为。
下面上一个实际使用中的结构图,表面websocketfiles和网络层及现有C++工程的关系:
参考文献:
RFC6455
https://segmentfault.com/a/1190000012948613
需转载本文请与我联系