聊天项目(15) 客户端实现TCP管理者

客户端TCP管理者

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

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

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

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

  1. TcpMgr::TcpMgr():_host(""),_port(0),_b_recv_pending(false),_message_id(0),_message_len(0)
  2. {
  3. QObject::connect(&_socket, &QTcpSocket::connected, [&]() {
  4. qDebug() << "Connected to server!";
  5. // 连接建立后发送消息
  6. emit sig_con_success(true);
  7. });
  8. QObject::connect(&_socket, &QTcpSocket::readyRead, [&]() {
  9. // 当有数据可读时,读取所有数据
  10. // 读取所有数据并追加到缓冲区
  11. _buffer.append(_socket.readAll());
  12. QDataStream stream(&_buffer, QIODevice::ReadOnly);
  13. stream.setVersion(QDataStream::Qt_5_0);
  14. forever {
  15. //先解析头部
  16. if(!_b_recv_pending){
  17. // 检查缓冲区中的数据是否足够解析出一个消息头(消息ID + 消息长度)
  18. if (_buffer.size() < static_cast<int>(sizeof(quint16) * 2)) {
  19. return; // 数据不够,等待更多数据
  20. }
  21. // 预读取消息ID和消息长度,但不从缓冲区中移除
  22. stream >> _message_id >> _message_len;
  23. //将buffer 中的前四个字节移除
  24. _buffer = _buffer.mid(sizeof(quint16) * 2);
  25. // 输出读取的数据
  26. qDebug() << "Message ID:" << _message_id << ", Length:" << _message_len;
  27. }
  28. //buffer剩余长读是否满足消息体长度,不满足则退出继续等待接受
  29. if(_buffer.size() < _message_len){
  30. _b_recv_pending = true;
  31. return;
  32. }
  33. _b_recv_pending = false;
  34. // 读取消息体
  35. QByteArray messageBody = _buffer.mid(0, _message_len);
  36. qDebug() << "receive body msg is " << messageBody ;
  37. _buffer = _buffer.mid(_message_len);
  38. }
  39. });
  40. //5.15 之后版本
  41. // QObject::connect(&_socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred), [&](QAbstractSocket::SocketError socketError) {
  42. // Q_UNUSED(socketError)
  43. // qDebug() << "Error:" << _socket.errorString();
  44. // });
  45. // 处理错误(适用于Qt 5.15之前的版本)
  46. QObject::connect(&_socket, static_cast<void (QTcpSocket::*)(QTcpSocket::SocketError)>(&QTcpSocket::error),
  47. [&](QTcpSocket::SocketError socketError) {
  48. qDebug() << "Error:" << _socket.errorString() ;
  49. switch (socketError) {
  50. case QTcpSocket::ConnectionRefusedError:
  51. qDebug() << "Connection Refused!";
  52. emit sig_con_success(false);
  53. break;
  54. case QTcpSocket::RemoteHostClosedError:
  55. qDebug() << "Remote Host Closed Connection!";
  56. break;
  57. case QTcpSocket::HostNotFoundError:
  58. qDebug() << "Host Not Found!";
  59. emit sig_con_success(false);
  60. break;
  61. case QTcpSocket::SocketTimeoutError:
  62. qDebug() << "Connection Timeout!";
  63. emit sig_con_success(false);
  64. break;
  65. case QTcpSocket::NetworkError:
  66. qDebug() << "Network Error!";
  67. break;
  68. default:
  69. qDebug() << "Other Error!";
  70. break;
  71. }
  72. });
  73. // 处理连接断开
  74. QObject::connect(&_socket, &QTcpSocket::disconnected, [&]() {
  75. qDebug() << "Disconnected from server.";
  76. });
  77. QObject::connect(this, &TcpMgr::sig_send_data, this, &TcpMgr::slot_send_data);
  78. }

连接对端服务器

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

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

  1. void TcpMgr::slot_send_data(ReqId reqId, QString data)
  2. {
  3. uint16_t id = reqId;
  4. // 将字符串转换为UTF-8编码的字节数组
  5. QByteArray dataBytes = data.toUtf8();
  6. // 计算长度(使用网络字节序转换)
  7. quint16 len = static_cast<quint16>(data.size());
  8. // 创建一个QByteArray用于存储要发送的所有数据
  9. QByteArray block;
  10. QDataStream out(&block, QIODevice::WriteOnly);
  11. // 设置数据流使用网络字节序
  12. out.setByteOrder(QDataStream::BigEndian);
  13. // 写入ID和长度
  14. out << id << len;
  15. // 添加字符串数据
  16. block.append(data);
  17. // 发送数据
  18. _socket.write(block);
  19. }

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

  1. void LoginDialog::initHttpHandlers()
  2. {
  3. //注册获取登录回包逻辑
  4. _handlers.insert(ReqId::ID_LOGIN_USER, [this](QJsonObject jsonObj){
  5. int error = jsonObj["error"].toInt();
  6. if(error != ErrorCodes::SUCCESS){
  7. showTip(tr("参数错误"),false);
  8. enableBtn(true);
  9. return;
  10. }
  11. auto user = jsonObj["user"].toString();
  12. //发送信号通知tcpMgr发送长链接
  13. ServerInfo si;
  14. si.Uid = jsonObj["uid"].toInt();
  15. si.Host = jsonObj["host"].toString();
  16. si.Port = jsonObj["port"].toString();
  17. si.Token = jsonObj["token"].toString();
  18. _uid = si.Uid;
  19. _token = si.Token;
  20. qDebug()<< "user is " << user << " uid is " << si.Uid <<" host is "
  21. << si.Host << " Port is " << si.Port << " Token is " << si.Token;
  22. emit sig_connect_tcp(si);
  23. });
  24. }

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

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

LoginDialog收到连接结果的槽函数

  1. void LoginDialog::slot_tcp_con_finish(bool bsuccess)
  2. {
  3. if(bsuccess){
  4. showTip(tr("聊天服务连接成功,正在登录..."),true);
  5. QJsonObject jsonObj;
  6. jsonObj["uid"] = _uid;
  7. jsonObj["token"] = _token;
  8. QJsonDocument doc(jsonObj);
  9. QString jsonString = doc.toJson(QJsonDocument::Indented);
  10. //发送tcp请求给chat server
  11. TcpMgr::GetInstance()->sig_send_data(ReqId::ID_CHAT_LOGIN, jsonString);
  12. }else{
  13. showTip(tr("网络异常"),false);
  14. enableBtn(true);
  15. }
  16. }

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

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

热门评论

热门文章

  1. Linux环境搭建和编码

    喜欢(594) 浏览(6419)
  2. C++ 类的继承封装和多态

    喜欢(588) 浏览(2960)
  3. 解密定时器的实现细节

    喜欢(566) 浏览(2125)
  4. windows环境搭建和vscode配置

    喜欢(587) 浏览(1801)
  5. slice介绍和使用

    喜欢(521) 浏览(1880)

最新评论

  1. 泛型算法的定制操作 secondtonone1:lambda和bind是C11新增的利器,善于利用这两个机制可以极大地提升编程安全性和效率。
  2. C++ 虚函数表原理和类成员内存分布 WangQi888888:class Test{ int m; int b; }中b成员是int,为什么在内存中只占了1个字节。不应该是4个字节吗?是不是int应该改为char。这样的话就会符合图上说明的情况
  3. 类和对象 陈宇航:支持!!!!
  4. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  5. slice介绍和使用 恋恋风辰:切片作为引用类型极大的提高了数据传递的效率和性能,但也要注意切片的浅拷贝隐患,算是一把双刃剑,这世间的常态就是在两极之间寻求一种稳定。

个人公众号

个人微信