之前在做一个简单的聊天工具,界面基本是完成了,但是肯定是要用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 | ... | 数据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
构造函数中主要增加了:
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