Qt5.14.2 独步天下!自制Qt5 P2P聊天神器,让安全与可靠性不再是梦


在上一期的文章中,我们已经了解了如何利用Qt开发一个基本的P2P聊天系统,并对服务端的用户管理和客户端的网络交互、音视频处理等模块进行了代码实践。

不过光有基本的通信功能还远远不够,要想开发出一款值得信赖的聊天应用,我们还必须重点关注系统的安全性和可靰性这两大核心需求。今天,我们就来全方位剖析一下在P2P架构中如何实现这两方面的技术保障。


一、安全性----坚不可摧的数据防线


1、用户认证


对于任何网络应用,用户身份认证都是安全防护的第一道防线。我们的聊天系统中也不例外,需要为每个用户分配唯一的账号密码,并在服务端存储这些认证信息。

// server.h
class UserManager : public QObject  
{
    Q_OBJECT

public:
    // 注册新用户
    void addUser(const QString& username, const QString& password);
    
    // 检查用户凭据
    bool checkCredential(const QString& username, const QString& password) const;

private:
    // 用户名->密码哈希(SHA-256)
    QHash<QString, QByteArray> m_credentials;
};

// server.cpp
void UserManager::addUser(const QString& username, const QString& password)
{
    QByteArray pwdHash = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256);
    m_credentials.insert(username, pwdHash);
}

bool UserManager::checkCredential(const QString& username, const QString& password) const
{
    QByteArray pwdHash = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256);
    return m_credentials.value(username) == pwdHash;
}

这里我们在UserManager中使用QHash存储所有注册用户的凭据。但与存明文密码不同的是,我们对用户密码进行了SHA-256算法的哈希加密,只存储哈希值而不是明文。

登录时,客户端将用户输入的账号密码传给服务器,服务器重新计算密码的哈希值,并与存储的哈希值对比,只有完全匹配才认证通过。这样即使服务器数据库被攻破,窃取到的也只是不可逆的哈希值,用户密码的安全性也得到了保障。


2、通信加密


用户认证环节保证了系统的入口安全,但是对于正式建立的通话会话,我们也需要对传输数据进行端到端加密。否则即使会话建立的过程是安全的,但数据在网络传输途中一旦被截获,通话隐私也将泄露。

// client.h
class P2PClient : public QObject
{
    Q_OBJECT
    ...
public slots:
    void startCall(const QString& peer);
    void sendData(const QByteArray& data);

private:
    QCryptographicHash m_crypto;
    QByteArray m_sessionKey; // 会话密钥
    QString m_peer; // 通话对象
    ...
};

// client.cpp
void P2PClient::startCall(const QString& peer)
{
    // 生成会话密钥
    m_peer = peer;
    m_sessionKey = m_crypto.hash((peer + QUuid::createUuid().toByteArray()), QCryptographicHash::Sha256);

    // 发起呼叫,并传递会话密钥  
    QByteArray keyData = encodeData(m_sessionKey);
    m_socket->write(encodeCallRequest(peer, keyData));
}

void P2PClient::sendData(const QByteArray& rawData)
{
    // 使用会话密钥加密数据
    QByteArray encryptedData = m_crypto.encrypt(rawData, m_sessionKey);
    m_socket->write(encodeDataPacket(encryptedData));
}

这里我们使用了Qt提供的QCryptographicHash类,在建立通话时基于双方的身份信息和随机UUID生成一个会话密钥。之后所有的通话数据都将使用该密钥进行对称加密,确保了端到端的通信隐私。

会话密钥的协商过程也可以进一步用基于PKI的非对称加密技术进行保护,以防中间人攻击等网络攻击手段。


3、权限控制


最后,出于隐私考虑,我们的聊天系统需要有一定的权限控制机制,防止任何用户都可以窥探或加入他人的通话。一种较为简单的方式是使用"白名单"模式:

// server.h
class CallManager : public QObject
{
    Q_OBJECT
    ...
public:
    void addWhitelist(const QString& caller, const QString& callee);
    bool isCallAllowed(const QString& caller, const QString& callee);
    ...
};

// server.cpp 
void CallManager::addWhitelist(const QString& caller, const QString& callee)
{
    m_whitelists[caller].insert(callee);
    m_whitelists[callee].insert(caller);
}

bool CallManager::isCallAllowed(const QString& caller, const QString& callee)
{
    return m_whitelists.value(caller).contains(callee) && 
           m_whitelists.value(callee).contains(caller);
}

这里我们引入了CallManager模块,使用QHash保存每个用户的"白名单"列表,即允许通信的对象名单。只有双方都将对方加入了白名单,才能够互相呼叫和接受对方的通话请求。

当然,这只是一个简单的示例实现方案,实际应用中我们可以设计更加复杂和人性化的权限控制机制。关键是要防止意外或恶意呼叫对隐私的侵扰。


二、可靠性----让通信永不间断


一款优秀的聊天软件,除了要有足够的安全防护,还必须确保通信的可靠性,避免中断或数据丢失。针对这方面的需求,我们同样可以从多个层面着手。


1、断线重连


由于各种原因,客户端与服务器可能会失去连接,比如网络中断、进程崩溃等。我们需要设计重连机制,在连接恢复时能自动重新接入系统,继续之前的通话:

// client.h
class P2PClient : public QObject
{
    Q_OBJECT

public:
    P2PClient(QObject *parent = nullptr);

public slots:
    void connectToServer(const QString& host, quint16 port);
    void login(const QString& username, const QString& password);
    
signals:
    void serverConnected();
    void loginSuccess(const QString& username);
    void connectionClosed();

private slots:
    void onSocketStateChanged(QAbstractSocket::SocketState state);
    void onSocketError(QAbstractSocket::SocketError error);
    void onServerData();

private:
    QTcpSocket* m_socket;
    QString m_username;
    bool m_reconnecting = false;
    
    void reconnect();
};

// client.cpp
P2PClient::P2PClient(QObject *parent) : QObject(parent)
{
    m_socket = new QTcpSocket(this);
    connect(m_socket, &QTcpSocket::stateChanged, this, &P2PClient::onSocketStateChanged);
    connect(m_socket, &QTcpSocket::errorOccurred, this, &P2PClient::onSocketError);
    connect(m_socket, &QTcpSocket::readyRead, this, &P2PClient::onServerData);
}

void P2PClient::onSocketStateChanged(QAbstractSocket::SocketState state)
{
    if (state == QAbstractSocket::UnconnectedState) {
        if (m_reconnecting) {
            // 正在重连中,暂不处理
            return;
        }
        emit connectionClosed();
        reconnect(); // 启动重连
    }
}

void P2PClient::onSocketError(QAbstractSocket::SocketError error)
{
    if (error != QAbstractSocket::SocketTimeoutError)
        reconnect(); // 启动重连
}

void P2PClient::reconnect()
{
    m_reconnecting = true;
    m_socket->abort(); // 断开当前连接

    // 5秒后重新连接
    QTimer::singleShot(5000, [=]() {
        m_socket->connectToHost(m_serverHost, m_serverPort);
    });
}

在这个实现中,我们监听了QTcpSocket的stateChanged和errorOccurred信号。一旦检测到Socket断开连接或发生错误时,就会调用reconnect()函数尝试重新连接服务器。

reconnect()函数首先会断开当前连接,然后延迟5秒后再重新连接。这样可以避免连接失败后立即reconnect导致的过度重试。

同时我们引入了m_reconnecting标志,确保重连过程中的重复事件不会触发多次重连动作。

当重连成功时,Socket会进入ConnectedState,整个reconnect过程结束。重连期间,其他与服务器的交互动作会暂时中止,待重连成功后再自动恢复。

通过这种机制,我们可以较好地应对短暂的网络抖动和中断,提高了客户端与服务端连接的稳定性和可靠性。

需要注意的是,这只是一个比较简单的重连实现,实际应用中我们可能还需要考虑更多情况,如永久性连接失败后的退回到登录界面、重传丢失数据等。但基本思路是相同的,这里就不再赘述了。


2、数据缓存和重传


即使有了断线重连机制,仍然无法避免在短暂网络抖动时的数据丢包问题。为了确保数据的完整传输,我们需要在应用层设计重传机制:

// client.h
class P2PClient : public QObject
{
    Q_OBJECT
    ...
private:
    QMap<quint16, QByteArray> m_sendBuffer; // 发送缓冲区
    quint16 m_sendSequence; // 发送序列号

    QMap<quint16, QByteArray> m_receiveBuffer; // 接收缓冲区
    quint16 m_expectedSequence; // 期望接收序列号
    ...
};

// client.cpp
void P2PClient::sendData(const QByteArray& rawData)
{
    QByteArray packetData = encodeDataPacket(rawData, m_sendSequence);
    m_socket->write(packetData);
    
    m_sendBuffer[m_sendSequence] = rawData; // 缓存数据
    m_sendSequence++;
}

void P2PClient::processDataPacket(const QByteArray& packetData)
{
    quint16 sequence;
    QByteArray data = decodeDataPacket(packetData, sequence);
    
    if (sequence == m_expectedSequence) {
        // 按序到达,立即处理
        handleData(data);
        m_expectedSequence++;
        
        // 处理缓冲区中的后续数据包
        while(m_receiveBuffer.contains(m_expectedSequence)) {
            handleData(m_receiveBuffer[m_expectedSequence]);
            m_receiveBuffer.remove(m_expectedSequence);
            m_expectedSequence++;
        }
    } else {
        // 出序到达,缓存起来
        m_receiveBuffer[sequence] = data;
    }
}

这里我们为发送和接收的数据分别设置了序列号。发送时,将编码后的数据缓存到发送缓冲区;接收时,如果包序号连续则立即处理,否则先暂存入接收缓冲区。

一旦网络质量恢复,就可以按序从缓冲区取出之前丢失的数据包,从而实现可靠的数据传输。

我们也可以在此基础上设计确认和重传机制,进一步提高可靠性,这里我就不再赘述了。


3、数据冗余与纠错


对于实时的音视频数据来说,我们无法采用像文本那样的重传机制,因为那会导致延迟增大,影响体验。取而代之的是增加数据冗余和纠错能力。

一种方法是在应用层通过添加冗余编码,使接收端具有一定的纠错和丢包容忍能力。比如:

// client.h 
struct VideoData
{
    quint32 frameIndex; // 帧序号
    QByteArray rawData; // 原始视频数据
    QByteArray redundantData; // 冗余纠错数据
};

// 发送端
VideoData encodeVideoData(const QByteArray& rawData)  
{
    VideoData data;
    data.frameIndex = m_frameIndex++; // 为每个数据包添加序列号
    data.rawData = rawData;

    // Reed-Solomon码冗余编码  
    QByteArray redundantData = reedSolomonEncode(rawData);
    data.redundantData = redundantData;

    return data;
}

void sendVideoData(const VideoData& data)
{
    // 分别发送原始数据和冗余数据
    m_socket->write(encodePacket(data.rawData));
    m_socket->write(encodePacket(data.redundantData));
}

// 接收端  
void processVideoPacket(const QByteArray& packet)
{
    if (isRawVideoData(packet)) {
        // 收到原始数据包
        VideoData data = decodeRawVideoData(packet);
        emit newVideoFrame(data.frameIndex, data.rawData);

        m_frameBuffer[data.frameIndex] = data;
    } else {
        // 收到冗余数据包
        int frameIndex = getFrameIndex(packet);
        if (m_frameBuffer.contains(frameIndex)) {
            // 该帧原始数据已收到,忽略冗余数据
            return;
        }

        // 利用冗余数据纠错恢复原始数据
        VideoData data = recoverVideoData(packet);
        emit newVideoFrame(data.frameIndex, data.rawData);
        m_frameBuffer[data.frameIndex] = data;
    }
}

这里我们在发送端为每个视频帧数据都添加了冗余编码(可以使用像Reed-Solomon这种纠错码),并以两个独立数据包的形式发送原始数据和冗余数据。

接收端如果收到原始数据包,则直接进行视频解码和渲染;如果收到冗余数据包,且对应帧的原始数据没有收到,就利用冗余数据进行纠错恢复,从而提高了视频数据的可靠传输能力。

除了应用层编码,我们也可以选择在传输层使用带纠错能力的协议,如GBN(Go-Back-N)和SR(选择重传)协议。


4、负载均衡


最后,如果我们的系统规模较大,负载较重,为了进一步提高可用性和可扩展性,我们可以考虑在服务端架构上作进一步优化,采用负载均衡和集群部署方式:

             +------------+  
             | Load       |
             | Balancer   |
             +------+-----+
                    |
            +-------------+
            |            |
+------------+----------+   +------------+-----------+
|         Server        |   |         Server         |
|        Cluster        |   |         Cluster        |
+------------------------+   +-------------------------+

我们可以使用硬件负载均衡器或者软件的负载均衡组件(如Nginx),将来自客户端的连接请求均匀分发给后端的服务器集群。每个服务器实例相互独立,分别维护一部分在线用户,从而实现负载分散和高可用。

当某个服务器实例发生故障时,其他实例可以继续运行,新的客户端请求将被转发至正常的实例,使系统as a whole保持可用状态。我们也可以在实例故障时自动在集群中拉起新的实例,从而保持整体服务能力。

通过以上负载均衡和集群化部署,我们的聊天系统将具备很强的可扩展能力,能够轻松应对规模扩大和负载变重的场景,持续为用户提供稳定可靠的服务。


三、真香定律


通过上述一系列安全性和可靠性的技术措施,我相信我们已经为这款自制Qt P2P聊天神器的研发和应用奠定了坚实的基础。无论是用户认证、数据加密,还是断线重连、数据纠错、负载均衡等,我们在各个环节都作了全方位的考虑和部署,确保了整个系统的稳定安全可靠运行。

实现这一切的关键,就在于利用Qt框架强大的网络通信、多媒体、并行计算等跨平台能力,有效整合各种开源技术和自主开发的模块,将它们高度集成到了一个完整的解决方案之中。

当然,这只是一个起点。一款真正出色的产品,不仅需要过硬的技术实力,更需要与时俱进的创新思维。我们离开开源精神,促进网络通信的民主化和去中心化。让通信不再受限于任何门槛,真正做到"人与人之间免费、安全、可靠地交流"的终极目标,还有很长的路要走。

不过,有了现在这个基础,相信我们未来一定能逐步突破更多技术障碍,打造出一款具有革命性的新型通信工具。就让我们共同期待,期待这款自制Qt P2P聊天神器何时可以与世人见面,展现它真正的"神力"吧!


  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

w风雨无阻w

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值