21.添加websocket模块

这里默认读者了解websocket协议,若是还不了解可以看下这篇文章wesocket协议。

websocket主要有三个步骤,1通过HTTP进行握手连接,2进行双向通信,3.协商断开连接

第一步的握手连接需要HTTP,所以还需要使用到上一节讲解的HTTP模块中的部分内容HttpContext类和HttpRequest类。

建立握手连接后,就不再需要使用HTTP了。之后就是通过帧的形式就行数据传输。

那可以给数据帧或者说是数据包封装成一个类WebsocketPacket。

1.WebsocketPacket类

该类一定是有帧头的一些信息,如fin,opcode等等。

enum WSOpcodeType : uint8_t
{WSOpcode_Continue = 0x0,WSOpcode_Text = 0x1,WSOpcode_Binary = 0x2,WSOpcode_Close = 0x8,WSOpcode_Ping = 0x9,WSOpcode_Pong = 0xA,
};class WebsocketPacket
{
public:WebsocketPacket():fin_(1)	//1表示是消息的最后一个分片,表示不分包, rsv1_(0), rsv2_(0), rsv3_(0), opcode_(1)	//默认是发送文本帧, mask_(0), payload_length_(0){memset(masking_key_, 0, sizeof(masking_key_));}~WebsocketPacket(){ }void reset(){fin_ = 1;	//默认是1rsv1_ = 0;rsv2_ = 0;rsv3_ = 0;opcode_ = 1;//默认是发送文本帧mask_ = 0;memset(masking_key_, 0, sizeof(masking_key_));payload_length_ = 0;}void decodeFrame(Buffer* frameBuf, Buffer* output);void encodeFrame(Buffer* output, Buffer* data)const;public:uint8_t fin() const { return fin_; }uint8_t rsv1() const { return rsv1_; }uint8_t rsv2()const { return rsv2_; }//省略部分成员的的获取函数如rsv3()等等,这里就没有显示出来,可查看完整代码//...................void set_fin(uint8_t fin) { fin_ = fin; }void set_rsv1(uint8_t rsv1) { rsv1_ = rsv1; }void set_rsv2(uint8_t rsv2) { rsv2_ = rsv2; }//省略部分成员的设置的函数如set_rsv3(uint8_t rsv3)等等,这里就没有显示出来private:uint8_t fin_;uint8_t rsv1_;uint8_t rsv2_;uint8_t rsv3_;uint8_t opcode_;uint8_t mask_;uint8_t masking_key_[4];uint64_t payload_length_;
};

这里重点就是两个函数decodeFrameencodeFrame。从名字就可以看出来,一个是解帧,即是解析客户端发送过来的帧;另一个是封装成帧,发送给客户端。

1.1decodeFrame函数

按照websocket协议的数据帧进行解析即可。

这里要注意的是,若payloadlength是多字节的话,需要进行转序。

有掩码的操作就是这样,可以不用做过多了解,但想了解多点也可以。

void WebsocketPacket::decodeFrame(Buffer* frameBuf,Buffer* output)
{const char* msg = frameBuf->peek();int pos = 0;//获取fin_fin_=((unsigned char)msg[pos] >> 7);//获取opcode_opcode_ = msg[pos] & 0x0f;pos++;//获取mask_mask_ = (unsigned char)msg[pos] >> 7;//获取payload_length_payload_length_ = msg[pos] & 0x7f;pos++;if (payload_length_ == 126) {uint16_t length = 0;memcpy(&length, msg + pos, 2);pos += 2;payload_length_ = ntohs(length);}else if (payload_length_ == 127) {uint64_t length = 0;memcpy(&length, msg + pos, 8);pos += 8;payload_length_ = ntohl(length);}//获取masking_key_if (mask_ == 1) {for (int i = 0; i < 4; i++)masking_key_[i] = msg[pos + i];pos += 4;}if (mask_ != 1) {output->append(msg + pos, payload_length_);}else {for (uint64 i = 0; i < payload_length_; i++) {output->append(msg[pos + i] ^ masking_key_[i % 4], payload_length_);}}}

1.2encodeFrame

也是按照websocket协议的数据帧进行封装帧即可。

注意是若payloadlength是多字节的话,需要进行转序。

还有服务器端发送的是没有掩码的。

void WebsocketPacket::encodeFrame(Buffer* output,Buffer* data)const
{uint8_t onebyte = 0;onebyte |= (fin_ << 7);onebyte |= (rsv1_ << 6);onebyte |= (rsv2_ << 5);onebyte |= (rsv3_ << 4);onebyte |= (opcode_ & 0x0F);output->append((char*)&onebyte, 1);onebyte = 0;//set mask flagonebyte = onebyte | (mask_ << 7);int length = data->readableBytes();if (length < 126){onebyte |= length;output->append((char*)&onebyte, 1);}else if (length == 126){onebyte |= length;output->append((char*)&onebyte, 1);auto len = htons(length);output->append((char*)&len, 2);}else if (length == 127){onebyte |= length;output->append((char*)&onebyte, 1);// also can use htonll if you have itonebyte = (payload_length_ >> 56) & 0xFF;output->append((char*)&onebyte, 1);onebyte = (payload_length_ >> 48) & 0xFF;output->append((char*)&onebyte, 1);onebyte = (payload_length_ >> 40) & 0xFF;output->append((char*)&onebyte, 1);onebyte = (payload_length_ >> 32) & 0xFF;output->append((char*)&onebyte, 1);onebyte = (payload_length_ >> 24) & 0xFF;output->append((char*)&onebyte, 1);onebyte = (payload_length_ >> 16) & 0xFF;output->append((char*)&onebyte, 1);onebyte = (payload_length_ >> 8) & 0xFF;output->append((char*)&onebyte, 1);onebyte = payload_length_ & 0XFF;output->append((char*)&onebyte, 1);}if (mask_ == 1)	//服务器发送给客户端的,是不带mask_key的,所以这个是没有用到的{output->append((char*)masking_key_, 4);	// save masking keychar value = 0;for (uint64_t i = 0; i < payload_length_; ++i) {value = *(char*)(data->peek());data->retrieve(1);value = value ^ masking_key_[i % 4];output->append(&value, 1);}}else {output->append(data->peek(), data->readableBytes());}
}

数据帧解析和封装说完了,那就到握手连接和双向通信的了。可以封装个类WebsocketContext。

2.WebsocketContext类

该类有点类似上一节的HttpContext类,解包和封包的操作已有WebsocketPacket去处理。那这个类需要处理握手连接等问题。

WebsocketContext会拥有WebsocketPacket类型的请求包requestPacket_。其中函数parseData就是调用requestPacket_的decodeFrame

websocketStatus_表示是否已握手连接的,构造函数是默认kUnconnect的。

class WebsocketContext {
public:enum class WebsocketSTATUS { kUnconnect, kHandsharked };WebsocketContext();~WebsocketContext();void handleShared(Buffer* buf, const std::string& server_key);void parseData(Buffer* buf, Buffer* output);void reset() { requestPacket_.reset(); }void setwebsocketHandshared() { websocketStatus_ = WebsocketSTATUS::kHandsharked; }WebsocketSTATUS getWebsocketSTATUS()const { return websocketStatus_; }uint8_t getRequestOpcode()const { return requestPacket_.opcode(); }private:WebsocketSTATUS websocketStatus_;WebsocketPacket requestPacket_;
};

那么接下来看看如何握手连接的

2.1handleShared

源代码里会有base64和sha1的代码。

这里就主要是按照给定的服务器端回复的握手格式进行回复。

#include "base64.h"
#include "sha1.h"#define MAGIC_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"void WebsocketContext::handleShared(Buffer* buf, const std::string& serverKey)
{buf->append("HTTP/1.1 101 Switching Protocols\r\n");buf->append("Connection: upgrade\r\n");buf->append("Sec-WebSocket-Accept: ");std::string server_key = serverKey;server_key += MAGIC_KEY;SHA1 sha;unsigned int message_digest[5];sha.Reset();sha << server_key.c_str();sha.Result(message_digest);for (int i = 0; i < 5; i++) {message_digest[i] = htonl(message_digest[i]);}server_key = base64_encode(reinterpret_cast<const unsigned char*>(message_digest), 20);server_key += "\r\n";buf->append(server_key);buf->append("Upgrade: websocket\r\n\r\n");
}

3.websocketServer

接着封装一个websocketServer类方便使用。这个类和HttpServer是很相似的,流程和HttpServer也是差不多的。

class websocketServer
{
public:using WebsocketCallback = std::function<void(const Buffer*, Buffer*, WebsocketPacket& respondPacket)>;websocketServer(EventLoop* loop, const InetAddr& listenAddr);void setHttpCallback(const WebsocketCallback& cb) { websocketCallback_ = cb; }void start(int numThreads);private:void onConnetion(const ConnectionPtr& conn);	//连接到来的回调函数void onMessage(const ConnectionPtr& conn, Buffer* buf);	//消息到来的回调函数void handleData(const ConnectionPtr& conn, WebsocketContext* websocket, Buffer* buf);Server server_;WebsocketCallback websocketCallback_;
};

setHttpCallback是设置用户的业务函数。

3.1onConnetion函数

连接到来时候会执行该函数

void websocketServer::onConnetion(const ConnectionPtr& conn)
{if (conn->connected()) {//conn->setContext(HttpContext());   //这是之前HttpServer的conn->setContext(WebsocketContext());//测试使用,用来测试绑定不符合的类型//int a = 10;  conn->setContext(a);}
}

3.2onMessage

消息到来的时候会执行该函数。

该函数就先获取该conn的getMutableContext,得到该WebsocketContext类对象。

之后就两种情况,一种是还没进行握手的,一种是已进行握手的,进行通信的。

需要握手的 ,先通过解析http请求,获取请求头中的特定字段 ,发送特殊的HTTP响应头进行握手确认。

void websocketServer::onMessage(const ConnectionPtr& conn, Buffer* buf)
{auto context = std::any_cast<WebsocketContext>(conn->getMutableContext());	//c++117if (!context) {printf("context kong...\n");LOG_ERROR << "context is bad\n";return;}if (context->getWebsocketSTATUS() == WebsocketContext::WebsocketSTATUS::kUnconnect) {HttpContext http;if (!http.parseRequest(buf)) {conn->send("HTTP/1.1 400 Bad Request\r\n\r\n");conn->shutdown();}if (http.gotAll()) {auto httpRequese = http.request();if (httpRequese.getHeader("Upgrade") != "websocket" ||httpRequese.getHeader("Connection") != "Upgrade" ||httpRequese.getHeader("Sec-WebSocket-Version") != "13" ||httpRequese.getHeader("Sec-WebSocket-Key") == "") {conn->send("HTTP/1.1 400 Bad Request\r\n\r\n");conn->shutdown();return;		//表明不是websocket连接}Buffer handsharedbuf;context->handleShared(&handsharedbuf, http.request().getHeader("Sec-WebSocket-Key"));conn->send(&handsharedbuf);context->setwebsocketHandshared();//设置建立握手}}else {handleData(conn, context, buf);}
}

另一种情况,可以进行通信的,调用函数handleData

主要流程:

  1. 先调用websocketContext的解析帧的函数parseData。之后得到fin,opcode等信息并把传输过来的数据写入到DataBuf中去。
  2. 之后再根据情况进行设置opcode。
  3. 之后再调用用户设置的回调函数来进行用户的业务处理。
  4. 再进行封装帧操作,发送给客户端。

这里需要注意的是:当收到客户主动发送过来的opcode是0x8(即是关闭),需要服务器端也返回ox8给客户端。因为websocket关闭是双方协商的。之后客户端收到0x8后就会关闭连接了。

void websocketServer::handleData(const ConnectionPtr& conn, WebsocketContext* websocket, Buffer* buf)
{Buffer DataBuf;websocket->parseData(buf, &DataBuf);WebsocketPacket respondPacket;int opcode = websocket->getRequestOpcode();switch (opcode){case WSOpcodeType::WSOpcode_Continue:respondPacket.set_opcode(WSOpcodeType::WSOpcode_Continue);break;case WSOpcodeType::WSOpcode_Text:respondPacket.set_opcode(WSOpcodeType::WSOpcode_Text);break;case WSOpcodeType::WSOpcode_Binary:respondPacket.set_opcode(WSOpcodeType::WSOpcode_Binary);break;case WSOpcodeType::WSOpcode_Close:respondPacket.set_opcode(WSOpcodeType::WSOpcode_Close);break;case WSOpcodeType::WSOpcode_Ping:respondPacket.set_opcode(WSOpcodeType::WSOpcode_Pong);	//进行心跳响应break;case WSOpcodeType::WSOpcode_Pong:		//表示这是一个心跳响应(pong),那就不用回复了return;default:LOG_INFO << "WebSocketEndpoint - recv an unknown opcode.\n";return;}Buffer sendbuf;if(opcode != WSOpcodeType::WSOpcode_Close && opcode != WSOpcode_Ping && opcode != WSOpcode_Pong)websocketCallback_(&DataBuf, &sendbuf, respondPacket);Buffer frameBuf;respondPacket.encodeFrame(&frameBuf, &sendbuf);conn->send(&frameBuf);websocket->reset();
}

4.websocket的使用例子

用户主要就是写自己的业务函数,之后调用setHttpCallback设置自己的业务函数。

//用户的业务函数
void onRequest(const Buffer* input, Buffer* output){//进行echo回复output->append(input->peek(),input->readableBytes());
}int main(int argc, char* argv[])
{int numThreads = 0;if (argc > 1) {Logger::setLogLevel(Logger::LogLevel::WARN);numThreads = atoi(argv[1]);}EventLoop loop;websocketServer server(&loop, InetAddr(9999));server.setHttpCallback(onRequest);   //设置自己的业务函数server.start(numThreads);loop.loop();return 0;
}

websocket的服务器基本就是这样了。

5.修复问题,Connection::handleRead()中的问题

这是在测试websocket的时候发现的问题。

在有新消息到来的时刻,是会调用Connection::handleRead()函数

那么在该函数中需要添加inputBuffer_.retrieve(inputBuffer_.readableBytes());这句代码。

void Connection::handleRead()
{int savedErrno = 0;auto n = inputBuffer_.readFd(fd(), &savedErrno);if (n > 0) {//这个是用户设置好的函数messageCallback_(shared_from_this(), &inputBuffer_);//新添加的,没有这句代码的话,那readindex可能就没有变化,那读取的数据就会包含上一次的inputBuffer_.retrieve(inputBuffer_.readableBytes());//messageCallback_中处理好读取的数据后,更新readerIndex位置}else if (n == 0) {//表示客户端关闭了连接handleClose();}//....省略了对错误的处理
}

不然每次inputBuffer_的readerIndex就不会改变,那么每次input中获取到的数据都会包含上一次的数据

也可以不添加,让用户在写业务函数的时候手动添加去更新readerIndex,但这样就不方便了,用户不应该去处理这些问题的。

在server_v10代码中,加不加这句代码是没有影响的,是因为用户的业务函数使用了Buffer::retrieveAllAsString()函数,该函数是会更新buf的readerIndex的,所以才会没有问题的。

//在代码server_v10中用户的业务函数
void onMessage(const ConnectionPtr& conn, Buffer* buf) {std::string msg(buf->retrieveAllAsString());printf("onMessage() %ld bytes reveived:%s\n", msg.size(), msg.c_str());conn->send(msg);
}int main(){//..............
}

但不是每个用户编写自己的业务函数时候都一定使用这个函数的。所以需要在这添加这句代码inputBuffer_.retrieve(inputBuffer_.readableBytes());

可以试试不添加这句代码和添加了这句代码的websocket服务器的效果。

完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v21

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/128674.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python实现猎人猎物优化算法(HPO)优化BP神经网络回归模型(BP神经网络回归算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 猎人猎物优化搜索算法(Hunter–prey optimizer, HPO)是由Naruei& Keynia于2022年提出的一种最新的…

OpenCV(三十三):计算轮廓面积与轮廓长度

1.介绍轮廓面积与轮廓长度 轮廓面积&#xff08;Contour Area&#xff09;是指轮廓所包围的区域的总面积。通常情况下&#xff0c;轮廓面积的单位是像素的平方。 轮廓长度&#xff08;Contour Length&#xff09;又称周长&#xff08;Perimeter&#xff09;&#xff0c;表示轮廓…

Unity 从0开始编写一个技能编辑器_01_分析需求

入职以来一直很想实现一个技能编辑器&#xff0c;在积累了一些经验以后&#xff0c;决定利用ScriptableObject开发一个&#xff0c;在此记录 1.简单的需求分析 在游戏开发中&#xff0c;技能系统是一个至关重要的组成部分。技能决定了游戏角色可以执行的各种动作&#xff0c;例…

代码随想录算法训练营第十八天|513. 找树左下角的值|112. 路径总和|106. 从中序与后序遍历序列构造二叉树

513. 找树左下角的值 题目&#xff1a;给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1 思路一&#xff1a;层序遍历&#xff0c;最后一层的第一个元素&#xff0c;即…

java实时监控mysql数据库变化

对于二次开发来说&#xff0c;很大一部分就找找文件和找数据库的变化情况 对于数据库变化。还没有发现比较好用的监控数据库变化监控软件。 今天&#xff0c;我就给大家介绍一个如何使用mysql自带的功能监控数据库变化 1、打开数据库配置文件my.ini &#xff08;一般在数据库…

c语言 2.0

1.数据类型 数据类型介绍 数据类型&#xff1a;c语言中数据类型有3种&#xff0c;分别是基本数据类型、构造数据类型、指针数据类型。 数据类型的作用&#xff1a;编译器预算数据分配的内存空间大小。 ps&#xff1a;可以通俗理解为&#xff1a;数据类型是用来规范内存的开销…

python DVWA文件上传POC练习

先直接测试POC 抓包 GET /dv/vulnerabilities/sqli/?id1%27unionselect1%2Cmd5%28123%29%23&SubmitSubmit HTTP/1.1Host: 10.9.75.161Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrom…

Tomcat服务的部署及配置优化

文章目录 1. Tomcat的相关介绍1.1 Tomcat简介1.2 Tomcat的核心组件1.2.1 Web容器1.2.2 Servlet容器1.2.3 JSP容器 1.3 Tomcat的功能组件1.3.1 connector连接器1.3.2 container容器1.3.2.1 子容器及其相关功能 1.4 主要作用1.5 Tmocat处理请求的过程 2. Tomcata服务部署2.1 安装…

log4qt库的使用

log4qt库的使用 一,什么是log4qt?二,log4qt的下载三,如何集成log4qt?1.在vs2022中集成log4qt的方法:模块一:配置log4qt的步骤步骤一,将下好的log4qt库进行解压,然后再库文件中,新建build和Log4Qt文件夹步骤二,打开cmake,有两个填写路径的位置.步骤三,点击cmake的configure按钮…

tcp满开始和拥塞避免

tcp的拥塞控制有四种算法&#xff0c;后面的快重传和快恢复是后面新增的&#xff0c; 刚开始会初始化慢开始门限值&#xff0c;并将拥塞窗口值为1往网络中发送&#xff0c;若收到确认包则将拥塞窗口翻倍&#xff0c;执行慢开始算法&#xff0c;当拥塞窗口值达到慢开始门限后&am…

02-Tomcat打破双亲委派机制

上一篇&#xff1a;01-从JDK源码级别剖析JVM类加载机制 Tomcat 如果使用默认的双亲委派类加载机制行不行&#xff1f; 我们思考一下&#xff1a;Tomcat是个web容器&#xff0c; 那么它要解决什么问题&#xff1a; 一个web容器可能需要部署两个应用程序&#xff0c;不同的应用…

Alibaba(获得店铺的所有商品) API 接口

为了进行电商平台 的API开发&#xff0c;首先我们需要做下面几件事情。 1&#xff09;开发者注册一个账号 2&#xff09;然后为每个alibaba应用注册一个应用程序键&#xff08;App Key) 。 3&#xff09;下载alibaba API的SDK并掌握基本的API基础知识和调用 4&#xff09;利…

深入浅出Android同步屏障机制

原文链接 Android Sync Barrier机制 诡异的假死问题 前段时间&#xff0c;项目上遇到了一个假死问题&#xff0c;随机出现&#xff0c;无固定复现规律&#xff0c;大量频繁随机操作后&#xff0c;便会出现假死&#xff0c;整个应用无法操作&#xff0c;不会响应事件&#xff…

【Linux】Systemd 中的单元(Unit)和单元文件(Unit File)怎么理解?

单元&#xff08;Unit&#xff09;单元文件&#xff08;Unit File&#xff09;感谢 &#x1f496; 关于systemd是什么&#xff0c;http://t.csdn.cn/pMkG7这篇文章里有详细说明。 这篇文件我们一起来看看Systemd 中的单元&#xff08;Unit&#xff09;和单元文件&#xff08;Un…

vue使用jsencrypt实现rsa前端加密

实现 RSA 加密 介绍 vue 完成 rsa 加密传输&#xff0c;jsencrypt 实现参数的前端加密 1 安装 jsencrypt npm install jsencrypt2 编写 jsencrypt.js 在 utils 文件夹中新建 jsencrypt.js 文件&#xff0c;内容如下&#xff1a;注意点&#xff1a;一般公钥都是后端生成好的&a…

excl在建模语言中的运用

目录 1.表格的定位 2.数学函数 3.自动填充功能 4.数据透视表的应用 5.切片器 6. Date(),time(),now()&#xff0c;today() 7.文本转日期 8.分裂 9.sumif函数 10.数字转换为文本的方法 11.SUMIFS()函数&#xff1a;多个条件筛选 12.宏 13.提取多个表中&#xff0c;…

大秒杀系统设计

参考链接&#xff1a;http://www.taodudu.cc/news/show-5770725.html?actiononClick 1. 一些数据 大家还记得2013年的小米秒杀吗&#xff1f;三款小米手机各11万台开卖&#xff0c;走的都是大秒系统&#xff0c;3分钟后成为双十一第一家也是最快破亿的旗舰店。 经过日志统计…

appium+jenkins实例构建

自动化测试平台 Jenkins简介 是一个开源软件项目&#xff0c;是基于java开发的一种持续集成工具&#xff0c;用于监控持续重复的工作&#xff0c;旨在提供一个开放易用的软件平台&#xff0c;使软件的持续集成变成可能。 前面我们已经开完测试脚本&#xff0c;也使用bat 批处…

文件上传之图片码混淆绕过(upload的16,17关)

目录 1.upload16关 1.上传gif loadup17关&#xff08;文件内容检查&#xff0c;图片二次渲染&#xff09; 1.上传gif&#xff08;同上面步骤相同&#xff09; 2.条件竞争 1.upload16关 1.上传gif imagecreatefromxxxx函数把图片内容打散&#xff0c;&#xff0c;但是不会…

MySQL用户管理

文章目录 MySQL用户管理1. 用户1.1 用户信息1.2 创建用户1.3 删除用户1.4 修改用户密码 2. 数据库的权限2.1 给用户授权2.2 回收权限2.2 回收权限 MySQL用户管理 如果我们只能使用root用户&#xff0c;这样存在安全隐患。这时&#xff0c;就需要使用MySQL的用户管理。 1. 用户…