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;        //不可以有parent
    ChatSocket *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 = 29
    tcpSocket->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

  • 13
    点赞
  • 200
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 36
    评论
Qt是一个跨平台的C++应用程序框架,它提供了丰富的库和工具来帮助开发者创建各种类型的应用程序,包括网络通信应用。 在Qt实现TCP服务端客户端的通信非常简单。首先,我们需要创建一个QTcpServer对象来作为服务端,该对象负责监听客户端的连接请求。接下来,我们需要为QTcpServer对象绑定一个IP地址和端口号,以便客户端能够连接到该地址和端口。当有新的客户端连接时,QTcpServer对象会发出newConnection()信号。 在服务端的槽函数中,我们可以使用QTcpServer的nextPendingConnection()函数获取新连接的QTcpSocket对象,该对象用于和客户端进行通信。我们可以在该对象上发送和接收数据,以实现服务端客户端之间的数据交互。 接下来,我们需要创建一个QTcpSocket对象作为客户端,该对象负责和服务端建立连接。我们需要使用connectToHost()函数来指定要连接的服务端的IP地址和端口号。一旦连接成功,QTcpSocket对象会发出connected()信号。 在客户端的槽函数中,我们可以使用QTcpSocket对象发送和接收数据。我们可以使用write()函数发送数据,使用readyRead()信号和read()函数接收数据。 通过这种方式,我们可以在服务端客户端之间进行双向的数据通信。服务端可以同时接受多个客户端的连接,每个客户端都可以和服务端进行独立的数据交互。 总的来说,使用Qt实现TCP服务端客户端通信非常方便。通过使用QTcpServer和QTcpSocket类,我们可以轻松地建立连接,发送和接收数据。这种通信方式可以应用于多种场景,如实时聊天、远程控制等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦起丶

您的鼓励和支持是我创作最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值