聊天项目(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. vscode搭建windows C++开发环境

    喜欢(596) 浏览(82378)
  2. 聊天项目(28) 分布式服务通知好友申请

    喜欢(507) 浏览(6046)
  3. 使用hexo搭建个人博客

    喜欢(533) 浏览(11765)
  4. Linux环境搭建和编码

    喜欢(594) 浏览(13391)
  5. Qt环境搭建

    喜欢(517) 浏览(24411)

最新评论

  1. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗
  2. string类 WangQi888888:确实错了,应该是!isspace(sind[index]). 否则不进入循环,还是原来的字符串“some string”
  3. 类和对象 陈宇航:支持!!!!
  4. Qt MVC结构之QItemDelegate介绍 胡歌-此生不换:gpt, google
  5. protobuf配置和使用 熊二:你可以把dll放到系统目录,也可以配置环境变量,还能把dll丢到lib里
  6. 网络编程学习方法和图书推荐 Corleone:啥程度可以找工作
  7. 再谈单例模式 secondtonone1:是的,C++11以后返回局部static变量对象能保证线程安全了。
  8. visual studio配置boost库 一giao里我离giaogiao:请问是修改成这样吗:.\b2.exe toolset=MinGW
  9. 聊天项目(9) redis服务搭建 pro_lin:redis线程池的析构函数,除了pop出队列,还要free掉redis连接把
  10. 聊天项目(7) visualstudio配置grpc diablorrr:cmake文件得改一下 find_package(Boost REQUIRED COMPONENTS system filesystem),要加上filesystem。在target_link_libraries中也同样加上
  11. 利用栅栏实现同步 Dzher:作者你好!我觉得 std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); 这个例子中的assert fail并不会发生,原子变量设定了非relaxed内存序后一个线程的原子变量被写入,那么之后的读取一定会被同步的,c和d线程中只可能同时发生一个z++未执行的情况,最终z不是1就是2了,我测试了很多次都没有assert,请问我这个观点有什么错误,谢谢!
  12. 面试题汇总(一) secondtonone1:看到网络上经常提问的go的问题,做了一下汇总,结合自己的经验给出的答案,如有纰漏,望指正批评。
  13. interface应用 secondtonone1:interface是万能类型,但是使用时要转换为实际类型来使用。interface丰富了go的多态特性,也降低了传统面向对象语言的耦合性。
  14. 聊天项目(15) 客户端实现TCP管理者 lkx:已经在&QTcpSocket::readyRead 回调函数中做了处理了的。
  15. 堆排序 secondtonone1:堆排序非常实用,定时器就是这个原理制作的。
  16. 答疑汇总(thread,async源码分析) Yagus:如果引用计数为0,则会执行 future 的析构进而等待任务执行完成,那么看到的输出将是 这边应该不对吧,std::future析构只在这三种情况都满足的时候才回block: 1.共享状态是std::async 创造的(类型是_Task_async_state) 2.共享状态没有ready 3.这个future是共享状态的最后一个引用 这边共享状态类型是“_Package_state”,引用计数即使为0也不应该block啊
  17. 创建项目和编译 secondtonone1:谢谢支持
  18. Qt 对话框 Spade2077:QDialog w(); //这里是不是不需要带括号
  19. 处理网络粘包问题 zyouth: //消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里 if (bytes_transferred < data_len) { memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred); _recv_msg_node->_cur_len += bytes_transferred; ::memset(_data, 0, MAX_LENGTH); _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self)); //头部处理完成 _b_head_parse = true; return; } 把_b_head_parse = true;放在_socket.async_read_some前面是不是更好
  20. 构造函数 secondtonone1:构造函数是类的基础知识,要着重掌握
  21. boost::asio之socket的创建和连接 项空月:发现一些错别字 :每隔vector存储  是不是是每个. asio::mutable_buffers_1 o或者    是不是多打了个o
  22. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  23. C++ 并发三剑客future, promise和async Yunfei:大佬您好,如果这个线程池中加入的异步任务的形参如果有右值引用,这个commit中的返回类型推导和bind绑定就会出现问题,请问实际工程中,是不是不会用到这种任务,如果用到了,应该怎么解决?

个人公众号

个人微信