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
请求:
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/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
协议的客户端连接。报文字段比较多,我们重点关注这几个字段:
WebSocket
协议传输数据以消息为概念单位,⼀个消息有可能由⼀个或多个帧组成1
个比特大小: 1
,表示这是消息的最后一个分片0
,表示这不是消息的最后一个分片1
个比特大小: 1
,若收到不全为 0
的数据帧,且未协商扩展则立即终止连接。4
个比特,标志当前数据帧的类型: Opcode
为 0
时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。Payload数据
字段进行掩码操作: 1
个比特大小: 1
,那么 Masking-key
字段中会定义一个掩码键,并用这个掩码键对数据载荷进行反掩码。7
位、7+16
位、7+64
位,假设 x
为数据载荷的长度,则其表示如下: 2
个字节代表⼀个 16
位的无符号整数,该无符号整数的值为数据的长度8
个字节代表⼀个 64
位的无符号整数(最高位为 0
),该无符号整数的值为数据的长度0
或 4
字节 Mask
为 1
,则包含 4
字节的 Masking-key
Mask
为 0
,则不包含 Masking-key
0
字节。所有的扩展都必须声明扩展数据的长度,扩展如何使用必须在握手阶段就协商好 WebSocketpp
是⼀个跨平台的开源(BSD许可证)头部 专用C++库,它实现了 RFC6455
(WebSocket协议)和 RFC7692
(WebSocketCompression Extensions)。它允许将 WebSocket
客户端和服务器功能集成到 C++
程序中。在最常见的配置中,全功能网络 I/O
由 Asio
网络库提供。
WebSocketpp
的主要特性包括:
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
类的函数!connection
、message_buffer
等类都是用于在回调函数中请求和响应的类!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;
typedef lib::function<void(connection_hdl, message_ptr)> 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);
// 取消定时器,立刻触发之前设置的callback函数
std::size_t cancel();
};
template <typename config>
class server
: public endpoint<connection<config>, config>
{
// 初始化并启动服务端监听连接的accept事件处理
void start_accept();
};
template <typename config>
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请求头部中key关键字的
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)
};
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();
}
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;
static level const fail = 0x2000;
static level const access_core = 0x00003003;
static level const all = 0xffffffff;
};
}
namespace http
{
// 状态码
namespace status_code
{
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,
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,
not_extended = 510,
network_authentication_required = 511
};
}
}
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,
};
}
}
}
一般我们搭建服务器都是统一形式的,大概流程如下:
server
对象 websocketpp
里面的 server
,注意不要落了模板,一般模板里面的 config
这里都是使用 websocketpp::config::asio
asio
调度器和启用地址重用connection_hdl
类型是要通过命名空间 websocketpp
来引入的,就是 websocketpp::connection_hdl
。而对于 set_http_handler()
函数来说,它还多了一个函数指针 message_ptr
,因为它是属于 endpoint
类,或者它的子类 server
的,所以通过我们 typedef
的 server
类型 wsserver_t
来取出这个函数指针,就是 wsserver_t::message_ptr
。 我们先将主体框架搭起来,其中头文件我们要使用的是 server.hpp
和 asio_no_tls.hpp
,因为 websocketpp
库是放在 /usr/include/
中的,所以要使用这些头文件的话还得进入它们对应的路径下面引入。
#include <iostream>
#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
using wsserver_t = websocketpp::server<websocketpp::config::asio>;
void open_callback(websocketpp::connection_hdl hdl)
{}
void close_callback(websocketpp::connection_hdl hdl)
{}
void http_callback(websocketpp::connection_hdl hdl)
{}
void message_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msgptr)
{}
int main()
{
// 1.实例化server对象
wsserver_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(open_callback);
server.set_close_handler(close_callback);
server.set_message_handler(message_callback);
server.set_http_handler(http_callback);
// 5.设置监听窗口
server.listen(8080);
// 6.开始获取新连接
server.start_accept();
// 7.启动服务器
server.run();
return 0;
}
但是一般我们在回调函数中还需要用到 server
对象的 指针 或者 引用,所以要多传一个参数,但是我们不希望影响到这些函数的参数列表,因为比如这里的 typedef lib::function<void(connection_hdl)> open_handler
等函数已经要求参数中只能有一个参数,那要是我们想多传一个参数又不想影响包装器,怎么办呢❓❓❓
此时就需要使用 std::bind
绑定器,终于有用武之地了哈哈!
绑定器的作用就是可以将一些参数直接绑定到形参,相当于让计算机帮我们自动填我们预先设置好的参数,就仿佛这个参数不存在一样,有点像缺省参数,但不同的是,绑定器绑定的函数可以生成一个新的函数指针进行返回!
下面我们就给每个回调函数多传一个 server
对象的指针,然后在设置回调函数那里进行参数绑定:
void open_callback(wsserver_t* server, websocketpp::connection_hdl hdl)
{}
void close_callback(wsserver_t* server, websocketpp::connection_hdl hdl)
{}
void http_callback(wsserver_t* server, websocketpp::connection_hdl hdl)
{}
void message_callback(wsserver_t* server, websocketpp::connection_hdl hdl, wsserver_t::message_ptr msgptr)
{}
int main()
{
......
// 4.设置回调函数
server.set_open_handler(std::bind(open_callback, &server, std::placeholders::_1));
server.set_close_handler(std::bind(close_callback, &server, std::placeholders::_1));
server.set_message_handler(std::bind(message_callback, &server, std::placeholders::_1, std::placeholders::_2));
server.set_http_handler(std::bind(http_callback, &server, std::placeholders::_1));
......
}
http_callback() 函数
// 任务:打印请求内容,并且设置响应内容
void http_callback(wsserver_t* server, websocketpp::connection_hdl hdl)
{
// 首先将一些请求内容打印到终端
wsserver_t::connection_ptr conn_ptr = server->get_con_from_hdl(hdl); // 获取connection_ptr
std::cout << "body: " << conn_ptr->get_request_body() << std::endl; // 打印请求正文
websocketpp::http::parser::request req = conn_ptr->get_request(); // 获取http请求对象
std::cout << "method: " << req.get_method() << std::endl; // 打印请求方法
std::cout << "uri: " << req.get_uri() << std::endl; // 打印请求路径资源
std::cout << "version: " << req.get_version() << std::endl; // 打印请求版本
// 然后填充响应资源
std::string body = "<html><body><h1>Hello Liren!</h1></body></html>"; // 写一个简单的html页面格式
conn_ptr->set_body(body); // 设置响应正文
conn_ptr->append_header("Content-Type", "text/html"); // 设置响应头部
conn_ptr->set_status(websocketpp::http::status_code::ok); // 设置响应状态码
}
open_callback 函数
void open_callback(wsserver_t* server, websocketpp::connection_hdl hdl)
{
std::cout << "websocket握手成功!" << std::endl;
}
close_callback 函数
void close_callback(wsserver_t* server, websocketpp::connection_hdl hdl)
{
std::cout << "websocket断开连接!" << std::endl;
}
message_callback 函数
// 任务:收到一个消息进行打印,然后进行响应
void message_callback(wsserver_t* server, websocketpp::connection_hdl hdl, wsserver_t::message_ptr msgptr)
{
// 打印获取的消息
wsserver_t::connection_ptr conn_ptr = server->get_con_from_hdl(hdl); // 获取connection_ptr
std::cout << "message: " << msgptr->get_payload() << std::endl; // 打印消息
// 响应
std::string callback = "server say: client say " + msgptr->get_payload(); // 创建响应内容
conn_ptr->send(callback, websocketpp::frame::opcode::text); // 发送响应
}
因为 websocketpp
用到了 pthread
和 boost_system
,所以要在编译的时候指定:
server:wsserver.cpp
g++ -o $@ $^ -std=c++11 -lboost_system -lpthread
.PHONY:clean
clean:
rm -f server
如果使用的是 http
客户端,也就是直接用浏览器输入 ip + 端口号,那么就能直接访问了。其连接等时候 触发的是 http_callback()
函数。
而这里我们用自己写一个 html
页面来充当 websocket
客户端,来测试一下服务器是否能正确的收发信息,其 触发的是 message_callback()
函数!
<!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
// 类比http:
// ws表示websocket协议
// 192.168.51.100 表示服务器地址
// 8888表⽰服务器绑定的端⼝
let websocket = new WebSocket("ws://81.71.97.127:8080/");
// 处理连接打开的回调函数
websocket.onopen = function() {
alert("连接建⽴");
}
// 处理收到消息的回调函数
// 控制台打印消息
websocket.onmessage = function(e) {
alert("收到消息: " + e.data);
}
// 处理连接异常的回调函数
websocket.onerror = function() {
alert("连接异常");
}
// 处理连接关闭的回调函数
websocket.onclose = function() {
alert("连接关闭");
}
// 实现点击按钮后, 通过 websocket实例 向服务器发送请求
let input = document.querySelector('#message');
let button = document.querySelector('#submit');
button.onclick = function() {
console.log("发送消息: " + input.value);
websocket.send(input.value);
}
</script>
</body>
</html>
然后我们在桌面可以创建一个 html
文件,将其复制进去之后,打开这个文件,就能测试了!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有