学习目标:Qt QWebSocket网络编程
学习前置环境
QT TCP多线程网络通信-CSDN博客
学习内容
WebSocket是一种通过单个TCP连接提供全双工通信信道的网络技术。2011年,IETF将WebSocket协议标准化为 RFC6455,QWebSocket可用于客户端应用程序和服务器应用程序。
它实现了浏览器与服务器全双工(full-duplex)通信,允许服务器主动发送信息到客户端。
主要特点:
-
与HTTP不同,WebSocket允许服务器主动发送数据给客户端,不需要客户端发起请求。
-
建立在TCP协议之上,服务器和客户端之间通过Ws(WebSocket)协议在单个端口上进行全双工通信。
-
支持以文本方式或者二进制方式传输数据。
-
协议建立在单个TCP连接上,服务器和客户端只需创建一个连接,且连接不会被关闭。
-
支持多种编程语言的客户端和服务器端库,如JavaScript,Java,C#,Python等。
常见应用场景:
-
聊天室:支持低延迟的实时对话。
-
在线game:需要实时同步游戏状态的同步引擎。
-
股票行情:需要推送即时行情给客户端的行情软件。
-
视频会议:需要语音和视频的低延迟实时通信。
-
实时协作编辑:如在线代码编辑器要求实时同步。
QWebSocket是Qt提供的WebSocket功能库。它建立在Qt网络模块之上,实现了RFC6455标准中的WebSocket协议。需要再Qmake文件中加入 QT+=websockets
-
QWebSocket只支持Text/Binary两种消息格式,不支持其他扩展格式。如果需要补充其他自定义协议,需要开发者在应用层自己处理。
-
它支持使用标准的http/https端口80/443访问websocket服务,也支持wss(加密websocket)协议。所以可以很方便地与现有的web服务器交互。
-
由于它基于QT CP套接字实现,完全支持所有Qt网络功能,比如代理设置、SSL配置等。这一点相比一些底层的C接口更易用。
-
它同时支持主动和被动连接模式。主动连接通过connectToHost(),被动通过监听端口accept()接受新的链接。这两种模式都很方便。
-
对于QT GUI应用,可以很方便地进行消息接收与界面更新,避免了多线程编程的复杂性。比如直接在textMessageReceived()里更新界面就行了。
-
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这两个函数都可以用来发送文本消息,但它们有一些区别:
- 函数定义不同:
-
sendTextMessage属于QWebSocket的成员函数;
-
writeTextMessage是QAbstractSocket的成员函数,QWebSocket继承于QAbstractSocket。
- 发送效率不同:
-
sendTextMessage内部会将消息先转成QByteArray,再通过write函数发送,多了一次转换;
-
writeTextMessage直接写入需要发送的QString,效率略高。
- 异步支持不同:
-
sendTextMessage是同步操作,发送完毕后再返回;
-
writeTextMessage支持异步调用,可以通过Lambda指定回调函数。
- 错误处理不同:
-
sendTextMessage不会返回错误信息,只能通过信号错误处理;
-
writeTextMessage可以获取返回的错误码判断发送情况。
- 使用场景不同:
-
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。
它的主要优点有:
-
完全面向对象的设计,API简单易用。
-
与QT网络其他组件高度集成,如SSL/代理支持都很好。
-
采用事件驱动模型,不需要开发者处理底层细节如多线程等。
-
和Qt GUI应用天然集成,消息与界面更新直接调用即可。
-
提供了WebSocket基础规范完整实现,开箱即用方便开发。
-
性能也不错,特别是QT5.10后支持了异步I/O调用方式。
-
丰富的示例和开源项目可供参考,入门门槛低。
而一些需要注意的点包括:
-
不支持一些扩展的websocket协议格式,需要自行实现。
-
消息发送和接收的顺序匹配需要自行控制。
-
文件与流式大数据传输支持不够友好直接。
-
无法改变底层使用的智能指针和内存管理机制。
-
对新的C++标准特性支持相对保守一些。
总体来说,对于大多数基于Tcp的WebSocket应用来说,QWebSocket提供了一个非常优秀而成熟的选择。开发效率高, bug少。对QT应用来说也是首选。如果有更高级别需求,可以考虑其他底层实现。但对绝大部分案例,QWebSocket已经足够好用了。
最后附上源代码链接
对您有帮助的话,帮忙点个star
Qt demo: 学习qt过程 (gitee.com)