目标:认识理解websocket协议、websocket切换过程和websocket协议格式。认识和学会使用websocketpp库常用接口。了解websocketpp库搭建服务器流程,认识和学会使用websocketpp库bin接口,最后使用websocketpp库搭建服务器。
初识websocket
WebSocket是从HTML5开始支持的一种网页端和服务端保持长连接的消息推送机制。
在传统的Web程序都属于是“一问一答”的形式,即客户端给服务器发送了HTTP请求,服务端才会给客户端返回一个HTTP响应。在这种情况下,服务端属于被动的一方,如果客户端不给服务端发送HTTP请求,服务端是不会主动给客户端发送HTTP响应的。
而在网页即时聊天或者五子棋对战中这种程序中,都是非常依赖“消息推送”的,即需要服务端主动推送消息给客户端。因此,只是使用原生的HTTP协议,想要实现消息推送一般需要通过轮询的方式实现。
基于上述两个问题,就产生了WebSocket协议。WebSocket更接近于TCP这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动地向对方发送数据。
原理解析
在建立TCP连接后,客户端向服务端发送一个HTTP请求,希望可以切换协议,切换成websocket协议。在HTTP请求当中,包含的重要信息有:
HTTP请求行 GET/ws HTTP/1.1
希望切换协议 Connection:Upgrade
切换的协议格式 Upgrade:Websocket
切换的协议的版本 Sec-WebSocket-Version:xxx
通信的钥匙 Sec-WebSocket-Key:xxx
服务端收到请求后,会查看客户端想要切换的协议和版本自己是否支持,如果支持,那么就会同意切换,并且发送HTTP响应给客户端,HTTP响应中包含的重要信息有:
响应行 HTTP/1.1 101 xxx 101表示切换协议的响应
切换协议 Connection:Upgrade
切换的协议格式 Upgrade:Websocket
通信的钥匙,也表示同意切换 Sec-WebSocket-Accept:xxx
切换完成,后续客户端和服务端直接就可以使用websocket协议进行通信,服务端可以主动给客户端推送请求了。
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],即数据的每一个字节,逐个字节都需要与mask-key的四个字节进行异或操作。
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服务器。
下面是websocketpp的常用接口,用于在写项目时做参考:
//需要记住websocketpp命名空间
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);/*清除指定等级的⽇志*/
/*设置指定事件的回调函数*/
/*回调函数相关接口:针对不同事件设置不同的处理函数,
websocketpp搭建了服务器之后,给不同的事件设置了不同的处理函数指针这些指针,
可以指向指定的函数,当服务器收到了指定的数据,
触发了指定的事件后就会通过函数指针去调用这些函数这时候,
我们程序员就可以编写一些业务处理函数,将其设置为对应事件的业务处理函数*/
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 <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请求头部*/
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,
};
}
}
}
使用websocketpp搭建服务器
搭建服务器的基本流程
1.实例化server对象。
2.设置日志输出等级。
3.初始化asio框架种的调度器。
4.设置业务处理回调函数(具体业务处理的函数由我们自己实现) 。5.设置服务器监听端口。
6.开始获取新建连接。7.启动服务器。
bind的使用
C++11中的bind,作用是用于实现对函数进行参数绑定的功能。
比如:我们实现了一个print函数:
void print(char *str)f std:cout << str << std:endl; }
在调用的时候,我们需要传入数据:print("nihao");
如果选择使用bind将函数和参数进行绑定,那么就不需要传参数了。
auto func = std:.bind(print, "nihao");
对print函数进行参数绑定并生成了一个新的可调用对象func
func();函数调用等价于print("nihao");
此外,如果还有参数传入,比如:
void print(char *str, int num){ std:cout << str << std:endl; }
print("nihao",10);调用函数的时候需要我们传入参数
在绑定的时候,增加 std:placeholders:._ 1,表示可以增加一个新的参数进去。
auto func = std.bind(print,"nihao",std:placeholders:._ 1);
对print函数进行参数绑定并生成了一个新的可调用对象func
func(10);函数调用等价于print("nihao",10);
示例代码:
#include<iostream>
#include<string>
#include<functional>
void print(const char* str,int num)
{
std::cout<<str<<num<<std::endl;
}
int main()
{
//print("nihao",10);
auto func = std::bind(print,"nihao",std::placeholders::_1);
func(23);
return 0;
}
使用websocketpp搭建简单服务器
通过上面搭建服务器的基本流程,我们可以逐一实现出来:
1.实例化server对象
从websocketpp的常用接口的介绍中可以看到,server类继承endpoint类,需要传入模板参数websocketpp中的config,而需要用到asio框架。
/*定义server类的类型,可变参数为websocketpp::config::asio,
因为server继承的endpoint类需要传入这个模板参数*/
typedef websocketpp::server<websocketpp::config::asio> wsserver_t;
wsserver_t wssrv;
完整代码如下:
#include<iostream>
#include<string>
//server的类包含在了webcoketpp中的server.hpp中
#include<websocketpp/server.hpp>
/*需要时用asio框架,就需要有asio的头文件,也是包含在了websocketpp,
我们需要的是非ssl加密的,因此选择asio_no_tls.hpp*/
#include<websocketpp/config/asio_no_tls.hpp>
/*定义server类的类型,可变参数为websocketpp::config::asio,
因为server继承的endpoint类需要传入这个模板参数*/
typedef websocketpp::server<websocketpp::config::asio> wsserver_t;
// 业务请求——处理http请求的回调函数 返回⼀个html欢迎⻚⾯
void http_callback(wsserver_t* srv,websocketpp::connection_hdl hdl)
{
//给客户端返回一个hello world的页面
/*通过server获取connection*/
wsserver_t::connection_ptr conn = srv->get_con_from_hdl(hdl);
/*获取请求正⽂*/
std::cout<<"body: "<<conn->get_request_body()<<std::endl;
/*获取http请求对象*/
websocketpp::http::parser::request req = conn->get_request();
/*获取请求⽅法*/
std::cout<<"method: "<<req.get_method()<<std::endl;
/*获取请求uri接⼝*/
std::cout<<"uri: "<<req.get_uri()<<std::endl;
std::string body = "<html><body><h1>Hello World</h1></body></html>";
//进行响应
conn->set_body(body);
conn->append_header("Content-Type","text/html");
conn->set_status(websocketpp::http::status_code::ok);
}
// websocket连接成功的回调函数
void wsopen_callback(wsserver_t* srv,websocketpp::connection_hdl hdl)
{
std::cout<<"websocket握手成功!\n";
}
// websocket关闭连接成功的回调函数
void wsclose_callback(wsserver_t* srv,websocketpp::connection_hdl hdl)
{
std::cout<<"websocket连接断开成功!\n";
}
// websocket连接收到消息的回调函数
void wsmsg_callback(wsserver_t* srv,websocketpp::connection_hdl hdl,wsserver_t::message_ptr msg)
{
wsserver_t::connection_ptr conn = srv->get_con_from_hdl(hdl);
//输出信息
std::cout<<"wsmsg: "<<msg->get_payload()<<std::endl;
//响应
std::string rsp = "client say: "+msg->get_payload();
conn->send(rsp,websocketpp::frame::opcode::text);
}
int main()
{
//1.实例化server对象
wsserver_t wssrv;
//2.设置日志等级
wssrv.set_access_channels(websocketpp::log::alevel::none);/*禁止日志等级,不打印*/
//3.初始化asio调度器
wssrv.init_asio();
wssrv.set_reuse_addr(true);
//4.设置回调函数
wssrv.set_http_handler(std::bind(http_callback,&wssrv,std::placeholders::_1));
wssrv.set_open_handler(std::bind(wsopen_callback,&wssrv,std::placeholders::_1));
wssrv.set_close_handler(std::bind(wsclose_callback,&wssrv,std::placeholders::_1));
wssrv.set_message_handler(std::bind(wsmsg_callback,&wssrv,std::placeholders::_1,std::placeholders::_2));
//5.设置监听端口
wssrv.listen(8085);
//6.开始获取新连接
wssrv.start_accept();
//7.启动服务器
wssrv.run();
return 0;
}
接着,我们写一个简单的前端页面,测试一下:
<!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://43.139.32.198:8085/");
// 处理连接打开的回调函数
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>
测试
运行服务器后:
打开这个前端页面,输入“你好”:
关闭页面: