学习目标:Qt QWebSocket网络编程
学习前置环境
学习内容
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常用成员函数
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这两个函数都可以用来发送文本消息,但它们有一些区别:
- 函数定义不同:
-
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发送
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。
它的主要优点有:
-
完全面向对象的设计,API简单易用。
-
与QT网络其他组件高度集成,如SSL/代理支持都很好。
-
采用事件驱动模型,不需要开发者处理底层细节如多线程等。
-
和Qt GUI应用天然集成,消息与界面更新直接调用即可。
-
提供了WebSocket基础规范完整实现,开箱即用方便开发。
-
性能也不错,特别是QT5.10后支持了异步I/O调用方式。
-
丰富的示例和开源项目可供参考,入门门槛低。
而一些需要注意的点包括:
-
不支持一些扩展的websocket协议格式,需要自行实现。
-
消息发送和接收的顺序匹配需要自行控制。
-
文件与流式大数据传输支持不够友好直接。
-
无法改变底层使用的智能指针和内存管理机制。
-
对新的C++标准特性支持相对保守一些。
总体来说,对于大多数基于Tcp的WebSocket应用来说,QWebSocket提供了一个非常优秀而成熟的选择。开发效率高, bug少。对QT应用来说也是首选。如果有更高级别需求,可以考虑其他底层实现。但对绝大部分案例,QWebSocket已经足够好用了。
最后附上源代码链接
对您有帮助的话,帮忙点个star