Technology Sharing

Qt QWebSocket Network Programming

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Learning objectives: Qt QWebSocket network programming

Pre-learning environment

QT TCP multi-threaded network communication -CSDN blog

Learning Content

WebSocket is a network technology that provides a full-duplex communication channel over a single TCP connection. In 2011, IETF standardized the WebSocket protocol as RFC6455, and QWebSocket can be used in both client applications and server applications.

It implements full-duplex communication between the browser and the server, allowing the server to actively send information to the client.

main feature:

  1. Unlike HTTP, WebSocket allows the server to actively send data to the client without the client initiating a request.

  2. Built on the TCP protocol, the server and the client communicate in full-duplex on a single port through the Ws (WebSocket) protocol.

  3. Supports data transmission in text or binary mode.

  4. The protocol is established on a single TCP connection. The server and client only need to create one connection, and the connection will not be closed.

  5. Supports client and server libraries in multiple programming languages, such as JavaScript, Java, C#, Python, etc.

Common application scenarios:

  1. Chat Room: Supports real-time conversations with low latency.

  2. Online game: A synchronization engine is needed to synchronize the game state in real time.

  3. Stock quotes: Quotes software that needs to push real-time quotes to the client.

  4. Video conferencing: Low-latency real-time communications requiring voice and video.

  5. Real-time collaborative editing: such as online code editors require real-time synchronization.

QWebSocket is a WebSocket library provided by Qt. It is built on the Qt network module and implements the WebSocket protocol in the RFC6455 standard. You need to add QT+=websockets to the Qmake file

  1. QWebSocket only supports two message formats: Text and Binary, and does not support other extended formats. If you need to add other custom protocols, developers need to handle them themselves at the application layer.

  2. It supports accessing websocket services using standard http/https ports 80/443, and also supports wss (encrypted websocket) protocol, so it can easily interact with existing web servers.

  3. Since it is based on QT CP socket implementation, it fully supports all Qt network functions, such as proxy settings, SSL configuration, etc. This is easier to use than some underlying C interfaces.

  4. It supports both active and passive connection modes. Active connection is done through connectToHost(), and passive connection is done through listening port accept() to accept new connections. Both modes are very convenient.

  5. For QT GUI applications, it is very convenient to receive messages and update the interface, avoiding the complexity of multi-threaded programming. For example, you can directly update the interface in textMessageReceived().

  6. QT5.10 supports asynchronous I/O, and the performance is slightly improved compared to before. The support for network delay is also better.

QWebSocket common member functions

  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可以绑定消息发送断开连接的异常处理等。

Both sendTextMessage and writeTextMessage functions can be used to send text messages, but they have some differences:

  1. The function definition is different:
  • sendTextMessage is a member function of QWebSocket;

  • writeTextMessage is a member function of QAbstractSocket, and QWebSocket inherits from QAbstractSocket.

  1. Different sending efficiency:
  • SendTextMessage will convert the message into QByteArray first, and then send it through the write function, which requires one more conversion;

  • writeTextMessage directly writes the QString that needs to be sent, which is slightly more efficient.

  1. Asynchronous support is different:
  • sendTextMessage is a synchronous operation and returns after sending;

  • writeTextMessage supports asynchronous calls and can specify callback functions through Lambda.

  1. Error handling is different:
  • sendTextMessage will not return error information, only signal error handling;

  • writeTextMessage can get the returned error code to determine the sending status.

  1. Different usage scenarios:
  • sendTextMessage focuses on WebSocket and is suitable for calling the WebSocket API;

  • writeTextMessage is more general and can be used in other QAbstractSocket subclasses.

In general:

  • sendTextMessage is simpler to use and well encapsulated for basic usage;

  • writeTextMessage is slightly more efficient and supports more features such as asynchrony and error handling, making it suitable for scenarios with higher performance or control requirements.

Realization Project

Clients communicate privately with each other, and clients communicate with servers.

Core code

Server

Process: Create QWebServer, bind new connection callback, and listen for disconnection.

New connection callback: When a new connection is added to the collection, the new connection socket is bound to offline, receive, and error callbacks.

Send message button: classify all and one, traverse the set collection, and send it using 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<<websocket;
  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;i<ui->clinetls->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<QWebSocket*>(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<QAbstractSocket::SocketError>::of(&QWebSocket::error),
  62. this,[this](QAbstractSocket::SocketError error){
  63. QWebSocket* web =qobject_cast<QWebSocket*>(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. }

Client

Click the button to implement the websocket connection process: create a websocket, bind various callbacks, and connect through open(url)

Send message button: classify private messages and server messages, encapsulate private messages in json format, and then send them.

  1. #include "widget.h"
  2. #include "ui_widget.h"
  3. #include<QMessageBox>
  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<QAbstractSocket::SocketError>::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. }

Summarize

In general, QWebSocket, as a component in the QT network library, provides a complete set of convenient APIs for developing WebSocket clients and servers.

Its main advantages are:

  1. Fully object-oriented design, the API is simple and easy to use.

  2. It is highly integrated with other QT network components, such as SSL/proxy support.

  3. Using an event-driven model, developers do not need to deal with underlying details such as multithreading.

  4. Naturally integrated with Qt GUI applications, messages and interface updates can be directly called.

  5. It provides a complete implementation of the WebSocket basic specification, which is ready for development.

  6. The performance is also good, especially after QT5.10, which supports asynchronous I/O calls.

  7. There are abundant examples and open source projects for reference, and the entry threshold is low.

Some points to note include:

  1. Some extended websocket protocol formats are not supported and need to be implemented by yourself.

  2. The order of message sending and receiving needs to be controlled by yourself.

  3. The support for file and streaming large data transfer is not friendly and direct enough.

  4. The underlying smart pointer and memory management mechanisms cannot be changed.

  5. Support for new C++ standard features is relatively conservative.

In general, for most TCP-based WebSocket applications, QWebSocket provides a very good and mature choice. It has high development efficiency and few bugs. It is also the first choice for QT applications. If there are higher-level requirements, other underlying implementations can be considered. But for most cases, QWebSocket is good enough.

Finally, attach the source code link
If it helps you, please give me a star

Qt demo: learning qt process (gitee.com)