Skip to content

Building a Chat Application using the QuantumGate P2P Networking library

Karel Donk edited this page Oct 29, 2021 · 4 revisions

Screenshot

Introduction

In this tutorial we'll take a look at a sample chat application that demonstrates how to start using the QuantumGate peer-to-peer networking library. The sample application can be found in its own repo on GitHub named "QuantumGate-ChatApp-Sample". You can download the sample code from there and also download pre-built binaries from the releases page to try it out without manually having to build it.

Creating the project

For this chat application I've chosen to use the Microsoft C++/WinRT framework for the UI. You can download the C++/WinRT extension for Visual Studio from the marketplace. The extension adds some additional C++ project templates in Visual Studio. For this tutorial I used Visual Studio 2019, and chose the Blank App (C++/WinRT) template while creating a new solution.

Screenshot

After you've gone through the "Create a new project" wizard, you will also need to install the Microsoft.Windows.CppWinRT NuGet package in Visual Studio. You'll also need the QuantumGate library; you can build from source if you want, but you can also download pre-built binaries from the releases page on GitHub, which include the .lib and .dll files that you will link to. In that case you can only use the source code for the required header files in order to use the QuantumGate API.

You'll now need to specify in the project properties (right click on the project name in Visual Studio and choose Properties) where the compiler can look for the QuantumGate header file and library files. This can be done in the VC++ Directories section as seen in the screenshot below. At the Include Directories you will need to add the QuantumgateLib\API subfolder that comes with QuantumGate so that the compiler will find the QuantumGate.h header file located there. And at the Library Directories you will need to specify the location of the QuantumGate*.lib files that you will link to. Note in the screenshot that variables have been used in the path for the Configuration and Platform subfolder names so that the compiler will choose the right one based on the configuration and platform you're compiling for.

Screenshot

Since QuantumGate requires C++20 features as of this writing, you'll also need to configure your project to use the latest C++ features. This can be done under the C++ Language Standard option as seen in the below screenshot. You'll need to specify "Features from the latest C++ working draft (/std:c++latest)".

Screenshot

Next you'll have to actually include the QuantumGate header and library files in your project. There are various ways of doing this, but in this project I've chosen to add it to the pch.h precompiled header file that Visual Studio generated for the project. You'll need to add the following piece of code in that file:

// 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

Apart from including the header file we also include the library file based on the configuration (Debug or Release) and platform (Win32 or Win64).

Now that things are set up we can continue to actually build the application.

Chat application

If you download the sample code for the chat application, you will find the following files of specific importance:

  • MainPage.xaml: Generated by the Visual Studio wizard, this file contains the UI for the main window. It has been customized with the UI for the chat application.
  • MainPage.cpp and MainPage.h, generated by the Visual Studio wizard, which contain the code behind the UI defined in the previous .xaml file.
  • ChatExtender.cpp and ChatExtender.h which contain the code for our custom QuantumGate extender that will implement the specific chat functionality we need on top of QuantumGate.
  • ChatTab.cpp and ChatTab.h which contain the code for managing tabs that we have open in the chat application and updating the UI. You'll find that the actual code needed for our chat extender and the use of QuantumGate is very little compared to all the UI code for the application. This is because QuantumGate does most of the heavy lifting for us under the hood. About the only work we have to do is define what data we will send and what data we will receive and handle those events.

Our Chat Extender

The way QuantumGate works is that in order to make use of it in our application we'll need to write a custom extender for it. You can view an extender as a plugin that will run on top of QuantumGate and provide additional functionality. There's a specific QuantumGate::Extender class that we need to derive from when building our extender and we need to provide implementations for a few callback functions depending on what we want to do. All of these functions are implemented in the ChatExtender.* files. The class definition is as follows:

class ChatExtender final : public QuantumGate::Extender
{
    //...
};

The constructor for our extender is as follows:

ChatExtender::ChatExtender() :
    QuantumGate::Extender(QuantumGate::ExtenderUUID(L"c055850e-2a88-f990-4e58-ad915552a375"),
                          QuantumGate::String(L"Chat Extender"))
{
    //...
}

You'll note that we provide a UUID and a name for our extender to the base class. New UUIDs can be created via the QuantumGate::UUID class; in this case we're using a pre-generated one and passing it in. Every extender should have its own unique ExtenderUUID.

In ChatExtender.cpp you'll find definitions for the following callback functions, most of which we don't actually use in this case:

bool OnStartup();
void OnPostStartup();
void OnPreShutdown();
void OnShutdown();
void OnPeerEvent(QuantumGate::Extender::PeerEvent&& event);
QuantumGate::Extender::PeerEvent::Result OnPeerMessage(QuantumGate::Extender::PeerEvent&& event);

The comments in the provided sample source code go into much more detail about each callback function. The callback functions are registered with QuantumGate in the constructor of our extender as follows:

// 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");
}

The callback functions of importance to us are the OnPeerEvent() and OnPeerMessage() functions, where we handle peer connection, disconnection and message events sent by QuantumGate. It's important to keep in mind that these functions can be called by multiple threads and in case you access data members from these functions, you will need to add synchronization (such as mutexes) to prevent concurrency issues. In the sample code you will see two mutexes in use to access peer data and the nickname we're using.

We keep track of connecting and disconnecting peers in the OnPeerEvent() function and add them to a container named m_Peers. The messages we send using our extender are defined as follows in ChatExtender.h:

enum class MessageType : std::uint8_t
{
    Unknown = 0,
    NicknameChange,
    PrivateChatMessage,
    BroadcastChatMessage
};

These are the messages we handle in the OnPeerMessage() function.

We also define some of our own callback functions in the extender which get set by the MainPage window to receive events in order to update the UI. These are the following functions declared in ChatExtender.h:

void OnPeerConnect(PeerConnectCallbackType&& cb) noexcept;
void OnPeerDisconnect(PeerDisconnectCallbackType&& cb) noexcept;
void OnPeerNicknameChanged(PeerNicknameChangeCallbackType&& cb) noexcept;
void OnPeerChatMessage(PeerChatMessageCallbackType&& cb) noexcept;

Our extender is instantiated and used in the MainPage of our application in the MainPage::InitializeChatExtender() member function. In that function we add the above mentioned callbacks for updating the UI. The comments in the code for that function in the sample file explain what each callback does.

Our local QuantumGate instance

In order to use QuantumGate in our application we have to define and use an object of type QuantumGate::Local. This is done in the MainPage.h header file as a data member of the MainPage struct which represents our main window. This object gets initialized in the MainPage::StartLocalInstance() member function. The comments in the included sample files explain what's going on, but in summary, we configure the identity used by the local instance:

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;
    }
}

This identity (the UUID, public key and private key) can be used for authentication purposes. If we were to create it once and save it, we could distribute the UUID along with only the public key to peers to uniquely identify the user (the private key should stay private). But for this simple example we just create a new identity every time.

Then we set the supported algorithms:

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
};

Note that for each category of algorithms we can add more and QuantumGate will select one based on what both peers support (there has to be some overlap). However in this code we only provide one for each category.

We then tell QuantumGate to start listening for incoming connections on TCP port 999 and immediately start any extenders that we have added.

// 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;

Then we also set the security and access settings for the local instance:

// 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;
}

We add our custom extender to the local instance. We just add our own extender here, but it's possible to add more if needed.

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;
}

And finally start the local instance:

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;
}

After the local instance has successfully started we can start using it for things like making connections to other peers.

Making connections to peers is handled in the MainPage::ConnectToPeer() member function. As soon as the local instance has started, other peers can also connect to it using the specified listener port. It's important that any routers on the network and firewalls on the machine (Windows firewall for example) are configured to allow incoming and outgoing connections on that port.

QuantumGate provides a console window where log messages are displayed that can make it much easier to troubleshoot any (connection) problems. The included chat application has a "Show Console" button on the main window that lets you open the console. If you're having trouble connecting, open the window and check the messages for any clues. It's also good to mention that QuantumGate allows you to redirect console output to your own custom class where you can save it to a file or display it in a UI control.

Installing a pre-built sample

The GitHub repo includes a pre-built version of the ChatApp sample as a Windows AppX distribution package on the releases page. Follow the instructions there for downloading and installing the sample application to try it out.

Building the sample from source

There are instructions in the README file on GitHub for building the included sample source code and running it from Visual Studio.

Using the sample ChatApp

After starting the ChatApp you can set your nickname and then click on the "Go Online" button to start the local instance. The app will then listen for incoming connections from peers. Alternatively you can also click on the "New Connection" tab and connect to a peer using their IP address. You'll see all connected peers in the "Connections" tab. In the "Broadcast" window you can send messages to all connected peers, or you can select a peer and click the "Private Chat" button to open a new private chat window and only send messages to that peer.

Screenshot

Further reading

Once you're familiar with the basic setup provided in this sample, you can start to look at the more advanced features offered by QuantumGate, such as providing cover traffic (called "Noise" in QuantumGate) and using relays for better security and privacy. There are more examples and tutorials available in the QuantumGate wiki on GitHub and the complete API is also documented there. You can try adding these extra features to the ChatApp or go on to build a completely new and different application based on what you've seen in this sample.

Clone this wiki locally