Qt QWebSocket网络编程

学习目标:Qt QWebSocket网络编程

学习前置环境

QT TCP多线程网络通信-CSDN博客

学习内容

WebSocket是一种通过单个TCP连接提供全双工通信信道的网络技术。2011年,IETF将WebSocket协议标准化为 RFC6455,QWebSocket可用于客户端应用程序和服务器应用程序。

它实现了浏览器与服务器全双工(full-duplex)通信,允许服务器主动发送信息到客户端。

主要特点:

  1. 与HTTP不同,WebSocket允许服务器主动发送数据给客户端,不需要客户端发起请求。

  2. 建立在TCP协议之上,服务器和客户端之间通过Ws(WebSocket)协议在单个端口上进行全双工通信。

  3. 支持以文本方式或者二进制方式传输数据。

  4. 协议建立在单个TCP连接上,服务器和客户端只需创建一个连接,且连接不会被关闭。

  5. 支持多种编程语言的客户端和服务器端库,如JavaScript,Java,C#,Python等。

常见应用场景:

  1. 聊天室:支持低延迟的实时对话。

  2. 在线game:需要实时同步游戏状态的同步引擎。

  3. 股票行情:需要推送即时行情给客户端的行情软件。

  4. 视频会议:需要语音和视频的低延迟实时通信。

  5. 实时协作编辑:如在线代码编辑器要求实时同步。

QWebSocket是Qt提供的WebSocket功能库。它建立在Qt网络模块之上,实现了RFC6455标准中的WebSocket协议。需要再Qmake文件中加入 QT+=websockets

  1. QWebSocket只支持Text/Binary两种消息格式,不支持其他扩展格式。如果需要补充其他自定义协议,需要开发者在应用层自己处理。

  2. 它支持使用标准的http/https端口80/443访问websocket服务,也支持wss(加密websocket)协议。所以可以很方便地与现有的web服务器交互。

  3. 由于它基于QT CP套接字实现,完全支持所有Qt网络功能,比如代理设置、SSL配置等。这一点相比一些底层的C接口更易用。

  4. 它同时支持主动和被动连接模式。主动连接通过connectToHost(),被动通过监听端口accept()接受新的链接。这两种模式都很方便。

  5. 对于QT GUI应用,可以很方便地进行消息接收与界面更新,避免了多线程编程的复杂性。比如直接在textMessageReceived()里更新界面就行了。

  6. QT5.10后支持了异步I/O,性能较以前有一点提升。对网延的支持也更好了。

QWebSocket常用成员函数


origin()
即 websocket=new QWebSocket("C1我是客户端",QWebSocketProtocol::VersionLatest,this);
websocket->origin()  -》 C1我是客户端

void connectToHost(const QUrl &url) - 用于连接到指定主机的websocket服务,这个函数是异步的。
void close() - 关闭与服务器的连接。
void textMessageReceived(const QString &message) - 收到文本消息时触发的信号,其参数就是收到的文本消息内容。
void binaryMessageReceived(const QByteArray &message) - 收到二进制消息时触发的信号,参数是原始二进制数据。
void error(QAbstractSocket::SocketError socketError) - 发生错误时触发的信号,参数是错误类型。
void stateChanged(QAbstractSocket::SocketState state) - 连接状态变化时触发,可以得知连接是否建立等。
void textMessageSent(qint64 numBytes) - 发送文本消息完成后触发,numBytes是字节数。
void bytesWritten(qint64 bytes) - 消息发送过程中的写入回调, bytes是一个部分发送出去的字节数。
void abort() - 主动断开连接。
bool waitForConnected(int msec = 30000) - 阻塞等待连接建立成功。

QString hostName() - 获取当前连接的主机名,常用于判断连接是否成功。
quint16 port() - 获取主机端口号。
bool openMode() - 判断当前是否为主动连接还是被动接受模式。
void writeTextMessage(const QString &text) - 发送文本消息,相比textMessage等更直观。
void writeMessage(const QByteArray &data) - 发送二进制数据。
qint64 bytesAvailable() - 查看接收缓存中可读取字节数。
qint64 readBufferSize() - 设置双向数据接收缓存大小。
void pauseIncomingPayload() - 暂停接收消息流。
void resumeIncomingPayload() - 恢复接收。
bool isValid() - 检查连接是否有效。

另外,作为QT套接字,它还支持一些通用功能:
void setProxy() - 设置代理。
void encrypt() - 设置SSL安全连接。
void flush() - 强制输出缓存写出。
bool waitForBytesWritten() - 等待数据发送完毕。

 
void QWebSocket::sendTextMessage(const QString &message)  用于发送文本消息
使用这个函数发送文本消息主要有以下几点需要注意的地方:
1发送文本消息前请确保WebSocket连接已经建立。可以通过ReadyState判断连接状态。
2发送的消息内容必须是纯文本,不支持转义编码等更多格式。
3一条消息发送完毕后,会触发textMessageSent()信号通知。
4可以通过waitForBytesWritten()等待数据完全发送出去。
5发送数据顺序可能与收到响应顺序不一致,需要应用层自己处理序号等。
6若消息较大,建议使用write或send到套接字后flush,而不是sendTextMessage。
7跨平台考虑,消息内容编码最好使用QString而不是QByteArray。
8使用该函数发送的文本消息类型,服务端一般对应文本框接受。
9可以绑定消息发送断开连接的异常处理等。

sendTextMessage和writeTextMessage这两个函数都可以用来发送文本消息,但它们有一些区别:

  1. 函数定义不同:
  • sendTextMessage属于QWebSocket的成员函数;

  • writeTextMessage是QAbstractSocket的成员函数,QWebSocket继承于QAbstractSocket。

  1. 发送效率不同:
  • sendTextMessage内部会将消息先转成QByteArray,再通过write函数发送,多了一次转换;

  • writeTextMessage直接写入需要发送的QString,效率略高。

  1. 异步支持不同:
  • sendTextMessage是同步操作,发送完毕后再返回;

  • writeTextMessage支持异步调用,可以通过Lambda指定回调函数。

  1. 错误处理不同:
  • sendTextMessage不会返回错误信息,只能通过信号错误处理;

  • writeTextMessage可以获取返回的错误码判断发送情况。

  1. 使用场景不同:
  • sendTextMessage专注WebSocket,适合 WebSocket API 的调用方式;

  • writeTextMessage更通用,可用于其他QAbstractSocket子类。

总的来说:

  • sendTextMessage使用更简单,封装良好适合基本用法;

  • writeTextMessage效率略高,支持更多特性如异步和错误处理,适合性能或控制需求较高的场景。

实现项目

客户端与客户端私聊通信,客户端与服务端之间通信。

 核心代码

服务端

流程:创建QWebServer,绑定新连接回调,监听断开。

新连接回调:有新连接 加入集合,给新连接socket绑定离线和接收以及错误的回调。

发送消息按钮:对all和one进行分类处理,遍历set集合,使用sendTextMesg发送

#include "widget.h"
#include "ui_widget.h"
//这个是服务器端
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    /*
     * mode
NonSecureMode: 不安全模式,即不使用SSL/TLS进行通信。这是默认值。
SecureMode: 安全模式,以SSL/TLS安全通信。客户端和服务端之间的连接将使用SSL握手建立安全通道。
AutomaticallyAcceptServerCertificates: 自动接受服务器证书。在SecureMode下,客户端无法验证证书时,自动接受服务器发来的证书以建立连接。
VerifyNone: 不验证证书。以SecureMode运行,但不会验证客户端和服务端使用的证书。
*/
    webServer = new QWebSocketServer("testWebServer",QWebSocketServer::NonSecureMode,this);
    QObject::connect(webServer,&QWebSocketServer::newConnection,this,&Widget::MyselfNewConnectCallBackSlot);

    webServer->listen(QHostAddress::Any,8888);
}
void Widget::MyselfNewConnectCallBackSlot(){//新连接回调

    if(webServer->hasPendingConnections()){
        QWebSocket* websocket=webServer->nextPendingConnection();
        ui->msgtext->append(websocket->origin()+"客户端已连接到服务器");
        sockets<<websocket;

        QListWidgetItem * item =new QListWidgetItem;
        item->setText(websocket->origin());
        ui->clinetls->addItem(item);


        //绑定离开
        QObject::connect(websocket,&QWebSocket::disconnected,this,[websocket,this](){
            ui->msgtext->append(websocket->origin()+"客户端断开服务器连接");
            sockets.removeOne(websocket);
            for(int i=0;i<ui->clinetls->count();i++)
            {
                QListWidgetItem *item=ui->clinetls->item(i);

                if(item->text()==websocket->origin())
                {
                    ui->clinetls->removeItemWidget(item);
                    delete item;
                    break;
                }
            }

            websocket->deleteLater();

        });


        //接受消息回调
        QObject::connect(websocket,&QWebSocket::textMessageReceived,this,[this](const QString &msg){
            QJsonDocument doc =QJsonDocument::fromJson(msg.toLatin1().data());
            if(doc.isNull()){
                QWebSocket* websocket =qobject_cast<QWebSocket*>(sender());//sender 触发信号的源头
                ui->msgtext->append("收到客户端消息["+websocket->origin()+"]--->"+msg);

            }else{
                //客户端之间的单发消息
                QJsonObject obj=doc.object();
                QString dst=doc["dst"].toString();
                for (auto& socket : sockets) {
                    if(dst == socket->origin()){
                        socket->sendTextMessage(msg);
                    }
                }
            }
        });


        QObject::connect(websocket,QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
                         this,[this](QAbstractSocket::SocketError error){
            QWebSocket* web =qobject_cast<QWebSocket*>(sender());
            ui->msgtext->append(web->origin()+"出错"+web->errorString());
        });
    }



}
Widget::~Widget()
{
    delete ui;
    for(auto socket:sockets)
    {
        socket->close();
    }

    webServer->close();
}


void Widget::on_sendpb_clicked()
{
    QString send =ui->sendtext->toPlainText().trimmed();
    if(send.isEmpty())return;
    if(ui->allradio->isChecked()){
        //群发
        if(sockets.size()==0)return;
        foreach(auto &socket,sockets){
            socket->sendTextMessage(send);
        }
        ui->msgtext->append("服务器给所有连接发送:"+send);
    }else{ //私发  取客户端名称 找 发
        if(!ui->clinetls->currentItem())return;
        QString cname =ui->clinetls->currentItem()->text();

        for(auto &socket:sockets)
        {
            if(socket->origin()==cname)
            {
                socket->sendTextMessage(send);
                ui->msgtext->append("服务端给["+socket->origin()+"]发送--->"+send);

                break;
            }
        }
    }


    ui->sendtext->clear();
}

客户端

点击按钮实现websocket连接流程:创建websocket,绑定各种回调,通过open(url)连接

发送消息按钮:对私发和服务器发进行分类,私发封装json格式,然后再发送。

#include "widget.h"
#include "ui_widget.h"
#include<QMessageBox>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    websocket=nullptr;

}

Widget::~Widget()
{
    delete ui;
}


void Widget::on_sendpb_clicked()
{
    if(!websocket && !websocket->isValid())
            return;

    QString send=ui->sendtext->toPlainText().trimmed();
    if(send.isEmpty())return;


    QString cname =ui->onetextmsg->text().trimmed();
    if(cname.isEmpty()){ //发给服务器
        websocket->sendTextMessage(send);
        ui->msgtext->append("发送消息:"+send);
    }else{
        //私聊发
        QJsonObject obj;
        obj["src"]=websocket->origin();
        obj["dst"]=cname;
        obj["msg"]=send;
        //将一个QJsonObject类型的obj转换成JSON字符串。 Compact参数指定格式化方式为紧凑格式(每个元素占一行)。紧凑格式输出结构清晰,容量小,适合传输和存储。
        QString str(QJsonDocument(obj).toJson(QJsonDocument::Compact));
        websocket->sendTextMessage(str);
    }
    ui->sendtext->clear();


}

void Widget::on_connect_clicked()
{

    if(websocket == nullptr){
        if(ui->server_addr->text().trimmed().isEmpty()){
            QMessageBox::critical(this,"错误","服务器名称不能为空,请重新检查!",QMessageBox::Yes);
            return;
        }
        //用于移除字符串开头和结尾处的空白字符。 输入: " test " trimmed()后的结果: "test"  VersionLatest使用最新的websocket协议版本。
        websocket=new QWebSocket(ui->client_name->text().trimmed(),QWebSocketProtocol::VersionLatest,this);

        //连接回调
        QObject::connect(websocket,&QWebSocket::connected,this,[this](){
            ui->msgtext->append("已经连接上"+websocket->peerAddress().toString());
            isConnecting=true;
            ui->connect->setText("断开服务器");
        });
        //断开回调
        QObject::connect(websocket,&QWebSocket::disconnected,this,[this](){
            ui->msgtext->append("已"+websocket->peerAddress().toString()+"断开连接");
            isConnecting=false;
            ui->connect->setText("连接服务器");
        });

        QObject::connect(websocket,QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error)
                         ,this,[this](QAbstractSocket::SocketError){
            ui->msgtext->append(websocket->origin()+"出错"+websocket->errorString());

        });


        //接受消息回调 网上知名bug 当你连续发送 A:A*100 B:200时 接收方:A*100200 通过消息队列方式发送即可
        connect(websocket,&QWebSocket::textMessageReceived
                ,this,[this](const QString &msg){
            QJsonDocument jsd=QJsonDocument::fromJson(msg.toUtf8().data());

            if(jsd.isNull()) //解析失败 即没有 c1:c2 客户端与客户端私聊
            {
                ui->msgtext->append("收到消息:"+msg);
            }
            else
            {
                QJsonObject jsobj=jsd.object();
                ui->msgtext->append("收到来自"+jsobj["src"].toString()+"的消息:"+jsobj["msg"].toString());
            }
        },Qt::QueuedConnection);



    }

    if(isConnecting){ //连接是成功的
        websocket->close();
        websocket->deleteLater();
        websocket=nullptr;

    } else
    {
        websocket->open(QUrl(ui->server_addr->text().trimmed()));
    }
}

总结

总的来说,QWebSocket作为QT网络库中的一个组件,提供了一整套用于开发WebSocket客户端和服务端的便利API。

它的主要优点有:

  1. 完全面向对象的设计,API简单易用。

  2. 与QT网络其他组件高度集成,如SSL/代理支持都很好。

  3. 采用事件驱动模型,不需要开发者处理底层细节如多线程等。

  4. 和Qt GUI应用天然集成,消息与界面更新直接调用即可。

  5. 提供了WebSocket基础规范完整实现,开箱即用方便开发。

  6. 性能也不错,特别是QT5.10后支持了异步I/O调用方式。

  7. 丰富的示例和开源项目可供参考,入门门槛低。

而一些需要注意的点包括:

  1. 不支持一些扩展的websocket协议格式,需要自行实现。

  2. 消息发送和接收的顺序匹配需要自行控制。

  3. 文件与流式大数据传输支持不够友好直接。

  4. 无法改变底层使用的智能指针和内存管理机制。

  5. 对新的C++标准特性支持相对保守一些。

总体来说,对于大多数基于Tcp的WebSocket应用来说,QWebSocket提供了一个非常优秀而成熟的选择。开发效率高, bug少。对QT应用来说也是首选。如果有更高级别需求,可以考虑其他底层实现。但对绝大部分案例,QWebSocket已经足够好用了。

最后附上源代码链接
对您有帮助的话,帮忙点个star

Qt demo: 学习qt过程 (gitee.com)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

社区团购小程序源码系统 带完整的安装代码以及搭建部署教程

系统概述 在这个数字化时代&#xff0c;线上活动成为了连接用户与组织者的桥梁。为了满足不同场景的需要&#xff0c;开发一个灵活、可定制的在线活动报名表单小程序显得尤为重要。本文将深入介绍一个自定义在线活动报名表单小程序的源码系统&#xff0c;并提供详细的搭建部署…

【JavaScript 算法】快速排序:高效的排序算法

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 一、算法原理二、算法实现三、应用场景四、优化与扩展五、总结 快速排序&#xff08;Quick Sort&#xff09;是一种高效的排序算法&#xff0c;通过分治法将数组分为较小的子数组&#xff0c;递归地排序子数组。快速排序通常…

vue使用quill编辑器自定义附件上传方法,并根据上传附件名称生成链接

1、附件上传 需求&#xff1a; 在编辑器中上传word,pdf,excel等附件后&#xff0c;能根据上传附件的名称生成link链接&#xff0c;在展示页面能实现点击链接下载或预览附件&#xff0c;效果图如下: 实现方法&#xff1a; quill编辑器自身带有link&#xff0c;但不满足需求&…

Java---SpringBoot详解二

勤奋勤劳铸梦成&#xff0c; 晨曦微露起长征。 汗水浇灌花似锦&#xff0c; 寒窗苦读岁月明。 千锤百炼心如铁&#xff0c; 万里征途志不倾。 持之以恒终有日&#xff0c; 功成名就笑谈中。 目录 一&#xff0c;统一响应结果 二&#xff0c;三层架构 三&#xff0c;分层解耦 四…

基于html开发的在线网址导航在线工具箱源码

基于html开发的在线网址导航在线工具箱源码&#xff0c;将全部文件复制到服务器&#xff0c;入口文件是index.html 如需修改网址&#xff0c;可修改index.html 如需修改关于页面&#xff0c;可修改about里面的index页面 源码下载&#xff1a;https://download.csdn.net/down…

存储实验:Linux挂载iscsi硬盘与华为OceanStor创建LUN全流程

目录 目的环境规划实验实验流程Centos配置0. 关闭防火墙1. 设置网卡信息2. 配置路由3. iscsiadm连接存储 iSCSI LUN创建&#xff08;以华为OceanStor为例&#xff09;验证1. 验证是否成功2. 开启自动挂载 目的 实现Linux连接iscsi硬盘&#xff0c;同时实现开机自启挂载 环境规…

综合实验作业

node01&#xff1a;192.168.175.146 node02&#xff1a;192.168.175.147 【node01】 node01 与 node02 防火墙在本实验中都需要放行的服务&#xff1b; [rootlocalhost ~]# firewall-cmd --permanent --add-servicedns success [rootlocalhost ~]# firewall-cmd --permanent -…

实变函数精解【3】

文章目录 点集求导集 闭集参考文献 点集 求导集 例1 E { 1 / n 1 / m : n , m ∈ N } 1. lim ⁡ n → ∞ ( 1 / n 1 / m ) 1 / m 2. lim ⁡ n , m → ∞ ( 1 / n 1 / m ) 0 3. E ′ { 0 , 1 , 1 / 2 , 1 / 3 , . . . . } E\{1/n1/m:n,m \in N\} \\1.\lim_{n \rightar…

PGCCC|【PostgreSQL】PCA认证考试大纲#postgresql认证

PostgreSQL Certified Associate|PCA&#xff08;初级&#xff09; 学员将学会安装、创建和维护PostgreSQL数据库。学完后&#xff0c;学员可以从事PostgreSQL数据库的数据操作和管理等工作。 获证途径 参加PostgreSQL培训再考试 考试为上机考试。 PostgreSQL PCA培训考试课…

“金山-讯飞”杯2024年武汉理工大学程序设计竞赛 A. Mobiusp败走***(思维题-点双连通分量、连通性)

题目 思路来源 官方题解 题解 手玩发现&#xff0c;能换的话&#xff0c;当且仅当.和1在一个环里&#xff0c;而这就是点双连通分量 所以最优策略是先把.换到(x,y)的位置&#xff0c;然后判断.和1在不在一个环里 也就是&#xff1a; 1. 判断删掉1时&#xff0c;.和(x,y)联…

VSCode上通过C++实现单例模式

单例模式实际上就是为了确保一个类最多只有一个实例&#xff0c;并且在程序的任何地方都可以访问这个实例&#xff0c;也就是提供一个全局访问点&#xff0c;单例对象不需要手动释放&#xff0c;交给系统来释放就可以了&#xff0c;单例模式的设计初衷就是为了在整个应用程序的…

基于扩散的生物打印策略,控制可打印性和结构特性

基于扩散的生物打印策略&#xff0c;控制可打印性和结构特性 在生物打印中&#xff0c;将生物材料和细胞按特定设计逐层堆积&#xff0c;构建具有复杂结构和功能的三维组织结构。微挤出生物打印是最常用的方法&#xff0c;其核心是生物墨水&#xff0c;它由聚合物材料和细胞组…

Go语言入门之Map详解

Go语言入门之Map详解 1.基础定义 map是一个无序的&#xff0c;k-v键值对格式的集合 &#xff08;1&#xff09;特点 类型特点&#xff1a;map为引用类型&#xff0c;所以在函数中更新value值会永久改变顺序特点&#xff1a;map的遍历是无序的&#xff0c;因为底层是哈希表&am…

[Linux][Shell][Shell逻辑控制]详细讲解

目录 1.if 判断1.if-then2.if-then-else3.elif4.case5.实际上手 2.条件测试0.事前说明1.test 命令2.[]3.双括号1.(())2.[[]] 4.实际上手 3.循环1.for2.while3.until命令4.控制循环1.break2.continue 5.处理循环的输出 1.if 判断 1.if-then 语法&#xff1a;if command thenco…

ARM功耗管理标准接口之SCMI

安全之安全(security)博客目录导读 思考&#xff1a;功耗管理有哪些标准接口&#xff1f;ACPI&PSCI&SCMI&#xff1f; Advanced Configuration and Power Interface Power State Coordination Interface System Control and Management Interface 下图示例说明了实现…

MongoDB教程(一):Linux系统安装mongoDB详细教程

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; 文章目录 引言一、Ubuntu…

昇思25天学习打卡营第23天|基于MindSpore通过GPT实现情感分类

1. 学习内容复盘 %%capture captured_output # 实验环境已经预装了mindspore2.2.14&#xff0c;如需更换mindspore版本&#xff0c;可更改下面mindspore的版本号 !pip uninstall mindspore -y !pip install -i https://pypi.mirrors.ustc.edu.cn/simple mindspore2.2.14 I…

一文入门【NestJs】Modules

&#x1f6a9;引言 在探索NestJS的生态系统时&#xff0c;理解模块&#xff08;Modules&#xff09;的概念是至关重要的第一步。NestJS&#xff0c;作为一个基于Node.js的现代框架&#xff0c;借鉴了Angular的模块化设计思路&#xff0c;为开发者提供了一种优雅的方式来组织和…

jenkins打包java项目报错Error: Unable to access jarfile tlm-admin.jar

jenkins打包boot项目 自动重启脚本失败 查看了一下项目日志报错&#xff1a; Error: Unable to access jarfile tlm-admin.jar我检查了一下这个配置&#xff0c;感觉没有问题&#xff0c;包可以正常打&#xff0c; cd 到项目目录下面&#xff0c;手动执行这个sh脚本也是能正常…

vue中el-table单元格复制功能

一、单页面中使用 1.在el-table上绑定单击事件 cell-click“copyText” 或双击事件 cell-dblclick“copyText” 注&#xff1a;cell-dblclick函数有四个参数&#xff0c;分别是row, column, cell, event&#xff1b; row&#xff1a;可看到被其操作单元格所在行的所有的数据&…