Qt使用TCP实现的简单服务端和客户端(带心跳检测)

2024-06-03 21:08

本文主要是介绍Qt使用TCP实现的简单服务端和客户端(带心跳检测),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【正文开始】

        之前在做一个简单的聊天工具 ( 仿 QQ ),地址为https://github.com/mengps/MChat

        界面基本是完成了,但是肯定是要用 TCP 传输的,自己大概的做了一个简单的实现,然后也加入了心跳检测的机制,还是先上一下效果图:

        使用 Qt 的网络功能,需要在.pro中加入 QT += network

        服务端我使用 QTcpServer 来建立, ps:(因为窗口是qml做的,所以会有很多 invokeMethod   ̄へ ̄,不用在意)

        主要就是重新实现其 void incomingConnection( qintptr socketDescriptor函数:

void ChatTcpServer::incomingConnection(qintptr socketDescriptor)
{QThread *thread = new QThread;        //不可以有parentChatSocket *socket = new ChatSocket(socketDescriptor);connect(thread, &QThread::finished, thread, &QThread::deleteLater);    //线程结束后自动删除自己connect(socket, &ChatSocket::consoleMessage, this, [this](const QString &message)    //这里使用lambda表达式,很方便{QMetaObject::invokeMethod(m_window, "addMessage", Q_ARG(QVariant, QVariant(message)));});connect(socket, &ChatSocket::clientDisconnected, this, [this](const QString &ip){QMetaObject::invokeMethod(m_window, "removeClient", Q_ARG(QVariant, QVariant(ip)));});QMetaObject::invokeMethod(m_window, "addNewClient", Q_ARG(QVariant, QVariant(socket->peerAddress().toString())));socket->moveToThread(thread);            //注意,使用moveToThread方法将socket转移到新线程中thread->start();
}

        每一个连接的 client 都用一个线程进行处理,下面是 ChatSocket 的实现,心跳检测也在其中完成:

        chatsocket.cpp:

#include <QTimer>
#include <QDataStream>
#include <QHostAddress>
#include <QCryptographicHash>
#include "chatsocket.h"
#include "mymessagedef.h"            //这个里面主要就是自己的一些消息定义ChatSocket::ChatSocket(qintptr socketDescriptor, QObject *parent): QTcpSocket(parent)
{if (!setSocketDescriptor(socketDescriptor)){emit consoleMessage(errorString());deleteLater();}m_heartbeat = new QTimer(this);m_heartbeat->setInterval(30000);                    //30秒进行一次心跳检测m_lastTime = QDateTime::currentDateTime();connect(this, &ChatSocket::readyRead, this, &ChatSocket::heartbeat);        //任何到来的数据都会重置心跳connect(this, &ChatSocket::readyRead, this, &ChatSocket::readClientData);connect(this, &ChatSocket::disconnected, this, &ChatSocket::onDisconnected);connect(m_heartbeat, &QTimer::timeout, this, &ChatSocket::checkHeartbeat);m_heartbeat->start();                 //开始心跳
}ChatSocket::~ChatSocket()
{
}void ChatSocket::heartbeat()
{if (!m_heartbeat->isActive())m_heartbeat->start();    m_lastTime = QDateTime::currentDateTime();
}void ChatSocket::checkHeartbeat()
{if (m_lastTime.secsTo(QDateTime::currentDateTime()) >= 30)   //超过30s即为掉线,停止心跳{qDebug() << "heartbeat 超时, 即将断开连接";m_heartbeat->stop();disconnectFromHost();}
}void ChatSocket::onDisconnected()
{//连接中断,删除自己emit clientDisconnected(peerAddress().toString());emit consoleMessage(peerAddress().toString() + " 断开连接..");deleteLater();
}/*  消息发送方式如下,先发一个消息头,然后接下来的都是数据  
*  | 消息标志flag || 消息类型type || 消息大小size || MD5验证 |   ...  | 数据data |
*                        {消息头}                                      {数据} 
*/void ChatSocket::readClientData()
{static int got_size = 0;                //已经获取到的数据大小,不包括消息头 static MSG_TYPE type = MT_UNKNOW;        //像MSG_TYPE这种类型,是我自己定义消息格式,忽略它....主要讲思路 static MSG_MD5_TYPE md5; if (m_data.size() == 0)             //必定为消息头,消息头在发送端用QDataStream发送的,因此读的时候也一样{ QDataStream in(this); in.setVersion(QDataStream::Qt_5_9); MSG_FLAG_TYPE flag; in >> flag; if (flag != MSG_FLAG)                //我在消息头加入了一个标志...忽略 return; in >> type; if (type == MT_HEARTBEAT) //如果是心跳检测,直接返回 return; MSG_SIZE_TYPE size; in >> size >> md5;                //读取接下来的数据大小以及md5验证 m_data.resize(size);} else //合并数据 { QByteArray data = read(bytesAvailable());        //非消息头的数据我直接用的write,因此读的时候用read m_data.replace(got_size, data.size(), data);     //用replace不会改变m_data的大小 got_size += data.size(); } if (got_size == m_data.size()) //接收完毕 { QByteArray md5_t = QCryptographicHash::hash(m_data, QCryptographicHash::Md5); if (md5 == md5_t) //正确的消息 { QString str = QString::fromLocal8Bit(m_data.data()); emit consoleMessage(QString("md5 一致,消息为:\"" + str + "\",大小:" + QString::number(m_data.size()))); switch (type) { case MT_SHAKE:                    //因为主要都是测试,所以都没有写,应该放自己的具体的操作 break; case MT_TEXT: break; default: break; }}got_size = 0; //重新开始 type = MT_UNKNOW; md5.clear(); m_data.clear(); }
}

        chatsocket.h:

#ifndef CHATSOCKET_H  
#define CHATSOCKET_H  #include <QTcpSocket>  
#include <QDateTime>  class QTimer;  
class ChatSocket : public QTcpSocket  
{  Q_OBJECT  public:  ChatSocket(qintptr socketDescriptor, QObject *parent = nullptr);  ~ChatSocket();  public slots:  void readClientData();  private slots:  void heartbeat();  void checkHeartbeat();  void onDisconnected();  signals:  void clientDisconnected(const QString &ip);  void consoleMessage(const QString &message);  private:  QTimer *m_heartbeat;  QDateTime m_lastTime;  QByteArray m_data;  
};  #endif // CHATSOCKET_H  

        客户端就比较简单了,我是直接在官方的例子 fortuneclient 上改的 ( 咳咳我太懒了....),名字是 Fortune Client Example ( 可以直接在 QtCreator 中搜索到 )。

        构造函数中主要增加了:

connect(getFortuneButton, &QAbstractButton::clicked, this, &Client::sendMessageHeader);
connect(tcpSocket, &QTcpSocket::bytesWritten, this, &Client::sendMessage);    //byteWritten信号在每次数据发送后emit

        两个槽函数 sendMessageHeader()sendMessage() 如下:

void Client::sendMessageHeader()
{MSG_FLAG_TYPE flag = MSG_FLAG;MSG_TYPE type = MT_TEXT;MSG_SIZE_TYPE size = messageEdit->text().toLocal8Bit().size();        //收发都使用 toLocal8Bit,中文不会乱码MSG_MD5_TYPE md5 = QCryptographicHash::hash(messageEdit->text().toLocal8Bit(), QCryptographicHash::Md5);QByteArray block;QDataStream out(&block, QIODevice::WriteOnly);out.setVersion(QDataStream::Qt_5_9);out << flag << type << size << md5;fileBytes = size + 29;                // sizeof(flag) + sizeof(type) +  sizeof(size) + md5.size()16字节 + 4 = 29tcpSocket->write(block);              //用QDataStream写QByteArray 会在前面加 4 字节的大小信息,所以最后加了 4 字节
}void Client::sendMessage(qint64 sentSize)  //发送实际的data,因为只是测试,大的数据应该在分开发送,在最后一行tcpSocket->write(block,包大小);
{static int sentBytes = 0;sentBytes += sentSize;if (sentBytes >= fileBytes){fileBytes = 0;sentBytes = 0;return;}QByteArray block = messageEdit->text().toLocal8Bit();Sleep(10);tcpSocket->write(block);    //直接使用write
}

        哦,忘了还有 listen(),我放在 main 中了:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "chattcpserver.h"int main(int argc, char *argv[])
{QGuiApplication app(argc, argv);QQmlApplicationEngine engine;ChatTcpServer server(&engine);if (!server.listen(QHostAddress::AnyIPv4, 56789)){QGuiApplication::exit(1);}else server.loadWindow();return app.exec();
}

 【结语】

        好了,差不多写完了,还是有点长的....,不过我的注释还是很多的,思路应该还是比较清楚的吧....

        代码下载:https://download.csdn.net/download/u011283226/10347489

这篇关于Qt使用TCP实现的简单服务端和客户端(带心跳检测)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1028118

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置