websocketpp 安装及使用
介绍
WebSocket 是从 HTML5 开始支持的一种网页端和服务端保持长连接的消息推送机制。
- 传统的 web 程序都是属于 \"一问一答\" 的形式,即客户端给服务器发送了一个 HTTP 请求,服务器给客户端返回一个 HTTP 响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应。
- 像网页即时聊天这样的程序都是非常依赖 \"消息推送\" 的,即需要服务器主动推动消息到客户端。如果只是使用原生的 HTTP 协议,要想实现消息推送一般需要通过 \"轮询\" 的方式实现, 而轮询的成本比较高并且也不能及时的获取到消息的响应。
基于上述两个问题, 就产生了 WebSocket 协议,旨在实现客户端与服务器之间的 全双工、持久化通信 。WebSocket 更接近于 TCP 这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。
与 HTTP 对比
核心特性
-
全双工通信:客户端和服务器可以同时双向发送数据,无需等待请求-响应周期。
-
低延迟:建立连接后,数据可以即时传输,无需重复建立连接(HTTP 每次请求需握手)。
-
轻量级协议:数据帧头部开销小(最小仅 2 字节),适合高频通信。
-
基于 TCP:工作在 OSI 模型的传输层之上,依赖 TCP 的可靠性。
原理解析
WebSocket 协议本质上是一个基于 TCP 的协议。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程并升级协议的过程。
- HTTP 升级请求(客户端发起)
客户端发送一个包含 Upgrade: websocket 头的 HTTP 请求,其中 Sec-WebSocket-Key 是随机生成的 Base64 字符串,用于安全校验:
GET /chat HTTP/1.1Host: example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Version: 13
- 服务器响应切换协议
服务器返回 HTTP 101 Switching Protocols 表示协议升级成功,其中 Sec-WebSocket-Accept 是客户端 Key 与固定 GUID 拼接后通过 SHA-1 哈希生成的,用于验证握手合法性:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- 连接建立
此后,通信转为 WebSocket 协议,数据以帧(Frame)形式传输。
数据帧格式
WebSocket 数据帧包含以下关键字段(二进制格式):
报文字段比较多,我们重点关注这几个字段 :
- FIN: WebSocket 传输数据以消息为概念单位,一个消息有可能由一个或多个帧组成,FIN 字段为 1 表示末尾帧。
- RSV1~3:保留字段,只在扩展时使用,若未启用扩展则应置 1,若收到不全为 0 的数据帧,且未协商扩展则立即终止连接。
- opcode: 标志当前数据帧的类型
- 0x0: 表示这是个延续帧,当 opcode 为 0 表示本次数据传输采用了数据分片,当前收到的帧为其中一个分片
- 0x1: 表示这是文本帧
- 0x2: 表示这是二进制帧
- 0x3-0x7: 保留,暂未使用
- 0x8: 表示连接断开
- 0x9: 表示 ping 帧
- 0xa: 表示 pong 帧
- 0xb-0xf: 保留,暂未使用
- mask:表示 Payload 数据是否被编码,若为 1 则必有 Mask-Key,用于解码 Payload 数据。仅客户端发送给服务端的消息需要设置。
- Payload length:数据载荷的长度,单位是字节, 有可能为 7 位、7+16 位、7+64
- 位。假设 Payload length = x:
- x 为 0~126:数据的长度为 x 字节
- x 为 126:后续 2 个字节代表一个 16 位的无符号整数,该无符号整数的值为数据的长度
- x 为 127:后续 8 个字节代表一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度
- Mask-Key:当 mask 为 1 时存在,长度为 4 字节,解码规则: DECODED[i] = ENCODED[i] ^ MASK[i % 4]
- Payload data: 报文携带的载荷数据
Websocketpp 介绍
WebSocketpp 是一个跨平台的开源头部专用 C++ 库,它实现了 RFC6455( WebSocket 协议)和 RFC7692 ( WebSocketCompression Extensions )。它允许将 WebSocket 客户端和服务器功能集成到 C++ 程序中。在最常见的配置中,全功能网络 I/O 由 Asio 网络库提供。 WebSocketpp 的主要特性包括:
- 事件驱动的接口
- 支持 HTTP/HTTPS、WS/WSS、IPv6
- 灵活的依赖管理 — Boost 库/C++11 标准库
- 可移植性:Posix/Windows、32/64bit、Intel/ARM
- 线程安全
下面是该项目的一些常用网站。
- github:https://github.com/zaphoyd/websocketpp
- 用户手册: http://docs.websocketpp.org/
- 官网:http://www.zaphoyd.com/websocketpp
安装
sudo apt-get install libboost-dev libboost-system-dev libwebsocketpp-dev
安装完毕后,若在 /usr/include 下有了 websocketpp 目录就表示安装成功了。
websocketpp 常用接口
namespace websocketpp{ typedef lib::weak_ptr connection_hdl; template class endpoint : public config::socket_type { typedef lib::shared_ptr timer_ptr; typedef typename connection_type::ptr connection_ptr; typedef typename connection_type::message_ptr message_ptr; typedef lib::function open_handler; typedef lib::function close_handler; typedef lib::function http_handler; typedef lib::function message_handler; /* websocketpp::log::alevel::none 禁止打印所有日志*/ void set_access_channels(log::level channels); /*设置日志打印等级*/ void clear_access_channels(log::level channels); /*清除指定等级的日志*/ /*设置指定事件的回调函数*/ void set_open_handler(open_handler h); /*websocket 握手成功回调处理函数*/ void set_close_handler(close_handler h); /*websocket 连接关闭回调处理函数*/ void set_message_handler(message_handler h); /*websocket 消息回调处理函数*/ void set_http_handler(http_handler h); /*http 请求回调处理函数*/ /*发送数据接口*/ void send(connection_hdl hdl, std::string &payload, frame::opcode::value op); void send(connection_hdl hdl, void *payload, size_t len, frame::opcode::value op); /*关闭连接接口*/ void close(connection_hdl hdl, close::status::value code, std::string &reason); /*获取 connection_hdl 对应连接的 connection_ptr*/ connection_ptr get_con_from_hdl(connection_hdl hdl); /*websocketpp 基于 asio 框架实现,init_asio 用于初始化 asio 框架中的 io_service 调度器*/ void init_asio(); /*设置是否启用地址重用*/ void set_reuse_addr(bool value); /*设置 endpoint 的绑定监听端口*/ void listen(uint16_t port); /*对 io_service 对象的 run 接口封装,用于启动服务器*/ std::size_t run(); /*websocketpp 提供的定时器,以毫秒为单位*/ timer_ptr set_timer(long duration, timer_handler callback); }; template class server : public endpoint<connection, config> { /*初始化并启动服务端监听连接的 accept 事件处理*/ void start_accept(); }; template class connection : public config::transport_type::transport_con_type, public config::connection_base { /*发送数据接口*/ error_code send(std::string &payload, frame::opcode::value op = frame::opcode::text); /*获取 http 请求头部*/ std::string const &get_request_header(std::string const &key); /*获取请求正文*/ std::string const &get_request_body(); /*设置响应状态码*/ void set_status(http::status_code::value code); /*设置 http 响应正文*/ void set_body(std::string const &value); /*添加 http 响应头部字段*/ void append_header(std::string const &key, std::string const &val); /*获取 http 请求对象*/ request_type const &get_request(); /*获取 connection_ptr 对应的 connection_hdl */ connection_hdl get_handle(); }; namespace http { namespace parser { class parser { std::string const &get_header(std::string const &key); std::string const &get_body(); typedef std::map header_list; header_list const &get_headers(); }; class request : public parser { /*获取请求方法*/ std::string const &get_method() /*获取请求 uri 接口*/ std::string const &get_uri() }; } }; namespace message_buffer { /*获取 websocket 请求中的 payload 数据类型*/ frame::opcode::value get_opcode(); /*获取 websocket 中 payload 数据*/ std::string const &get_payload(); };}
Websocketpp 使用
main.cc
#include #include // 1. 定义server类型typedef websocketpp::server server_t;void onOpen(websocketpp::connection_hdl hdl){ std::cout << \"websocket长连接建立成功!\\n\";}void onClose(websocketpp::connection_hdl hdl){ std::cout <get_payload(); std::cout << \"收到消息:\" << body <get_con_from_hdl(hdl); conn->send(body + \"-Hello!\", websocketpp::frame::opcode::value::text);}int main(){ // 2. 实例化服务器对象 server_t server; // 3. 初始化日志输出,关闭日志输出 server.set_access_channels(websocketpp::log::alevel::none); // 4. 初始化asio框架 server.init_asio(); // 5. 设置消息处理/连接握手成功/关闭连接回调函数 server.set_open_handler(onOpen); server.set_close_handler(onClose); auto msg_handler=std::bind(onMessage,&server,std::placeholders::_1,std::placeholders::_2); server.set_message_handler(msg_handler); // 6. 启用地址重用 server.set_reuse_addr(true); // 7. 设置监听端口 server.listen(9090); // 8. 开始监听 server.start_accept(); // 9. 启动服务器 server.run(); return 0;}
makefile
main:main.ccg++ -o $@ $^ -std=c++17 -lpthread -lboost_system
test.html
Test Websocket // 创建 websocket 实例 // ws://192.168.22.129:9090 // 类比 http // ws 表示 websocket 协议 let websocket = new WebSocket(\"ws://192.168.22.129:9090\"); // 处理连接打开的回调函数 websocket.onopen = function () { console.log(\"连接建立\"); } // 处理收到消息的回调函数 // 控制台打印消息 websocket.onmessage = function (e) { console.log(\"收到消息: \" + e.data); } // 处理连接异常的回调函数 websocket.onerror = function () { console.log(\"连接异常\"); } // 处理连接关闭的回调函数 websocket.onclose = function () { console.log(\"连接关闭\"); } // 实现点击按钮后, 通过 websocket 实例 向服务器发送请求 let input = document.querySelector(\'#message\'); let button = document.querySelector(\'#submit\'); button.onclick = function () { console.log(\"发送消息: \" + input.value); websocket.send(input.value); }