C++实现WebSocket功能及WebSocket协议详解(附代码)

目录

1. C++实现WebSocket功能的一些开源参考库和建议

1.1 背景

1.2 Websocketfiles简介

2. WebSocket协议解析

2.1 连接握手

2.2 数据传输

3. Websocketfiles使用方法

3.1 源码文件介绍:

3.2 示例程序介绍

3.3 使用方法



1. C++实现WebSocket功能的一些开源参考库和建议

1.1 背景

项目中会遇到让已有的C++服务端增加WebSocket协议支持的情况,Github上有不少开源的C++代码可以参考,比较知名的如websocketcpp,beast, uWebSockets,restbed等等,不过即便有这些代码参考,也难以快速移植代码到你的C++服务器端程序里,原因大致有下面几点:

  1. websocketcpp,beast, uWebSockets等库相对比较重型,代码量较大,快速裁剪并移植到现有C++工程里比较耗时。大家都懂,一般开发时间是比较紧张的,花精力配置运行起来、再对比搞懂这些库没时间啊。
  2. 相当多Websocket开源库采用C++11规则编写,但现实是很多C++服务端程序只支持C++98,难以升级编译器。移植C++11代码得不偿失。
  3. WebSocket开源库等与底层网络库耦合的较多,相信各位的服务端都有自己定制的网络库,切换网络库,再考虑线程安全等因素,给修改移植工作带来不少工作量。
  4. 另外,Github上还有一些简单的、实验性的WebSocket程序。可惜其中很多写的太简单,协议解析和网络传输代码混杂在一起,散落各处,不便于封装,只能参考,难以用在实际中。

贝松君就遇到上面的尴尬,最后花时间写了一套“简单易用,刚刚合适“的WebSocket解析程序,倒不是我喜欢重新发明轮子,只是拆别人轮子的时间比造一个合适的轮子还长,我也是没办法啊!现在这部分WebSocket代码已经开源到Github上,希望能为大家节省时间,提高开发效率。项目暂且命名websocketfiles,欢迎使用,觉有用就Star一下吧。

websocketfiles 代码地址:https://github.com/beikesong/websocketfiles

 

1.2 Websocketfiles简介

先顺着上面简介下,想直接看WebSocket协议讲解的可跳到第二节。

Websocketfiles提供WebSocket协议的最基本功能,完成handshake后,只做协议解包封包,不绑定网络传输层,可以自由移植。特点如下 :

  1. 符合RFC6455协议
  2. 只专注于WebSocket解包封包,不绑定网络传输层,主体代码不足1000行,能快速熟悉,便于移植。
  3. 只包含两个关键类:WebsocketPacket:WebSocket协议包解包封包,WebsocketEndpoint:定义服务器/客户端行为,可直接修改、继承和扩展为服务端/客户端程序。
  4. 针对TCP协议做了粘包处理,读取完整的WebSocket packet后才做处理。你只需要把网络层读取的数据送入from_wire函数,通过to_wire函数发送回复。
  5. C++98实现,代码可以在多平台上编译通过(Linux/Windows/armLinux等)
  6. 提供丰富的调试信息,便于理解和修改程序。
  7. 作为演示,提供了一个基于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值的计算,具体计算方式如下:

  1. 将客户端送来的Sec-Websocket-Key的值和258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接
  2. 258EAFA5-E914-47DA-95CA-C5AB0DC85B11是一个magic key,是RFC6455 Page24页中定义的一个固定值,直接用即可。
  3. 通过SHA1计算出摘要, 并转成base64字符串

至此,一来一回,客户端和服务器端已经完成WebSocket 握手,连接建立,下一步就是传输数据了。在websocketfiles,握手通过WebsocketEndpoint::parser_packet,WebsocketPacket::recv_handshake和pack_handshake完成。

2.2 数据传输

RFC6455中定义了数据帧的格式,如下:

数据帧的组成结构和其他协议类似,归纳起来:数据头+载荷

WebSocket的数据头长度是可变的,有两个因素影响:

  1. 载荷长度的数值大小,Payload length: 占7或7+16或7+64bit,具体看下面详解。
  2. 是否有maks key,有的话头部多4个字节

数据帧格式如下:

                        

  • FIN: 占1bit

    • 0表示不是消息的最后一个分片
    • 1表示是消息的最后一个分片
  • RSV1RSV2RSV3: 各占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: 载荷数据

贝松君划重点,这里有几个关键点需要注意:

  1. Fin为0,表示一个完整的消息被分片成多个数据帧中传输的,需要一直等待接到Fin为1的数据帧之后,才算收到一个完整的消息。websocketfiles 中的recv_dataframe已经考虑到这一点,因此为此返回给上层的数据都是一个完整的消息包。
  2. 只有客户端给服务器端发送数据时才会有masking key,服务器端给客户端发送数据不需要masking key
  3. mask掩码计算可以查看文档,或者直接阅读 pack_dataframe代码。

在websocketfiles,数据帧收发通过WebsocketEndpoint::parser_packet,WebsocketPacket::recv_dataframe和pack_dataframe完成。好了,够简洁吧,大家可以参考websocketfiles这个项目的代码,看看每一部分的实现,这样更能融会贯通,即便不使用别人的代码相信也能自己写出来。

 

3. Websocketfiles使用方法

3.1 源码文件介绍:

  1. WebsocketPacket.cpp: 定义WebSocket协议握手及数据包解包封包函数
  2. WebsocketEndpoint.cpp:定义WebSocket服务端客户端行为,可以修改或继承实现自己特有的服务端客户端行为。
  3. string_helper.cpp: 字符串操作函数
  4. sha1.cpp和base64.cpp:SHA1和Base64编码所需函数
  5. 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目录的所有文件拷贝过来,然后:

  1. 将网络层得到的read buffer 送入 WebsocketEndpoint::from_wire函数,完成握手和数据帧接收工作
  2. 定义自己的 user_defined_process函数,根据Websocket消息种类,做出不同处理,
  3. 将处理后需要回复的write_buffer填入WebsocketEndpoint::from_wire函数,将数据发送给网络层。
  4. 目前to_wire函数使用回调方式,需要将网络层的发送函数指针传入。你可以可以采用其他方式,只需要修改WebsocketEndpoint类来定义发送和接收行为。

下面上一个实际使用中的结构图,表面websocketfiles和网络层及现有C++工程的关系:

                              

 

参考文献:

RFC6455

https://segmentfault.com/a/1190000012948613

需转载本文请与我联系

 

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值