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

    1. origin()
    2. 即 websocket=new QWebSocket("C1我是客户端",QWebSocketProtocol::VersionLatest,this);
    3. websocket->origin()  -》 C1我是客户端
    4. void connectToHost(const QUrl &url) - 用于连接到指定主机的websocket服务,这个函数是异步的。
    5. void close() - 关闭与服务器的连接。
    6. void textMessageReceived(const QString &message) - 收到文本消息时触发的信号,其参数就是收到的文本消息内容。
    7. void binaryMessageReceived(const QByteArray &message) - 收到二进制消息时触发的信号,参数是原始二进制数据。
    8. void error(QAbstractSocket::SocketError socketError) - 发生错误时触发的信号,参数是错误类型。
    9. void stateChanged(QAbstractSocket::SocketState state) - 连接状态变化时触发,可以得知连接是否建立等。
    10. void textMessageSent(qint64 numBytes) - 发送文本消息完成后触发,numBytes是字节数。
    11. void bytesWritten(qint64 bytes) - 消息发送过程中的写入回调, bytes是一个部分发送出去的字节数。
    12. void abort() - 主动断开连接。
    13. bool waitForConnected(int msec = 30000) - 阻塞等待连接建立成功。
    14. QString hostName() - 获取当前连接的主机名,常用于判断连接是否成功。
    15. quint16 port() - 获取主机端口号。
    16. bool openMode() - 判断当前是否为主动连接还是被动接受模式。
    17. void writeTextMessage(const QString &text) - 发送文本消息,相比textMessage等更直观。
    18. void writeMessage(const QByteArray &data) - 发送二进制数据。
    19. qint64 bytesAvailable() - 查看接收缓存中可读取字节数。
    20. qint64 readBufferSize() - 设置双向数据接收缓存大小。
    21. void pauseIncomingPayload() - 暂停接收消息流。
    22. void resumeIncomingPayload() - 恢复接收。
    23. bool isValid() - 检查连接是否有效。
    24. 另外,作为QT套接字,它还支持一些通用功能:
    25. void setProxy() - 设置代理。
    26. void encrypt() - 设置SSL安全连接。
    27. void flush() - 强制输出缓存写出。
    28. bool waitForBytesWritten() - 等待数据发送完毕。
    29. void QWebSocket::sendTextMessage(const QString &message) 用于发送文本消息
    30. 使用这个函数发送文本消息主要有以下几点需要注意的地方:
    31. 1发送文本消息前请确保WebSocket连接已经建立。可以通过ReadyState判断连接状态。
    32. 2发送的消息内容必须是纯文本,不支持转义编码等更多格式。
    33. 3一条消息发送完毕后,会触发textMessageSent()信号通知。
    34. 4可以通过waitForBytesWritten()等待数据完全发送出去。
    35. 5发送数据顺序可能与收到响应顺序不一致,需要应用层自己处理序号等。
    36. 6若消息较大,建议使用write或send到套接字后flush,而不是sendTextMessage。
    37. 7跨平台考虑,消息内容编码最好使用QString而不是QByteArray。
    38. 8使用该函数发送的文本消息类型,服务端一般对应文本框接受。
    39. 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发送

    1. #include "widget.h"
    2. #include "ui_widget.h"
    3. //这个是服务器端
    4. Widget::Widget(QWidget *parent)
    5. : QWidget(parent)
    6. , ui(new Ui::Widget)
    7. {
    8. ui->setupUi(this);
    9. /*
    10. * mode
    11. NonSecureMode: 不安全模式,即不使用SSL/TLS进行通信。这是默认值。
    12. SecureMode: 安全模式,以SSL/TLS安全通信。客户端和服务端之间的连接将使用SSL握手建立安全通道。
    13. AutomaticallyAcceptServerCertificates: 自动接受服务器证书。在SecureMode下,客户端无法验证证书时,自动接受服务器发来的证书以建立连接。
    14. VerifyNone: 不验证证书。以SecureMode运行,但不会验证客户端和服务端使用的证书。
    15. */
    16. webServer = new QWebSocketServer("testWebServer",QWebSocketServer::NonSecureMode,this);
    17. QObject::connect(webServer,&QWebSocketServer::newConnection,this,&Widget::MyselfNewConnectCallBackSlot);
    18. webServer->listen(QHostAddress::Any,8888);
    19. }
    20. void Widget::MyselfNewConnectCallBackSlot(){//新连接回调
    21. if(webServer->hasPendingConnections()){
    22. QWebSocket* websocket=webServer->nextPendingConnection();
    23. ui->msgtext->append(websocket->origin()+"客户端已连接到服务器");
    24. sockets<
    25. QListWidgetItem * item =new QListWidgetItem;
    26. item->setText(websocket->origin());
    27. ui->clinetls->addItem(item);
    28. //绑定离开
    29. QObject::connect(websocket,&QWebSocket::disconnected,this,[websocket,this](){
    30. ui->msgtext->append(websocket->origin()+"客户端断开服务器连接");
    31. sockets.removeOne(websocket);
    32. for(int i=0;iclinetls->count();i++)
    33. {
    34. QListWidgetItem *item=ui->clinetls->item(i);
    35. if(item->text()==websocket->origin())
    36. {
    37. ui->clinetls->removeItemWidget(item);
    38. delete item;
    39. break;
    40. }
    41. }
    42. websocket->deleteLater();
    43. });
    44. //接受消息回调
    45. QObject::connect(websocket,&QWebSocket::textMessageReceived,this,[this](const QString &msg){
    46. QJsonDocument doc =QJsonDocument::fromJson(msg.toLatin1().data());
    47. if(doc.isNull()){
    48. QWebSocket* websocket =qobject_cast(sender());//sender 触发信号的源头
    49. ui->msgtext->append("收到客户端消息["+websocket->origin()+"]--->"+msg);
    50. }else{
    51. //客户端之间的单发消息
    52. QJsonObject obj=doc.object();
    53. QString dst=doc["dst"].toString();
    54. for (auto& socket : sockets) {
    55. if(dst == socket->origin()){
    56. socket->sendTextMessage(msg);
    57. }
    58. }
    59. }
    60. });
    61. QObject::connect(websocket,QOverload::of(&QWebSocket::error),
    62. this,[this](QAbstractSocket::SocketError error){
    63. QWebSocket* web =qobject_cast(sender());
    64. ui->msgtext->append(web->origin()+"出错"+web->errorString());
    65. });
    66. }
    67. }
    68. Widget::~Widget()
    69. {
    70. delete ui;
    71. for(auto socket:sockets)
    72. {
    73. socket->close();
    74. }
    75. webServer->close();
    76. }
    77. void Widget::on_sendpb_clicked()
    78. {
    79. QString send =ui->sendtext->toPlainText().trimmed();
    80. if(send.isEmpty())return;
    81. if(ui->allradio->isChecked()){
    82. //群发
    83. if(sockets.size()==0)return;
    84. foreach(auto &socket,sockets){
    85. socket->sendTextMessage(send);
    86. }
    87. ui->msgtext->append("服务器给所有连接发送:"+send);
    88. }else{ //私发 取客户端名称 找 发
    89. if(!ui->clinetls->currentItem())return;
    90. QString cname =ui->clinetls->currentItem()->text();
    91. for(auto &socket:sockets)
    92. {
    93. if(socket->origin()==cname)
    94. {
    95. socket->sendTextMessage(send);
    96. ui->msgtext->append("服务端给["+socket->origin()+"]发送--->"+send);
    97. break;
    98. }
    99. }
    100. }
    101. ui->sendtext->clear();
    102. }

    客户端

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

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

    1. #include "widget.h"
    2. #include "ui_widget.h"
    3. #include
    4. Widget::Widget(QWidget *parent)
    5. : QWidget(parent)
    6. , ui(new Ui::Widget)
    7. {
    8. ui->setupUi(this);
    9. websocket=nullptr;
    10. }
    11. Widget::~Widget()
    12. {
    13. delete ui;
    14. }
    15. void Widget::on_sendpb_clicked()
    16. {
    17. if(!websocket && !websocket->isValid())
    18. return;
    19. QString send=ui->sendtext->toPlainText().trimmed();
    20. if(send.isEmpty())return;
    21. QString cname =ui->onetextmsg->text().trimmed();
    22. if(cname.isEmpty()){ //发给服务器
    23. websocket->sendTextMessage(send);
    24. ui->msgtext->append("发送消息:"+send);
    25. }else{
    26. //私聊发
    27. QJsonObject obj;
    28. obj["src"]=websocket->origin();
    29. obj["dst"]=cname;
    30. obj["msg"]=send;
    31. //将一个QJsonObject类型的obj转换成JSON字符串。 Compact参数指定格式化方式为紧凑格式(每个元素占一行)。紧凑格式输出结构清晰,容量小,适合传输和存储。
    32. QString str(QJsonDocument(obj).toJson(QJsonDocument::Compact));
    33. websocket->sendTextMessage(str);
    34. }
    35. ui->sendtext->clear();
    36. }
    37. void Widget::on_connect_clicked()
    38. {
    39. if(websocket == nullptr){
    40. if(ui->server_addr->text().trimmed().isEmpty()){
    41. QMessageBox::critical(this,"错误","服务器名称不能为空,请重新检查!",QMessageBox::Yes);
    42. return;
    43. }
    44. //用于移除字符串开头和结尾处的空白字符。 输入: " test " trimmed()后的结果: "test" VersionLatest使用最新的websocket协议版本。
    45. websocket=new QWebSocket(ui->client_name->text().trimmed(),QWebSocketProtocol::VersionLatest,this);
    46. //连接回调
    47. QObject::connect(websocket,&QWebSocket::connected,this,[this](){
    48. ui->msgtext->append("已经连接上"+websocket->peerAddress().toString());
    49. isConnecting=true;
    50. ui->connect->setText("断开服务器");
    51. });
    52. //断开回调
    53. QObject::connect(websocket,&QWebSocket::disconnected,this,[this](){
    54. ui->msgtext->append("已"+websocket->peerAddress().toString()+"断开连接");
    55. isConnecting=false;
    56. ui->connect->setText("连接服务器");
    57. });
    58. QObject::connect(websocket,QOverload::of(&QWebSocket::error)
    59. ,this,[this](QAbstractSocket::SocketError){
    60. ui->msgtext->append(websocket->origin()+"出错"+websocket->errorString());
    61. });
    62. //接受消息回调 网上知名bug 当你连续发送 A:A*100 B:200时 接收方:A*100200 通过消息队列方式发送即可
    63. connect(websocket,&QWebSocket::textMessageReceived
    64. ,this,[this](const QString &msg){
    65. QJsonDocument jsd=QJsonDocument::fromJson(msg.toUtf8().data());
    66. if(jsd.isNull()) //解析失败 即没有 c1:c2 客户端与客户端私聊
    67. {
    68. ui->msgtext->append("收到消息:"+msg);
    69. }
    70. else
    71. {
    72. QJsonObject jsobj=jsd.object();
    73. ui->msgtext->append("收到来自"+jsobj["src"].toString()+"的消息:"+jsobj["msg"].toString());
    74. }
    75. },Qt::QueuedConnection);
    76. }
    77. if(isConnecting){ //连接是成功的
    78. websocket->close();
    79. websocket->deleteLater();
    80. websocket=nullptr;
    81. } else
    82. {
    83. websocket->open(QUrl(ui->server_addr->text().trimmed()));
    84. }
    85. }

    总结

    总的来说,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)

  • 相关阅读:
    网络原理——No.4 传输层_TCP协议中的延迟应答, 捎带应答, 面向字节流与TCP的异常处理
    生命周期成本VE——五十铃汽车
    【jvm】《尚硅谷宋红康JVM全套教程(详解java虚拟机)》上篇 笔记
    C语言结构体中成员的引用
    国产ETL工具BeeDI 产品 之 全国连锁到集团总部 数据同步
    18-移动端等比例缩放rem
    震动检测模块使用介绍
    部署LVS-DR群集
    2.1.6.15 漏洞利用-smb-RCE远程命令执行
    (转)什么是数据匿名化
  • 原文地址:https://blog.csdn.net/weixin_50873490/article/details/140332606