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常用成员函数

cpp 复制代码
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发送

cpp 复制代码
#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格式,然后再发送。

cpp 复制代码
#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)

相关推荐
麻瓜也要学魔法2 小时前
链路状态路由协议-OSPF
网络
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
傻啦嘿哟3 小时前
代理IP在后端开发中的应用与后端工程师的角色
网络·网络协议·tcp/ip
Red Red3 小时前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
亚远景aspice5 小时前
ISO 21434标准:汽车网络安全管理的利与弊
网络·web安全·汽车
Estar.Lee5 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
友友马5 小时前
『 Linux 』网络层 - IP协议(一)
linux·网络·tcp/ip
码老白6 小时前
【老白学 Java】Warshipv2.0(二)
java·网络
HackKong7 小时前
小白怎样入门网络安全?
网络·学习·安全·web安全·网络安全·黑客