五子棋网页游戏(C++在线五子棋对战(网页版)项目:websocket协议)

目标:认识理解websocket协议、websocket切换过程和websocket协议格式。认识和学会使用websocketpp库常用接口。了解websocketpp库搭建服务器流程,认识和学会使用websocketpp库bin接口,最后使用websocketpp库搭建服务器。

初识websocket

平时我们在逛某宝,点击商品查看商品信息,从HTTP角度来看,就是客户端向某宝的服务器发送了一次HTTP请求,服务器接收到请求后,就将HTTP响应发送给客户端,这种情况下,服务器不会主动向客户端发送一次消息,就好像你的女神永远不会给你主动发一次信息一样。

当我们在逛网页的时候,突然发现网页的边边角角上【资源之家】弹出一个框框,上面写着:系兄弟就来砍我,我在沙场等你!点击进去后,我们注册用户,进入到游戏后,发现有一直怪在攻击我们。像这样的我们全程每动过一次鼠标键盘,服务器就会将怪物的移动数据和攻击数据源源不断地发送给我们客户端的情况,其实看起来就是服务器在主动向客户端发送消息。

实质上,网页的前端代码里不断定时地向服务器发送HTTP请求,这就形参了一种伪服务器推的形式,最常见的场景就是用户扫码登录。当二维码出现在页面后,前端页面根本不知道二维码有没有被用户扫描,于是就不断地在一定间隔内向服务器询问,看看有没有人扫码登录。这种间隔一般在1-2秒内,这样可以保证用户在1到2秒内在扫码后得到及时的反馈,这就是H【资源之家】TTP定时轮询。

HTTP定时轮询会有以下两个问题:①当我们使用f12打开页面时,就会发现满屏幕的HTTP请求,这就会消耗带宽,并且增加服务器的负担。②在最快的情况,用户扫码后,都需要等待1到2秒的时候,前端才会再次发送一次HTTP请求,然后才跳转页面,用户会感到明显的卡顿或延迟。

解决HTTP定时轮询的方法可以使用长轮询机制。HTTP请求发出后,会留出一段时间给服务器发送HTTP响应,比如30秒。如果在规定的时间没有返回HTTP响应,那么就会立马重新发送合HTTP请求,这样,就能减少HTTP请求,并且,在一般情况下,用户都会在30秒内进行扫码,此时就会立马得到响应!因此,像这种发起一个请求,在较【资源之家】长的时间内等待服务器的响应的机制,就是所谓的长轮询机制。

不管是HTTP定时轮询还是长轮询机制,本质都是客户端主动向服务器发送消息,服务器才会响应客户端,像扫码这样的场景还可以去用,但是如果是网页游戏呢?一般而言,游戏都会有大量的数据需要服务器主动推送给客户端,此时,需要websocket了。

WebSocket是从HTML5开始支持的一种网页端和服务端保持长连接的消息推送机制。

在传统的Web程序都属于是“一问一答”的形式,即客户端给服务器发送了HTTP请求,服务端才会给客户端返回一个HTTP响应。在这种情况下,服务端属于被动的一方,如果客户端不给服务端发送HTTP请求,服务端是不会主动给客户端发【资源之家】送HTTP响应的。

而像在网页即时聊天或者五子棋对战中这种程序中,都是非常依赖“消息推送”的,即需要服务端主动推送消息给客户端。因此,只是使用原生的HTTP协议,想要实现消息推送一般需要通过轮询的方式实现。

老舅推荐:C++刷爆2023面试圈的C++六大优质简历项目:点击查看C++硬核项目(含源码)

如果你正在挑战(C++后端, SPDK, 内核,音视频,go云原生,Qt)的开发岗位

这里的每一个项目都能征服你的面试leader,斩获满意offer。

基于上述两个问题,就产生了WebSocket协议。WebSocket更接近于TCP这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动地向对方发【资源之家】送数据。

原理解析

从HTTP协议切换到websocket协议

在建立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位。

无论数据有多大,都先读最先的7bit,然后根据其取值决定,还要不要读接下来的16bit或者64bit。如果最开始7bit的值是0-125,那么它就表示了payload的全部长度,那么只读这7bit就可以了。如果最开始的7bit的值是126(0x7E),那么表示payloday的长度范围是126-65535,接下来还需要读16bit。如果最开始7bit的值伪127(0x7F),表示payload的长度大于等于65535,接下来还需要读64bit。

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> classendpoin【资源之家】t :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::functi【资源之家】on<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& pa【资源之家】yload, 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 h【资源之家】dl);/*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, time【资源之家】r_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 , publicconfig::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::stri【资源之家】ng 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::strin【资源之家】g 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; staticle【资源之家】velconst 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; staticlevel【资源之家】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, pa【资源之家】rtial_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, m【资源之家】ethod_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 = 4【资源之家】15, 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 = 5【资源之家】02, 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, r【资源之家】sv4 =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, }; } } }

①lib命名空间是websocketpp命名空间中的子命名空间,它常用的函数有:

lib::weak_ptr:弱指针类,用【资源之家】于表示指向对象的非拥有引用。 lib::shared_ptr:共享指针类,用于管理动态分配的对象的所有权。 lib::asio::steady_timer:基于 asio 的稳定定时器类,用于定时触发事件。 lib::function:函数对象类,用于保存和调用可调用对象。

那么在上面提供的类和函数接口中,有用到lib命名空间的有:

typedef lib::weak_ptr<void> connection_hdl;:这里使用了 lib::weak_ptr 类型来定义了 connection_hdl 类型,用于代表 WebSocket 连接的句柄。 typedeflib【资源之家】::shared_ptr<lib::asio::steady_timer> timer_ptr;:这里使用了 lib::shared_ptr 类型来定义了 timer_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(conn【资源之家】ection_hdl,message_ptr)>

在 typedef lib::weak_ptr<void> connection_hdl; 这行代码中,lib::weak_ptr<void> 使用了 void 类型来实例化一个弱指针 connection_hdl。在这种情况下,void 是作为一个占位符来使用,表示 connection_hdl 是一个可以指向任何类型对象的弱指针。换句话说,typedef lib::weak_ptr<void> connection_hdl; 将 connection_hdl 定义为一个可以指向任何类型对象的弱指针类型,具体的指向类型将由上下文中的类型转换来确【资源之家】定。

在typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;这行代码中,lib::asio::steady_timer 是 asio 库中的一个类,用于创建基于时间的稳定定时器。该定时器可以用来创建和管理在指定时间点触发的事件。timer_ptr指针在后续中,用作于session的定时器。

使用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<stri【资源之家】ng> #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中 #in【资源之家】clude<websocketpp/server.hpp> /*需要时用asio框架,就需要有asio的头文件,也是包含在了websocketpp, 我们需要的是非ssl加密的,因此选择asio_no_tls.hpp*/ #include<websocketpp/config/asio_no_tls.hpp> /*定义server类的类型,可变参数为websocketpp::config::asio, 因为server继承的endpoint类需要传入这个模板参数*/ typedefwebsocketpp::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_heade【资源之家】r(“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::connec【资源之家】tion_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::s【资源之家】tring 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_c【资源之家】allback,&wssrv,std::placeholders::_1,std::placeholders::_2)); //5.设置监听端口 wssrv.listen(8085); //6.开始获取新连接 wssrv.start_accept(); //7.启动服务器 wssrv.run(); return 0; }

代码解释:

对于http_callback回调函数和wsmsg_callback回调函数:HTTP请求回调处理函数,是专门处理来自HTTP请求的,而websocket消息处理回调函数,是专门处理websocket请求的。而且,HTTP请求回调函数,需要就是先获取了来自客户端的连接,然后通【资源之家】过连接,获取HTTP请求中的正文,然后获取HTTP请求的对象,通过这个对象,获取uri和方法。然后根据方法和uri来返回内容。而websocket请求回调处理函数,是先获取来自客户端的连接,然后直接通过send直接响应回去,不需要获取uri和方法之类的信息。

总结一点就是:

HTTP请求回调处理函数主要是处理来自客户端的HTTP请求,它从连接对象中获取HTTP请求的正文,并通过请求对象获取URI和方法等信息,然后根据不同的方法和URI来进行相应的处理,最后构建HTTP响应对象并发送回客户端。HTTP是一种无状态协议,每个请求都是独立的。 WebSocket消息处理回调函数主要是处理来自客户端的We【资源之家】bSocket消息,它从连接对象中获取WebSocket消息的内容,并进行相应的处理逻辑。不像HTTP请求那样需要获取URI和方法等信息,WebSocket是一种双向通信协议,服务器和客户端可以在持久连接上进行实时双向通信。这个回调函数通过使用连接对象的 send 方法直接将响应消息发送回客户端。

接着,我们写一个简单的前端页面,测试一下:

<!DOCTYPE html> <html lang=“en”> <head> <meta charset=“UTF-8”> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <meta name=“viewport” con【资源之家】tent=“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); } </scrip【资源之家】t> </body> </html>

测试

运行服务器后:

打开这个前端页面,输入“你好”:

关闭页面: