文章目录
- 安装
- [Ⅰ. WebSocket](#Ⅰ. WebSocket)
- [Ⅱ. WebSocketpp](#Ⅱ. WebSocketpp)

安装
shell
sudo apt-get install libboost-dev libboost-system-dev libwebsocketpp-dev
安装完毕后,若在 /usr/include 下有了 websocketpp 目录就表示安装成功了。
shell
ls /usr/include/websocketpp/
Ⅰ. WebSocket
一、简介
WebSocket 是从 HTML5 开始支持的一种网页端和服务端保持长连接的 消息推送机制。
传统的 web 程序都是属于 "一问一答" 的形式,即客户端给服务器发送了一个 HTTP 请求,服务器给客户端返回一个 HTTP 响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应
像网页即时聊天或者我们做的五子棋游戏这样的程序都是非常依赖 "消息推送" 的,即需要服务器主动推动消息到客户端。如果只是使用原生的 HTTP 协议,要想实现消息推送一般需要通过 "轮询" 的方式实现, 而轮询的成本比较高并且也不能及时的获取到消息的响应。
基于上述两个问题, 就产生了 WebSocket 协议。WebSocket 更接近于 TCP 这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。
并且要注意,WebSocket 和我们平时说的 Socket 是没有半毛钱关系的,注意区分开来!
二、特点
- 建立在
TCP协议之上,支持双向通信,实时性更强。 - 与
HTTP协议有着良好的兼容性,握手阶段采用HTTP协议,默认端口是80和443。 - 数据格式比较轻量,性能开销小、通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是
ws(如果加密,则为wss),形式:ws://echo.websocket.org - 支持扩展。
ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)
三、原理解析
WebSocket 协议本质上是一个基于 TCP 的协议。
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程并【升级协议】的过程。

通过一些抓包来分析一下切换过程中 HTTP请求 和 HTTP响应 的包有何不同:

HTTP请求:
http
GET /ws HTTP/1.1 // URL通常会设置为/ws表示这是websocket
Host: localhost:2021
Upgrade: websocket // 希望升级的协议格式
Connection: Upgrade // 希望升级协议
Sec-Websocket-Key: mViTimINUhcF0fBHeX+wqA== // 是客户端发送的一个base64编码的秘文,
Sec-Websocket-Version: 13 // 该websocket的版本
Sec-Websocket-Key是客户端发送的一个base64编码的秘文,要求服务端返回一个对应加密的Sec-Websocket-Accept应答,否则客户端会抛出Error during WebSocket handshake错误,并关闭连接。Sec-Websocket-Version: 13表示websocket的版本,如果服务端不支持该版本,需要返回一个Sec-Websocket-Version里面包含服务端支持的版本号。
HTTP响应:
http
HTTP/1.1 101 Switching Protocols // 表示服务端接受websocket协议的
Connection: Upgrade // 升级协议
Upgrade: websocket // 升级的协议格式
Sec-Websocket-Accept: YLcYR/p/mS8hENqlgMXtFTggdv8=
Sec-Websocket-Accept是服务端采用与客户端一致的秘钥计算出来后返回客户端。HTTP/1.1 101 Switching Protocols表示服务端接受WebSocket协议的客户端连接。
四、报文格式

报文字段比较多,我们重点关注这几个字段:
- FIN :
WebSocket协议传输数据以消息为概念单位,⼀个消息有可能由⼀个或多个帧组成- 占
1个比特大小:- 如果是
1,表示这是消息的最后一个分片 - 如果是
0,表示这不是消息的最后一个分片
- 如果是
- RSV1 、RSV2 、RSV3 :
- 各占
1个比特大小:- 保留字段,只在扩展时使用,若未启用扩展则应置
1,若收到不全为0的数据帧,且未协商扩展则立即终止连接。
- 保留字段,只在扩展时使用,若未启用扩展则应置
- 各占
- Opcode :
- 占
4个比特,标志当前数据帧的类型:- 0x0 :表示一个延续帧。当
Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。 - 0x1 :表示这是一个文本帧(
frame) - 0x2 :表示这是一个二进制帧(
frame) - 0x3-0x7:保留的操作代码,用于后续定义的非控制帧
- 0x8 :表示连接断开
- 0x9 :表示这是一个
ping操作 - 0xA :表示这是一个
pong操作 - 0xB-0xF:保留的操作代码,用于后续定义的控制帧
- 0x0 :表示一个延续帧。当
- 占
- Mask :
- 表示是否要对数据载荷 也就是
Payload数据字段进行掩码操作:- 从客户端向服务端发送数据时,需要 对数据进行掩码操作。如果服务端接收到的数据没有进行掩码操作,服务端需要断开连接!
- 从服务端向客户端发送数据时,不需要 对数据进行掩码操作。
- 占
1个比特大小:- 如果为
1,那么Masking-key字段中会定义一个掩码键,并用这个掩码键对数据载荷进行反掩码。
- 如果为
- 表示是否要对数据载荷 也就是
- Payload length :
- 数据载荷的长度,单位是 字节 。有可能为
7位、7+16位、7+64位,假设x为数据载荷的长度,则其表示如下:x在[0,126)内 :数据的长度就是Payload length表示的大小x为126:后续2个字节代表⼀个16位的无符号整数,该无符号整数的值为数据的长度x为127:后续8个字节代表⼀个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度
- 数据载荷的长度,单位是 字节 。有可能为
- Masking-key :
- 占
0或4字节Mask为1,则包含4字节的Masking-keyMask为0,则不包含Masking-key
- 占
- Payload data :
- 报文携带的载荷数据
- 如果没有协商使用扩展的话,扩展数据为
0字节。所有的扩展都必须声明扩展数据的长度,扩展如何使用必须在握手阶段就协商好
Ⅱ. 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 服务器。
下面是该项目的一些常用网站:
二、常用接口
这里做一下下面接口和类的大概介绍:
- 下面的几个指定事件的回调事件,如
set_open_handler()函数,它们都是用来 设置 针对不同事件而设置的处理函数,而处理函数是由我们自己来写的,因为WebSocketpp负责搭建服务器,它给不同的事件分配了不同的处理函数指针,比如open_handler其实就是握手成功后处理函数的指针,当服务器收到了指定的数据,触发了指定的事件之后,就会通过函数指针去调用我们自己写的那些处理函数! server类是继承于endpoint类的 ,我们后面也是通过创建server类来完成搭建服务器的操作。connection这个类也很重要,它提供了一些接口,用于处理连接的事件和状态 ,而connection_hdl是一个指向connection对象的轻量级句柄。它的作用是在WebSocket库内部,用于管理和操作connection对象,以及在回调函数中传递连接对象的引用。
- 一般来说我们通过
server类的get_con_from_hdl()函数,传入connection_hdl参数就能获得connection_ptr从而来操作connection类的函数!
asio是一个关键组件,主要用来处理底层的网络I/O操作 。asio作为一个跨平台的库,提供了异步网络编程的能力,而websocketpp则构建在asio之上,实现了 WebSocket 协议。
cpp
namespace websocketpp {
// 定义连接句柄类型,通常用于标识连接
typedef lib::weak_ptr<void> connection_hdl;
template <typename config>
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; // 连接开启事件回调类型
typedef lib::function<void(connection_hdl)> close_handler; // 连接关闭事件回调类型
typedef lib::function<void(connection_hdl)> http_handler; // HTTP请求事件回调类型
typedef lib::function<void(connection_hdl, message_ptr)> message_handler; // 消息接收事件回调类型
public:
// 设置日志打印的等级
void set_access_channels(log::level channels);
// 清除指定的日志打印等级
void clear_access_channels(log::level channels);
void set_open_handler(open_handler h); // 设置连接开启的回调函数
void set_close_handler(close_handler h); // 设置连接关闭的回调函数
void set_message_handler(message_handler h); // 设置消息接收的回调函数
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);
// 初始化asio框架,必须在使用之前调用
void init_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);
// 取消定时器并立即触发回调函数
std::size_t cancel();
};
template <typename config>
class server : public endpoint<connection<config>, config> {
// 启动服务端的accept事件处理,即开始监听,要先于run()之前执行
void start_accept();
};
template <typename config>
class connection : public config::transport_type::transport_con_type,
public config::connection_base {
public:
// 发送数据
error_code send(std::string& payload, frame::opcode::value op = frame::opcode::text);
// 获取HTTP请求头部中指定key的值
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请求对象
request_type const& get_request();
// 获取连接对应的连接句柄
connection_hdl get_handle();
};
namespace http {
namespace parser {
// HTTP解析器类,用于解析HTTP头部和正文
class parser {
public:
// 获取HTTP头部中指定key的值
std::string const& get_header(std::string const& key);
};
// HTTP请求类,继承自parser类
class request : public parser {
public:
// 获取请求方法
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();
}
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; // 所有日志
};
}
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 // 网络认证失败
};
}
}
namespace frame {
namespace opcode {
// WebSocket帧的操作码定义
enum value {
continuation = 0x0, // 数据续传帧
text = 0x1, // 文本帧
binary = 0x2, // 二进制帧
rsv3 = 0x3, // 保留帧3
rsv4 = 0x4, // 保留帧4
rsv5 = 0x5, // 保留帧5
rsv6 = 0x6, // 保留帧6
rsv7 = 0x7, // 保留帧7
close = 0x8, // 关闭帧
ping = 0x9, // Ping帧
pong = 0xA, // Pong帧
control_rsvb = 0xB, // 控制帧RSVB
control_rsvc = 0xC, // 控制帧RSVC
control_rsvd = 0xD, // 控制帧RSVD
control_rsve = 0xE, // 控制帧RSVE
control_rsvf = 0xF // 控制帧RSVF
};
}
}
三、搭建服务器样例
- 实例化
server对象- 一般我们使用
typedef websocketpp::server<websocketpp::config::asio> server_t来定义服务器对象类型,防止实例化时候语句过长!
- 一般我们使用
- 关闭日志输出
- 初始化
asio和启用地址重用 - 设置回调函数
- 设置监听窗口,开始监听
- 启动服务器
cpp
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
// 定义server类型
typedef websocketpp::server<websocketpp::config::asio> server_t;
void onOpen(websocketpp::connection_hdl hdl) {
std::cout << "websocket长连接建立成功!\n";
}
void onClose(websocketpp::connection_hdl hdl) {
std::cout << "websocket长连接断开!\n";
}
void onMessage(server_t *server, websocketpp::connection_hdl hdl, server_t::message_ptr msg) {
// 1. 获取有效消息载荷数据,进行业务处理
std::string body = msg->get_payload();
std::cout << "收到消息:" << body << std::endl;
// 2. 对客户端进行响应
// 获取通信连接
auto conn = server->get_con_from_hdl(hdl);
// 发送数据
conn->send(body + "-Hello!", websocketpp::frame::opcode::value::text);
}
int main()
{
// 1. 实例化服务器对象
server_t server;
// 2. 关闭日志输出
server.set_access_channels(websocketpp::log::alevel::none);
// 3. 初始化asio框架,启用地址重用
server.init_asio();
server.set_reuse_addr(true);
// 4. 设置消息处理/连接握手成功/连接关闭回调函数
server.set_open_handler(onOpen);
server.set_close_handler(onClose);
auto msg_hadler = std::bind(onMessage, &server, std::placeholders::_1, std::placeholders::_2);
server.set_message_handler(msg_hadler);
// 5. 设置监听端口,开始监听
server.listen(9090);
server.start_accept();
// 6. 启动服务器
server.run();
return 0;
}
makefile 文件:
makefile
main : main.cc
g++ -std=c++17 $^ -o $@ -lpthread -lboost_system
客户端测试代码
下面我们提供一个测试 websocket 的客户端代码,我们只需要生成一份 html 文件然后打开它即可!
html
<!DOCTYPE html>
<html lang="en">
<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 实例
// ws://192.168.51.100:8888
// ws 表示 WebSocket 协议
// 192.168.51.100 表示服务器地址
// 8888 表示服务器绑定的端口
let websocket = new WebSocket("ws://113.45.137.183: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 服务器
websocket.send(input.value);
}
</script>
</body>
</html>
在浏览器打开 html 文件后,建立长连接,可以在浏览器按 f12 进行查看:

对话框输入内容后会有响应:

