之前在做一个简单的聊天工具,界面基本是完成了,但是肯定是要用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
#include
#include
#include
#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
#include
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
#include
#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