使用 QuantumGate P2P 网络库构建聊天应用程序





5.00/5 (6投票s)
构建用于安全和私密通信的点对点聊天应用程序的示例
引言
这个示例聊天应用程序演示了如何开始使用 QuantumGate 点对点网络库。QuantumGate 是一个用 C++ 编写的点对点 (P2P) 通信协议、库和 API。QuantumGate 的长期目标是成为一个基于网状网络模型的分布式计算平台。短期目标是为开发者提供易于集成并用于自己应用程序的网络技术。
QuantumGate 为您处理所有底层网络和加密,让您可以专注于应用程序的特定功能。除其他外,它负责数据压缩、加密/解密、身份验证,甚至更高级的功能,例如中继(类似于 TOR),以及提供填充和掩盖流量以使流量分析和审查更加困难。此外,它还允许您在 IP、网络和对等体级别控制安全和访问设置。有关更多详细信息,请参阅 GitHub 上文档中的概述。
创建项目
对于这个聊天应用程序,我选择使用 Microsoft C++/WinRT 框架作为 UI。您可以从 marketplace 下载适用于 Visual Studio 的 C++/WinRT 扩展。该扩展在 Visual Studio 中添加了一些额外的 C++ 项目模板。对于本文,我使用了 Visual Studio 2019,并在创建新解决方案时选择了 Blank App (C++/WinRT) 模板。
完成“创建新项目”向导后,您还需要在 Visual Studio 中安装 Microsoft.Windows.CppWinRT NuGet 包。您还需要 QuantumGate
库;如果您愿意,可以从源代码构建,但您也可以从 GitHub 上的发布页面下载预构建的二进制文件,其中包括您将要链接的 .lib 和 .dll 文件。在这种情况下,您只能使用源代码中所需的头文件来使用 QuantumGate
API。
现在,您需要在项目属性中(在 Visual Studio 中右键单击项目名称并选择属性)指定编译器可以查找 QuantumGate 头文件和库文件的位置。这可以在VC++ 目录部分完成,如下图所示。在包含目录中,您需要添加 QuantumGate 附带的 QuantumgateLib\API 子文件夹,以便编译器能找到位于其中的 QuantumGate.h 头文件。在库目录中,您需要指定将要链接的 QuantumGate*.lib 文件的位置。请注意截图中,路径中使用了变量来表示配置和平台子文件夹名称,以便编译器根据您要编译的配置和平台选择正确的文件。
由于 QuantumGate 在撰写本文时需要 C++20 功能,您还需要将项目配置为使用最新的 C++ 功能。这可以在 C++ 语言标准选项下完成,如下图所示。您需要指定“来自最新 C++ 工作草案的功能 (/std:c++latest)”。
接下来,您必须实际将 QuantumGate 头文件和库文件包含到您的项目中。有多种方法可以做到这一点,但在本项目中,我选择将其添加到 Visual Studio 为项目生成的 pch.h 预编译头文件中。您需要在该文件中添加以下代码片段
// Include the QuantumGate main header with API definitions
#include <QuantumGate.h>
// Link with the QuantumGate library depending on architecture
#if defined(_DEBUG)
#if !defined(_WIN64)
#pragma comment (lib, "QuantumGate32D.lib")
#else
#pragma comment (lib, "QuantumGate64D.lib")
#endif
#else
#if !defined(_WIN64)
#pragma comment (lib, "QuantumGate32.lib")
#else
#pragma comment (lib, "QuantumGate64.lib")
#endif
#endif
除了包含头文件,我们还根据配置(调试或发布)和平台(Win32 或 Win64)包含库文件。
现在一切都已设置好,我们可以继续实际构建应用程序。
聊天应用程序
如果您下载聊天应用程序的示例代码,您会发现以下几个特别重要的文件
- MainPage.xaml:由 Visual Studio 向导生成,此文件包含主窗口的 UI。它已根据聊天应用程序的 UI 进行了定制。
- MainPage.cpp 和 MainPage.h,由 Visual Studio 向导生成,包含前一个 .xaml 文件中定义的 UI 的后台代码。
- ChatExtender.cpp 和 ChatExtender.h,其中包含我们自定义 QuantumGate 扩展器的代码,该扩展器将在 QuantumGate 之上实现我们需要的特定聊天功能。
- ChatTab.cpp 和 ChatTab.h,其中包含管理聊天应用程序中打开的选项卡和更新 UI 的代码。
你会发现,与应用程序的所有 UI 代码相比,我们的聊天扩展器和 QuantumGate 的实际代码非常少。这是因为 QuantumGate 在底层为我们完成了大部分繁重的工作。我们唯一需要做的就是定义我们将发送什么数据,接收什么数据,并处理这些事件。
我们的聊天扩展器
QuantumGate 的工作方式是,为了在我们的应用程序中使用它,我们需要为其编写一个自定义扩展器。您可以将扩展器视为一个插件,它将在 QuantumGate 之上运行并提供额外功能。有一个特定的 QuantumGate::Extender (文档) 类,我们在构建扩展器时需要继承它,并且根据我们想要做什么,我们需要为几个回调函数提供实现。所有这些函数都在 ChatExtender.* 文件中实现。类定义如下
class ChatExtender final : public QuantumGate::Extender
{
//...
};
我们扩展器的构造函数如下
ChatExtender::ChatExtender() :
QuantumGate::Extender(QuantumGate::ExtenderUUID(L"c055850e-2a88-f990-4e58-ad915552a375"),
QuantumGate::String(L"Chat Extender"))
{
//...
}
您会注意到,我们为基类提供了一个 UUID 和我们扩展器的名称。新的 UUID 可以通过 QuantumGate::UUID
类创建;在本例中,我们使用一个预生成的并将其传入。每个扩展器都应该有自己唯一的 ExtenderUUID
。
在 ChatExtender.cpp 中,您会找到以下回调函数的定义,其中大部分我们在本例中并未实际使用
bool OnStartup();
void OnPostStartup();
void OnPreShutdown();
void OnShutdown();
void OnPeerEvent(QuantumGate::Extender::PeerEvent&& event);
QuantumGate::Extender::PeerEvent::Result OnPeerMessage
(QuantumGate::Extender::PeerEvent&& event);
所提供的示例源代码中的注释更详细地介绍了每个回调函数。回调函数在我们的扩展器构造函数中注册到 QuantumGate,如下所示
// Add the callback functions for this extender; this can also be done
// in another function instead of the constructor, as long as you set the callbacks
// before adding the extender to the local instance
if (!SetStartupCallback(QuantumGate::MakeCallback(this, &ChatExtender::OnStartup)) ||
!SetPostStartupCallback(QuantumGate::MakeCallback(this, &ChatExtender::OnPostStartup)) ||
!SetPreShutdownCallback(QuantumGate::MakeCallback(this, &ChatExtender::OnPreShutdown)) ||
!SetShutdownCallback(QuantumGate::MakeCallback(this, &ChatExtender::OnShutdown)) ||
!SetPeerEventCallback(QuantumGate::MakeCallback(this, &ChatExtender::OnPeerEvent)) ||
!SetPeerMessageCallback(QuantumGate::MakeCallback(this, &ChatExtender::OnPeerMessage)))
{
throw std::exception("Failed to set one or more extender callbacks");
}
对我们来说重要的回调函数是 OnPeerEvent()
和 OnPeerMessage()
函数,我们在这里处理 QuantumGate 发送的对等连接、断开连接和消息事件。请务必记住,这些函数可以由多个线程调用,如果您从这些函数访问数据成员,您需要添加同步(例如互斥锁)以防止并发问题。在示例代码中,您将看到两个互斥锁用于访问对等数据和我们正在使用的昵称。
我们在 OnPeerEvent()
函数中跟踪连接和断开连接的对等体,并将它们添加到名为 m_Peers
的容器中。我们使用扩展器发送的消息在 ChatExtender.h 中定义如下
enum class MessageType : std::uint8_t
{
Unknown = 0,
NicknameChange,
PrivateChatMessage,
BroadcastChatMessage
};
这些是我们将在 OnPeerMessage()
函数中处理的消息。
我们还在扩展器中定义了一些自己的回调函数,这些函数由 MainPage
窗口设置,用于接收事件以更新 UI。这些是在 ChatExtender.h 中声明的以下函数
void OnPeerConnect(PeerConnectCallbackType&& cb) noexcept;
void OnPeerDisconnect(PeerDisconnectCallbackType&& cb) noexcept;
void OnPeerNicknameChanged(PeerNicknameChangeCallbackType&& cb) noexcept;
void OnPeerChatMessage(PeerChatMessageCallbackType&& cb) noexcept;
我们的扩展器在应用程序的 MainPage
的 MainPage::InitializeChatExtender()
成员函数中实例化和使用。在该函数中,我们添加了上述用于更新 UI 的回调。示例文件中该函数代码中的注释解释了每个回调的作用。
我们的本地 QuantumGate 实例
为了在我们的应用程序中使用 QuantumGate,我们必须定义并使用类型为 QuantumGate::Local
(文档) 的对象。这在 MainPage.h 头文件中作为代表我们主窗口的 MainPage
结构的数据成员完成。此对象在 MainPage::StartLocalInstance()
成员函数中初始化。包含的示例文件中的注释解释了正在发生的事情,但总而言之,我们配置了本地实例使用的身份
QuantumGate::StartupParameters params;
// Create a UUID for the local instance with matching keypair;
// normally you should do this once and save and reload the UUID
// and keys. The UUID and public key can be shared with other peers,
// while the private key should be protected and kept private.
{
auto [success, uuid, keys] = QuantumGate::UUID::Create(QuantumGate::UUID::Type::Peer,
QuantumGate::UUID::SignAlgorithm::EDDSA_ED25519);
if (success)
{
params.UUID = uuid;
params.Keys = std::move(*keys);
}
else
{
ShowErrorMessage(L"Failed to create peer UUID.");
return false;
}
}
如果我们创建一次此身份(UUID、公钥和私钥)并保存它,并将 UUID 和公钥分发给对等方,则此身份可用于身份验证。但对于这个简单的示例,我们每次都创建一个新身份。
然后我们设置支持的算法
params.SupportedAlgorithms.Hash = {
QuantumGate::Algorithm::Hash::BLAKE2B512
};
params.SupportedAlgorithms.PrimaryAsymmetric = {
QuantumGate::Algorithm::Asymmetric::ECDH_X25519
};
params.SupportedAlgorithms.SecondaryAsymmetric = {
QuantumGate::Algorithm::Asymmetric::KEM_NTRUPRIME
};
params.SupportedAlgorithms.Symmetric = {
QuantumGate::Algorithm::Symmetric::CHACHA20_POLY1305
};
params.SupportedAlgorithms.Compression = {
QuantumGate::Algorithm::Compression::ZSTANDARD
};
请注意,对于每种算法类别,我们可以添加更多(文档),QuantumGate 将根据两个对等体都支持的算法(必须有一些重叠)选择一个。但是在此代码中,我们只为每个类别提供一个。
然后我们告诉 QuantumGate 开始监听 TCP 端口 999
上的传入连接,并立即启动我们已添加的任何扩展器。
// Listen for incoming connections on startup
params.Listeners.TCP.Enable = true;
// Listen for incoming connections on these ports
params.Listeners.TCP.Ports = { 999 };
// Start extenders on startup
params.EnableExtenders = true;
然后我们还设置了本地实例的安全和访问设置
// For our purposes we disable authentication requirement; when
// authentication is required we would need to add peers to the instance
// via QuantumGate::Local::GetAccessManager().AddPeer() including their
// UUID and public key so that they can be authenticated when connecting
params.RequireAuthentication = false;
// For our purposes we allow access by default
m_Local.GetAccessManager().SetPeerAccessDefault
(QuantumGate::Access::PeerAccessDefault::Allowed);
// For our purposes we allow all IP addresses to connect;
// by default all IP Addresses are blocked
if (!m_Local.GetAccessManager().AddIPFilter
(L"0.0.0.0/0", QuantumGate::Access::IPFilterType::Allowed) ||
!m_Local.GetAccessManager().AddIPFilter
(L"::/0", QuantumGate::Access::IPFilterType::Allowed))
{
ShowErrorMessage(L"Failed to add an IP filter.");
return false;
}
我们将自定义扩展器添加到本地实例。我们只是在这里添加我们自己的扩展器,但如果需要,可以添加更多。
if (const auto result = m_Local.AddExtender(m_Extender); result.Failed())
{
ShowErrorMessage(L"Failed to add the ChatExtender to the QuantumGate local instance.");
return false;
}
最后启动本地实例
const auto result = m_Local.Startup(params);
if (result.Failed())
{
std::wstring str{ L"Failed to start the QuantumGate local instance
(" + result.GetErrorString() + L")." };
ShowErrorMessage(str.c_str());
return false;
}
本地实例成功启动后,我们就可以开始使用它来做一些事情,例如连接到其他对等体。
连接到对等体的操作在 MainPage::ConnectToPeer()
成员函数中处理。一旦本地实例启动,其他对等体也可以使用指定的监听端口连接到它。重要的是,网络上的任何路由器和机器上的防火墙(例如 Windows 防火墙)都配置为允许在该端口上进行传入和传出连接。
QuantumGate 提供了一个控制台窗口,其中显示日志消息,这可以极大地简化任何(连接)问题的故障排除。随附的聊天应用程序在主窗口上有一个“显示控制台”按钮,可让您打开控制台。如果您遇到连接问题,请打开窗口并检查消息以查找任何线索。另外值得一提的是,QuantumGate 允许您将控制台输出重定向到您自己的自定义类,您可以在其中将其保存到文件或在 UI 控件中显示。
安装预构建的示例
示例文件包含 ChatApp 示例的预构建版本,作为 Windows AppX 分发包。您只能将应用程序安装到 Microsoft Windows 10 版本 10.0.18362 或更高版本,这主要是由于 C++/WinRT 的要求。
为了运行应用程序,您必须使用提供的 Install.ps1 脚本在 Windows 上进行旁加载。如果您无法运行该脚本,您可能需要先配置 PowerShell,以允许在 PowerShell 命令行上使用以下命令运行脚本
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
在安装过程中,脚本还会要求您在 Windows 中启用开发人员模式,以便能够旁加载 Windows 商店应用程序。
从源代码构建示例
GitHub 上的 README 文件中提供了关于构建随附示例源代码并从 Visual Studio 运行它的说明。
使用示例 ChatApp
启动 ChatApp 后,您可以设置您的昵称,然后点击“上线”按钮启动本地实例。应用程序将监听来自对等体的传入连接。或者,您也可以点击“新建连接”选项卡,然后使用对等体的 IP 地址连接到对等体。您将在“连接”选项卡中看到所有已连接的对等体。在“广播”窗口中,您可以向所有已连接的对等体发送消息,或者您可以选择一个对等体并点击“私聊”按钮打开一个新的私聊窗口,只向该对等体发送消息。
延伸阅读
一旦您熟悉了本示例中提供的基本设置,就可以开始查看 QuantumGate 提供的更高级功能,例如提供掩盖流量(在 QuantumGate 中称为“噪声”)和使用中继以获得更好的安全性和隐私。GitHub 上的维基中提供了更多示例和教程,完整的 API 在 GitHub 上的维基中也有记录。您可以尝试将这些额外功能添加到 ChatApp,或者根据本示例中看到的内容构建一个全新且不同的应用程序。如有任何问题,请随时与我联系。
历史
- 2020 年 8 月 17 日:初始版本