WebSocketpp 介绍及使用
- [一. Websocket 协议介绍](#一. Websocket 协议介绍)
-
- [1. 原理解析](#1. 原理解析)
- [2. 报文格式](#2. 报文格式)
- [二. Websocketpp 介绍](#二. Websocketpp 介绍)
- [三. WebSocketpp 安装](#三. WebSocketpp 安装)
- [四. WebSocketpp 类与接口](#四. WebSocketpp 类与接口)
-
- [1. 服务器类](#1. 服务器类)
- [2. 日志等级](#2. 日志等级)
- [3. 连接对象](#3. 连接对象)
- [4. HTTP 相关类](#4. HTTP 相关类)
- [五. Websocketpp 使用样例](#五. Websocketpp 使用样例)
-
- [1. 目录结构](#1. 目录结构)
- [2. 项目构建](#2. 项目构建)
- [3. 代码实现](#3. 代码实现)
-
- [1. 简单 HTTP/WebSocket 服务器](#1. 简单 HTTP/WebSocket 服务器)
- [2. HTTP 客户端](#2. HTTP 客户端)
- [3. Websocket 客户端](#3. Websocket 客户端)
一. Websocket 协议介绍
WebSocket 是从 HTML5 开始支持的一种网页端和服务端保持长连接的消息推送机制。
- 传统的 web 程序都是属于 "一问一答" 的形式,即客户端给服务器发送了一个 HTTP 请求,服务器给客户端返回一个 HTTP 响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应。
- 像网页即时聊天或者五子棋游戏这样的程序都是非常依赖 "消息推送" 的,即需要服务器主动推动消息到客户端。如果只是使用原生的 HTTP 协议,要想实现消息推送一般需要通过 "轮询" 的方式实现,而轮询的成本比较高并且也不能及时的获取到消息的响应。
- 轮询 = 客户端定时向服务器发请求,是否有消息可以返回到客户端。
- 长轮询 = 客户端问一次,服务器等到有消息再发送给客户端。
基于上述两个问题,就产生了 WebSocket 协议。WebSocket 更接近于 TCP 这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。
1. 原理解析
WebSocket 协议本质上是一个基于 TCP 的协议。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程并升级协议的过程。

具体协议升级的过程如下:客户端向浏览器发送协议切换请求,服务器同意切换响应发送给客户端,之后客户端与服务器建立成功 websocket 协议,就可以进行长连接通信。

2. 报文格式

报文字段比较多,重点关注这几个字段:
- 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 是一个跨平台的开源 (BSD 许可证) 头部专用 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
- 线程安全。
WebSocketpp 同时支持 HTTP 和 Websocket 两种网络协议,比较适用于本次的项目,所以选用该库作为项目的依赖库用来搭建 HTTP 和 WebSocket 服务器。
下面是该项目的一些常用网站:
- github:https://github.com/zaphoyd/websocketpp
- 用户手册:http://docs.websocketpp.org/
- 官网:http://www.zaphoyd.com/websocketpp
三. WebSocketpp 安装
bash
sudo apt-get install libboost-dev libboost-system-dev libwebsocketpp-dev
在环境搭建中已经内置 websocketpp,这里不需要再次安装了。
四. WebSocketpp 类与接口
1. 服务器类
cpp
namespace websocketpp {
typedef lib::weak_ptr<void> connection_hdl; // 连接句柄
template <typename config>
// 端点类:用于管理WebSocket连接的类
class endpoint : public config::socket_type {
typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr; // 定时器指针
typedef typename connection_type::ptr connection_ptr; // 连接指针
typedef typename connection_type::message_ptr message_ptr; // 消息指针
typedef lib::function<void(connection_hdl)> open_handler; // WebSocket 连接成功时的回调函数
typedef lib::function<void(connection_hdl)> close_handler; // WebSocket 连接关闭时的回调函数
typedef lib::function<void(connection_hdl, message_ptr)> message_handler; // WebSocket 收到消息时的回调函数
typedef lib::function<void(connection_hdl)> http_handler; // http 请求的回调函数
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_ptr get_con_from_hdl(connection_hdl hdl); // 获取连接指针
void init_asio(); // 初始化asio框架
void set_reuse_addr(bool value); // 设置是否启用地址重用
void listen(uint16_t port); // 设置监听端口
std::size_t run(); // 运行服务器
timer_ptr set_timer(long duration, timer_handler callback); // 设置定时器
};
// 服务端类:用于处理WebSocket服务端的类
template <typename config>
class server : public endpoint<connection<config>, config> {
void start_accept(); // 启动接受连接
};
// 消息缓冲区类:用于存储和管理WebSocket消息的类
namespace message_buffer {
frame::opcode::value get_opcode(); // 获取数据类型
std::string const & get_payload(); // 获取数据内容
};
}
2. 日志等级
websocketpp 库内有人家自己的日志输出模块 (无法替换,除非修改库内源码中所有的日志输出操作后重新编译库进行安装),这里主要是了解他的日志级别,将不需要的日志输出给禁用掉,避免其运行时的大量日志输出影响我们的视线。
cpp
namespace log {
struct alevel {
static level const none = 0x0; // 无日志
static level const connect = 0x1; // 连接日志
static level const disconnect = 0x2; // 断开日志
static level const control = 0x4; // 控制帧日志
static level const frame_header = 0x8; // 帧头日志
static level const frame_payload = 0x10; // 帧体日志
static level const message_header = 0x20; // 消息头日志
static level const message_payload = 0x40; // 消息体日志
static level const endpoint = 0x80; // 端点日志
static level const debug_handshake = 0x100; // 握手日志
static level const debug_close = 0x200; // 关闭日志
static level const devel = 0x400; // 开发日志
static level const app = 0x800; // 应用日志
static level const http = 0x1000; // http日志
static level const fail = 0x2000; // 失败日志
static level const access_core = 0x00003003; // 核心访问日志
static level const all = 0xffffffff; // 所有日志
};
}
3. 连接对象
cpp
template <typename config>
// 连接类:用于处理HTTR/WebSocket连接的类
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); // 发送字符串数据
std::string const & get_request_header(std::string const & key); // 获取 http 请求头部字段值
std::string const & get_request_body(); // 获取 http 请求正⽂
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响应头部字段
typedef websocketpp::http::parser::request request_type; // http 请求对象类型
request_type const & get_request(); // 获取 http 请求对象
connection_hdl get_handle(); // 获取连接句柄
};
namespace frame {
namespace opcode {
// 数据类型枚举
enum value {
continuation = 0x0, // 继续帧
text = 0x1, // 文本帧
binary = 0x2, // 二进制帧
rsv3 = 0x3, // 保留帧
rsv4 = 0x4, // 保留帧
rsv5 = 0x5, // 保留帧
rsv6 = 0x6, // 保留帧
rsv7 = 0x7, // 保留帧
close = 0x8, // 关闭帧
ping = 0x9, // 心跳帧
pong = 0xA, // 心跳响应帧
control_rsvb = 0xB, // 保留控制帧
control_rsvc = 0xC, // 保留控制帧
control_rsvd = 0xD, // 保留控制帧
control_rsve = 0xE, // 保留控制帧
control_rsvf = 0xF, // 保留控制帧
};
}
}
4. HTTP 相关类
cpp
namespace http {
namespace parser {
// 解析http请求
class parser {
typedef std::map<std::string, std::string, utility::ci_less > header_list; // http请求头列表
header_list const & get_headers() // 获取http请求头列表
std::string const & get_header(std::string const & key) // 获取http请求头
std::string const & get_body() // 获取http请求体
}
// http请求类:用于解析http请求的类
class request : public parser {
std::string const & get_method() // 获取http请求方法
std::string const & get_uri() // 获取http请求uri
std::string const & get_version() // 获取http请求版本
};
}
};
namespace http {
namespace status_code {
// http状态码枚举
enum value {
uninitialized = 0, // 未初始化
continue_code = 100, // 继续
switching_protocols = 101, // 切换协议
ok = 200, // 成功
created = 201, // 创建成功
accepted = 202, // 已接收
non_authoritative_information = 203, // 非授权信息
no_content = 204, // 无内容
reset_content = 205, // 重置内容
partial_content = 206, // 部分内容
multiple_choices = 300, // 多选择
moved_permanently = 301, // 永久移动
found = 302, // 临时移动
see_other = 303, // 参见其他
not_modified = 304, // 未修改
use_proxy = 305, // 使用代理
temporary_redirect = 307, // 临时重定向
bad_request = 400, // 错误请求
unauthorized = 401, // 未授权
payment_required = 402, // 需要支付
forbidden = 403, // 禁止访问
not_found = 404, // 未找到
method_not_allowed = 405, // 不允许的请求方法
not_acceptable = 406, // 不可接受的媒体类型
proxy_authentication_required = 407, // 需要代理授权
request_timeout = 408, // 请求超时
conflict = 409, // 冲突
gone = 410, // 已删除
length_required = 411, // 需要内容长度
precondition_failed = 412, // 预条件失败
request_entity_too_large = 413, // 请求实体过大
request_uri_too_long = 414, // 请求uri过长
unsupported_media_type = 415, // 不支持的媒体类型
request_range_not_satisfiable = 416, // 请求范围不满足
expectation_failed = 417, // 期望失败
im_a_teapot = 418, // 我是一个茶壶
upgrade_required = 426, // 需要升级
precondition_required = 428, // 需要预条件
too_many_requests = 429, // 太多请求
request_header_fields_too_large = 431, // 请求头字段过大
internal_server_error = 500, // 内部服务器错误
not_implemented = 501, // 未实现
bad_gateway = 502, // 错误网关
service_unavailable = 503, // 服务不可用
gateway_timeout = 504, // 网关超时
http_version_not_supported = 505, // 不支持的http版本
not_extended = 510, // 未扩展
network_authentication_required = 511 // 需要网络授权
};
}
}
五. Websocketpp 使用样例
1. 目录结构
bash
webscoketpp/
|-- makefile
|-- server.cc
2. 项目构建
bash
# makefile
server: server.cc
g++ -o $@ $^ -std=c++17 -lboost_system -pthread
.PHONY: clean
clean:
rm -f server
3. 代码实现
1. 简单 HTTP/WebSocket 服务器
cpp
// server.cc
#include <iostream>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
typedef websocketpp::server<websocketpp::config::asio> websocketsvr_t; // 定义服务器类型
// WebSocket 连接成功时的回调函数
void onOpen(websocketsvr_t* server, websocketpp::connection_hdl hdl) {
std::cout << "WebSocket 握手成功" << std::endl;
}
// WebSocket 连接关闭时的回调函数
void onClose(websocketsvr_t* server, websocketpp::connection_hdl hdl) {
std::cout << "WebSocket 连接关闭" << std::endl;
}
// WebSocket 收到消息时的回调函数
void onMessage(websocketsvr_t* server, websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg) {
// WebSocket 是由 HTTP 升级而来的,所以连接中依然保存了 HTTP 请求的信息
// 1.获取连接对象
websocketsvr_t::connection_ptr conn = server->get_con_from_hdl(hdl);
// 2.获取请求对象
websocketpp::http::parser::request req = conn->get_request();
// 3.打印请求信息
std::cout << req.get_method() << " " << req.get_uri() << " " << req.get_version() << std::endl;
for (auto& header : req.get_headers()) {
std::cout << header.first << ": " << header.second << std::endl;
}
std::cout << std::endl;
std::cout << req.get_body() << std::endl;
std::cout << "收到 WebSocket 消息:" << msg->get_payload() << std::endl;
// 4.回复消息
conn->send("回显:" + msg->get_payload(), websocketpp::frame::opcode::text);
}
// HTTP 请求时的回调函数
void onHttp(websocketsvr_t* server, websocketpp::connection_hdl hdl) {
// 1.获取连接对象
websocketsvr_t::connection_ptr conn = server->get_con_from_hdl(hdl);
// 2.获取请求对象
websocketpp::http::parser::request req = conn->get_request();
// 3.打印请求信息
std::cout << req.get_method() << " " << req.get_uri() << " " << req.get_version() << std::endl;
for (auto& header : req.get_headers()) {
std::cout << header.first << ": " << header.second << std::endl;
}
std::cout << std::endl;
std::cout << req.get_body() << std::endl;
// 4.设置响应信息
conn->set_status(websocketpp::http::status_code::value::ok);
conn->set_body("<html><body><h1>Hello World</h1></body></html>");
conn->append_header("Content-Type", "text/html");
}
int main(int argc, char* argv[])
{
// 1.初始化服务器
websocketsvr_t server;
// 2.禁用日志
server.clear_access_channels(websocketpp::log::alevel::all);
// 3.设置回调函数
server.set_open_handler(std::bind(&onOpen, &server, std::placeholders::_1));
server.set_close_handler(std::bind(&onClose, &server, std::placeholders::_1));
server.set_message_handler(std::bind(&onMessage, &server, std::placeholders::_1, std::placeholders::_2));
server.set_http_handler(std::bind(&onHttp, &server, std::placeholders::_1));
// 4.初始化asio框架
server.init_asio();
// 5.监听端口
server.listen(9000);
// 6.开始接受连接
server.start_accept();
// 7.运行服务器
server.run();
return 0;
}
2. HTTP 客户端
使用浏览器作为 http 客户端即可,访问自己设置的服务器 IP 地址和端口号。


3. Websocket 客户端
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test WebSocket</title>
</head>
<body>
<input type="text" id="message" />
<button id="submit">提交</button>
<script>
// 创建 WebSocket 实例
const websocket = new WebSocket("ws://192.168.174.128:9000");
// WebSocket 连接建立成功时的回调函数
websocket.onopen = function () {
console.log("连接建立");
};
// WebSocket 收到服务器消息时的回调函数
websocket.onmessage = function (e) {
// console.log("收到消息: " + e.data);
alert("收到消息: " + e.data);
};
// WebSocket 连接发生错误时的回调函数
websocket.onerror = function () {
console.log("连接异常");
};
// WebSocket 连接关闭时的回调函数
websocket.onclose = function () {
console.log("连接关闭");
};
// 点击按钮发送消息
const input = document.querySelector("#message");
const button = document.querySelector("#submit");
button.onclick = function () {
console.log("发送消息: " + input.value);
websocket.send(input.value);
};
</script>
</body>
</html>
在控制台中我们可以看到连接建立、客户端和服务器通信以及断开连接的过程 (关闭服务器就会看到断开连接的现象),通过 F12 打开浏览器的调试模式。

