基于Crow的C++的WebSocket服务器
一、WebSocket
1.1 什么是WebSocket
WebSocket 是一种持久化的通讯协议。
很多网站为了实现推送技术,所用的技术都是轮询,这种解决方案是指由浏览器每隔一段时间向服务器发出 HTTP 请求,然后服务器返回最新的数据给客户端。这种模式有很明显的缺点,即浏览器需要不断的向服务器发出请求,然而 HTTP 请求与响应可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。
WebSocket 是一种网络传输协议,其本质是前端与服务器端建立一条TCP长连接,服务端可以随时向前端推送数据,前端也可以随时向服务端发送数据,实现了两者间双向数据实时传输。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455,后由 RFC 7936 补充规范。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
WebSocket 与 HTTP 对比如下:
WebSocket 的优点:
- 1)较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;
- 2)更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
- 3)保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息;
- 4)更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容;
- 5)可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。
1.2 协议结构
1.2.1 数据帧结构
前面提到的RFC6455中定义了数据帧的格式,如下:
数据帧的组成结构和其他协议类似,都是数据头+消息体(payload,直译为荷载),不过WebSocket的数据头长度是可变的,受两个因素影响:
- 消息体长度的大小,也就是图上的Payload len: 占7或7+16或7+64bit。
- 是否有maks key,有的话头部多4个字节。
1.2.1 数据帧格式
-
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 len: 占7或7+16或7+64bit
- 0~125: 数据长度等于该值
- 126: 后续的2个字节代表一个16位的无符号整数, 值为数据的长度
- 127: 后续的8个字节代表一个64位的无符号整数, 值为数据的长度
-
Masking-key: 占0或4bytes;只有客户端给服务器端发送数据时才会有masking key,服务器端给客户端发送数据不需要masking key
- 1: 携带了4字节的Masking-key
- 0: 没有Masking-key
-
payload data: 消息体
1.2.2 掩码算法
结构体中有一个Masking-key,这是一个掩码字段,它是一个由客户端随机选择的 32 位的值。掩码值必须是不可被预测的。因此,掩码必须来自强大的熵源(entropy),并且给定的掩码不能很容易的预测到后续帧,掩码的不可预测性对于预防恶意应用的作者在网上暴露相关的字节数据至关重要。
掩码只影响头的长度,不影响消息体,对数据进行掩码操作和对数据进行反掩码操作所涉及的步骤是相同的。
掩码、反掩码操作都采用如下算法:
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
其中:
- 1)original-octet-i:为原始数据的第 i 字节;
- 2)transformed-octet-i:为转换后的数据的第 i 字节;
- 3)masking-key-octet-j:为 mask key 第 j 字节。
算法是完全公开的,运算起来也不复杂。所以数据掩码的作用是增强协议的安全性。而不是为了保护数据本身,应该主要还是为了防止早期版本的协议中存在的代理缓存污染攻击等问题。
1.3 与WebSocket类似的协议
1.3.1 AJAX Polling:
客户端使用普通的http方式向服务器端请求网页,同时执行轮询脚本,定期循环的向服务器发送请求(例如每5秒发送一次请求),获取信息
服务器对每次请求作出响应,并返回相应信息,就像正常的http请求一样;
缺点:轮询耗费资源
1.3.2 AJAX Long-Polling:
与普通的polling相比区别就是异步等待;
客户端使用普通的http方式向服务器端请求网页,同时执行轮询脚本,向服务器发送数据、请求信息;
但是服务器并不是立即就对客户端的请求作出响应,而是等待有效的更新;
当信息是有效的更新时,服务器才会把数据推送给客户端;
当客户端接收到服务器的通知时,立即会发送一个新的请求,进入到下一次的轮询;
缺点:无更新时发送请求浪费资源
1.3.3 Server Sent Events (SSE) / EventSource:
客户端使用普通的http方式向服务器端请求网页,同时网页与服务器之间建立了一个连接;
当服务器端有更新时,会发送一个事件到客户端;
服务器无更新但客户端有请求时,仍使用HTTP模式进行请求与应答;
优点是如果已有的代码是HTTP请求,那么服务器可以不影响现有代码的情况下支持SSE;
缺点是半双工,客户端无法向服务器发送请求,并且部分浏览器不支持。
1.3.4 Comet:
Comet是一种用于web的推送技术,能使服务器实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式,长轮询和iframe流。
- 长轮询
长轮询是在打开一条连接以后保持,等待服务器推送来数据再关闭的方式。 - iframe流
iframe流方式是在页面中插入一个隐藏的iframe,利用其src属性在服务器和客户端之间创建一条长链接,服务器向iframe传输数据(通常是HTML,内有负责插入信息的javascript),来实时更新页面。
iframe流方式的优点是浏览器兼容好,Google公司在一些产品中使用了iframe流,如Google Talk。
1.3.5 WebRTC
WebRTC 是一种实时媒体传输技术,最初是一个开源项目,主要目标是为浏览器和移动应用程序提供建立连接的方法。一个连接是通过指示信号和同步信号建立的,这个过程被称为信令。为了在两个设备之间建立 WebRTC 连接,需要一个信令服务器。它是一个中间件,除了承担建立连接的主要功能外,还最大程度上降低了有价值的信息和机密数据泄露的风险。
WebRTC的延迟很低,很适合用于大数据量的传输;并且因为支持自定义协议,所以安全性可控;缺点是只有部分浏览器支持,以及需要中间件等。
二、CroW
2.1 什么是Crow
Crow是一个用C++编写的WebSocket框架,旨在提供简单易用的API和高性能。它最初是由Mozilla开发的,现在已经成为了一个独立的开源项目。它使用了类似于Python的Flask的路由,这使得它易于使用,同时其处理速度也很快。
2.2 优点以及缺点
2.2.1 优点
简单易用:Crow提供了简单易用的API,可以快速构建WebSocket服务器和客户端应用程序。
高性能:Crow使用了Asio库来处理网络I/O操作(旧版使用Boost.asio,目前已更新为独立版asio),可以实现高效的并发处理。此外,它还支持SSL/TLS加密,可以保证通信的安全性和可靠性。
可扩展性:Crow提供了插件机制,可以方便地扩展其功能。例如,可以通过编写自定义插件来实现自定义的消息处理逻辑。
跨平台:Crow可以在多种平台上运行,包括Windows、Linux和macOS等操作系统。
2.2.2 缺点
文档不够完善:Crow的文档很清晰简洁,相比于其他一些WebSocket框架来说,它的文档还有待进一步完善。
缺乏实时消息队列支持:Crow目前还不支持实时消息队列功能,如果需要实现类似消息队列的功能,需要自己编写相关的代码。
不支持多路复用:Crow目前只支持单个TCP连接,如果需要同时处理多个客户端请求,需要为每个客户端创建一个新的线程或进程。
不支持HTTPS:虽然Crow支持SSL/TLS加密,但是不支持HTTPS协议。如果需要通过HTTPS协议进行通信,需要自己实现相关的加密算法。
不适合大规模应用:由于Crow的设计初衷是用于小型到中型的应用场景,因此在面对大规模应用时可能会出现性能瓶颈等问题。
2.3 编译安装
在安装Crow之前需要先安装ASIO,要求1.22.0以上的版本;
然后下载、MAKE、install:
git clone https://github.com/CrowCpp/Crow.git
mkdir build
cd build
cmake .. -DCROW_BUILD_EXAMPLES=OFF -DCROW_BUILD_TESTS=OFF
make install
之后能看到如下目录:
其中include里的就是所有的源码,平时用的时候只需要包含include/crow.h即可
三、基于Crow的C++服务器与客户端示例
3.1 客户端
客户端我是用HTML+JS那一套,Crow也可以写C++客户端;
JS里面使用WebSocket可以直接使用WebSocket对象,这个类只有两个函数
var wSocket = new WebSocket("ws://localhost:18080/ws");
wSocket.close([code[, reason]]) // 关闭当前链接
wSocket.send(data) // 发送数据
写一个html,在里面加一个按钮,点击然后调用send发送数据即可;
具体可参考: https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
3.2 服务器
先定义Crow::app对象,然后绑定ws路由,再监听指定端口即可,
CROW_WEBSOCKET_ROUTE(app, "/ws")
.onopen([&](crow::websocket::connection& conn) {
CROW_LOG_INFO << "new websocket connection from " << conn.get_remote_ip();
})
.onclose([&](crow::websocket::connection& conn, const std::string& reason) {
CROW_LOG_INFO << "websocket connection closed: " << reason;
})
.onmessage([&](crow::websocket::connection& conn, const std::string& data, bool is_binary)
{
CROW_LOG_INFO << "Recv message:" << data;
if (is_binary)
conn.send_binary(data);
else
conn.send_text(data);
});
app.loglevel(LogLevel::Debug).port(18080).multithreaded().run();
3.3 运行测试
还可以使用测试网站:http://www.websocket-test.com/ (这个网站是我看到某个文章上推的,安全性未知)