C++ 全栈聊天项目(15) 实现自定义可点击Label以及定时按钮

客户端TCP管理者

因为聊天服务要维持一个长链接,方便服务器和客户端双向通信,那么就需要一个TCPMgr来管理TCP连接。

而实际开发中网络模块一般以单例模式使用,那我们就基于单例基类和可被分享类创建一个自定义的TcpMgr类,在QT工程中新建TcpMgr类,会生成头文件和源文件,头文件修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef TCPMGR_H
#define TCPMGR_H
#include <QTcpSocket>
#include "singleton.h"
#include "global.h"
class TcpMgr:public QObject, public Singleton<TcpMgr>,
public std::enable_shared_from_this<TcpMgr>
{
Q_OBJECT
public:
TcpMgr();
private:
QTcpSocket _socket;
QString _host;
uint16_t _port;
QByteArray _buffer;
bool _b_recv_pending;
quint16 _message_id;
quint16 _message_len;
public slots:
void slot_tcp_connect(ServerInfo);
void slot_send_data(ReqId reqId, QString data);
signals:
void sig_con_success(bool bsuccess);
void sig_send_data(ReqId reqId, QString data);
};

#endif // TCPMGR_H

接下来我们在构造函数中连接网络请求的各种信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
TcpMgr::TcpMgr():_host(""),_port(0),_b_recv_pending(false),_message_id(0),_message_len(0)
{
QObject::connect(&_socket, &QTcpSocket::connected, [&]() {
qDebug() << "Connected to server!";
// 连接建立后发送消息
emit sig_con_success(true);
});

QObject::connect(&_socket, &QTcpSocket::readyRead, [&]() {
// 当有数据可读时,读取所有数据
// 读取所有数据并追加到缓冲区
_buffer.append(_socket.readAll());

QDataStream stream(&_buffer, QIODevice::ReadOnly);
stream.setVersion(QDataStream::Qt_5_0);

forever {
//先解析头部
if(!_b_recv_pending){
// 检查缓冲区中的数据是否足够解析出一个消息头(消息ID + 消息长度)
if (_buffer.size() < static_cast<int>(sizeof(quint16) * 2)) {
return; // 数据不够,等待更多数据
}

// 预读取消息ID和消息长度,但不从缓冲区中移除
stream >> _message_id >> _message_len;

//将buffer 中的前四个字节移除
_buffer = _buffer.mid(sizeof(quint16) * 2);

// 输出读取的数据
qDebug() << "Message ID:" << _message_id << ", Length:" << _message_len;

}

//buffer剩余长读是否满足消息体长度,不满足则退出继续等待接受
if(_buffer.size() < _message_len){
_b_recv_pending = true;
return;
}

_b_recv_pending = false;
// 读取消息体
QByteArray messageBody = _buffer.mid(0, _message_len);
qDebug() << "receive body msg is " << messageBody ;

_buffer = _buffer.mid(_message_len);
}

});

//5.15 之后版本
// QObject::connect(&_socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred), [&](QAbstractSocket::SocketError socketError) {
// Q_UNUSED(socketError)
// qDebug() << "Error:" << _socket.errorString();
// });

// 处理错误(适用于Qt 5.15之前的版本)
QObject::connect(&_socket, static_cast<void (QTcpSocket::*)(QTcpSocket::SocketError)>(&QTcpSocket::error),
[&](QTcpSocket::SocketError socketError) {
qDebug() << "Error:" << _socket.errorString() ;
switch (socketError) {
case QTcpSocket::ConnectionRefusedError:
qDebug() << "Connection Refused!";
emit sig_con_success(false);
break;
case QTcpSocket::RemoteHostClosedError:
qDebug() << "Remote Host Closed Connection!";
break;
case QTcpSocket::HostNotFoundError:
qDebug() << "Host Not Found!";
emit sig_con_success(false);
break;
case QTcpSocket::SocketTimeoutError:
qDebug() << "Connection Timeout!";
emit sig_con_success(false);
break;
case QTcpSocket::NetworkError:
qDebug() << "Network Error!";
break;
default:
qDebug() << "Other Error!";
break;
}
});

// 处理连接断开
QObject::connect(&_socket, &QTcpSocket::disconnected, [&]() {
qDebug() << "Disconnected from server.";
});

QObject::connect(this, &TcpMgr::sig_send_data, this, &TcpMgr::slot_send_data);
}

连接对端服务器

1
2
3
4
5
6
7
8
9
void TcpMgr::slot_tcp_connect(ServerInfo si)
{
qDebug()<< "receive tcp connect signal";
// 尝试连接到服务器
qDebug() << "Connecting to server...";
_host = si.Host;
_port = static_cast<uint16_t>(si.Port.toUInt());
_socket.connectToHost(si.Host, _port);
}

因为客户端发送数据可能在任何线程,为了保证线程安全,我们在要发送数据时发送TcpMgr的sig_send_data信号,然后实现接受这个信号的槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void TcpMgr::slot_send_data(ReqId reqId, QString data)
{
uint16_t id = reqId;

// 将字符串转换为UTF-8编码的字节数组
QByteArray dataBytes = data.toUtf8();

// 计算长度(使用网络字节序转换)
quint16 len = static_cast<quint16>(data.size());

// 创建一个QByteArray用于存储要发送的所有数据
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);

// 设置数据流使用网络字节序
out.setByteOrder(QDataStream::BigEndian);

// 写入ID和长度
out << id << len;

// 添加字符串数据
block.append(data);

// 发送数据
_socket.write(block);
}

然后修改LoginDialog中的initHandlers中的收到服务器登陆回复后的逻辑,这里发送信号准备发起长链接到聊天服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void LoginDialog::initHttpHandlers()
{
//注册获取登录回包逻辑
_handlers.insert(ReqId::ID_LOGIN_USER, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
enableBtn(true);
return;
}
auto user = jsonObj["user"].toString();

//发送信号通知tcpMgr发送长链接
ServerInfo si;
si.Uid = jsonObj["uid"].toInt();
si.Host = jsonObj["host"].toString();
si.Port = jsonObj["port"].toString();
si.Token = jsonObj["token"].toString();

_uid = si.Uid;
_token = si.Token;
qDebug()<< "user is " << user << " uid is " << si.Uid <<" host is "
<< si.Host << " Port is " << si.Port << " Token is " << si.Token;
emit sig_connect_tcp(si);
});
}

在LoginDialog构造函数中连接信号,包括建立tcp连接,以及收到TcpMgr连接成功或者失败的信号处理

1
2
3
4
//连接tcp连接请求的信号和槽函数
connect(this, &LoginDialog::sig_connect_tcp, TcpMgr::GetInstance().get(), &TcpMgr::slot_tcp_connect);
//连接tcp管理者发出的连接成功信号
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_con_success, this, &LoginDialog::slot_tcp_con_finish);

LoginDialog收到连接结果的槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void LoginDialog::slot_tcp_con_finish(bool bsuccess)
{

if(bsuccess){
showTip(tr("聊天服务连接成功,正在登录..."),true);
QJsonObject jsonObj;
jsonObj["uid"] = _uid;
jsonObj["token"] = _token;

QJsonDocument doc(jsonObj);
QString jsonString = doc.toJson(QJsonDocument::Indented);

//发送tcp请求给chat server
TcpMgr::GetInstance()->sig_send_data(ReqId::ID_CHAT_LOGIN, jsonString);

}else{
showTip(tr("网络异常"),false);
enableBtn(true);
}

}

在这个槽函数中我们发送了sig_send_data信号并且通知TcpMgr将数据发送给服务器。

这样TcpMgr发送完数据收到服务器的回复后就可以进一步根据解析出来的信息处理不同的情况了。我们先到此为止。具体如何处理后续再讲。