Months ago, I came upon Javid's series of YouTube videos about building a portable framework for general purpose client/server applications in C++ with the use of Asio library. I watched the entire four videos but didn’t have the energy to complete the project myself. Then a few weeks ago, I decided to finally give it a go.
After toiling away with Asio and referring to Dmytro Radchuk’s Boost.Asio C++ Network Programming book, I finally completed a simplified build of the framework.
Intro
I’d like to start by talking about several things before going into the framework, those include Networking in C++, Asio and working with it, and some important concepts!
Networking 101
Computer networks and communication protocols greatly enhance the capabilities of a software by enabling various applications or distinct components of a single application to establish communication and collaborate towards a shared objective.
For two applications running on different computing devices to communicate with each other, a shared communication protocol must be established. While the applications developers have the option to create their own protocols, this is seldom done due to the complexity and time required. Instead, standardized protocols, already defined and implemented in popular operating systems, can be used. These protocols are defined by the TCP/IP standard. Don’t be fooled by the standard’s name; It’s not only TCP and IP but many more other protocols. In our case, we’ll only deal with what’s known as transport level protocols (such as TCP or UDP).
Let me stop here and say that networking on the whole is a huge complex subject, and I’m not interested in the low-level nature of it, meaning that, I won’t be digging into the details of how the TCP protocol works, or what is routing for instance. However, what I’m interested in is simply shifting data between two computers. If you’re worried about not having a working knowledge of how networks work, I’m telling you not to worry :)
Networking in C++
Networking in C++ is traditionally seen as quite tricky because there is no standard for it. On top of that, we have complex networking protocols. However, by adopting a library like Asio, we can reduce the complexity and minimize errors, ensuring that our programs will compile and work on different platforms.
Asio (Asynchronous Input Output)
Asio is an entirely header-based library for network and low-level I/O programming, it’s written in modern C++ approach and keep that in mind because the framework we’re building on top of it will also be written in a modern C++ way
Note:
Asio started as part of the Boost framework, but fortunately it has a Non-Boost version, so we don’t need the entirety of Boost to use it.
Throughout the development of this framework, I’ll be using Visual Studio.
The first thing we need to do is to actually include Asio for us to use, and since it’s header-based, there are no libraries to link to or anything to build. You can get Asio from its website and add it as an Include Directory in our project properties.
Now, that we can use Asio, why not start by making a simple program where we can connect to something on the internet and get some data, it’d be a good introduction to how we can get Asio up and running!
#define ASIO_STANDALONE
#include <asio.hpp>
You can see that I have included the Asio header file here, which represents the core of Asio. I specified that the header file should be used as a standalone, since I previously mentioned that Asio is also a part of the Boost framework. However, in this case, we specifically require the standalone version.
Note:
If you try to build the code now, you’ll notice a Windows-specific warning. This warning arises because each version of Windows has slightly different ways of handling networking. However, ignoring the warning won’t be an issue since it defaults to a suitable option when we run the program. Nevertheless, we can explicitly specify the Windows version we want to use.
#ifdef _WIN32
#define _WIN32_WINNT 0x0A00 // Windows 10
#endif
It’s generally recommended to enclose the definition of _WIN32_WINNT within #ifdef directives to ensure it is only applied when compiling for a Windows platform. This helps prevent issues when compiling the code on non-Windows systems.
In a client-server application, the client needs to know which server it wants to talk to. It does this by using something called an “endpoint”. Think of an endpoint as the address or location of the server application, or somewhere we wish to connect to.
int main()
{
// Serves as a mechanism for reporting and handling errors that may occur during asynchronous operations
asio::error_code ec;
asio::ip::tcp::endpoint ep(asio::ip::make_address("93.184.216.34", ec), 80);
std::cin.get();
return 0;
}
Here, I’ve created a specifically tcp-style endpoint, which is defined by an IP address and a port. You can see that I used asio::ip::make_address() to convert a string-based address into a valid ip::address object that can be understood by Asio and used in networking operations. For error handling, the variable ec will be used.
Note:
“93.184.216.34” is the IP address associated with the domain name “example.com.”. It is a reserved domain name used solely for illustrative purposes and not associated with any real website or server. Basic websites like this communicate on port 80.
You can use nslookup to check. It’s a command-line tool used for querying DNS (Domain Name System) servers to retrieve information about domain names, IP addresses, and other DNS records.
Now it’s a good time to talk about Sockets. A socket is an abstraction that represents an endpoint for communication between two devices over a network. Think of it an interface or hook into the operating system’s network drivers and will act as the doorway to the network that we’re connected to. In Asio, when we create a socket, we need to associate it with the asio context, in a way you can think of the context as an instance of asio that hides all the platform specific requirements.
int main()
{
// Serves as a mechanism for reporting and handling errors that may occur during asynchronous operations
asio::error_code ec;
asio::ip::tcp::endpoint ep(asio::ip::make_address("93.184.216.34", ec), 80);
// The platform specific interface
asio::io_context context;
asio::ip::tcp::socket socket(context);
// The socket will try to connect
socket.connect(ep, ec);
if (!ec)
std::cout << "Connected!" << std::endl;
else
std::cout << "Failed to connect to address:\n" << ec.message() << std::endl;
std::cin.get();
return 0;
}
If we run this program now, it’ll attempt to connect to the IP address provided, which is example.com.
Now, let’s try to make it connect to where I know there isn’t a working port 80, my own machine!
#include <iostream>
#ifdef _WIN32
#define _WIN32_WINNT 0x0A00 // Windows 10
#endif
#define ASIO_STANDALONE
#include <asio.hpp>
int main()
{
// Serves as a mechanism for reporting and handling errors that may occur during asynchronous operations
asio::error_code ec;
asio::ip::tcp::endpoint ep(asio::ip::make_address("127.0.0.1", ec), 80);
// The platform specific interface
asio::io_context context;
asio::ip::tcp::socket socket(context);
// The socket will try to connect
socket.connect(ep, ec);
if (!ec)
std::cout << "Connected!" << std::endl;
else
std::cout << "Failed to connect to address:\n" << ec.message() << std::endl;
std::cin.get();
return 0;
}
If you tried this yourself, you’ll notice that it took some time before returning the no connection message. Asio is very nice when it comes to returning descriptive error messages, and you’ll notice this a lot along the way :)
Returning to example.com, where a successful connection has been established, indicating that we have an active and live connection with the other endpoint and since I’m actually trying to connect to a website, the server at the other end is expecting an HTTP request.
if (socket.is_open())
{
std::string request =
"GET /index.html HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n\r\n";
socket.write_some(asio::buffer(request.data(), request.size()), ec);
}
The HTTP request here is a GET request sent to example.com for the file index.html. This request instructs the server to send the content of the index.html file and then close the connection. The sending is done via socket.write_some(), which, as the name implies, tries to send as much of this data as possible. It is worth noting that an asio::buffer is used because when working with Asio, reading and writing data is done with the help of buffers. An Asio buffer is essentially nothing but an array of bytes.
Now that we actually send our request, we should focus on receiving something back!
if (socket.is_open())
{
std::string request =
"GET /index.html HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n\r\n";
socket.write_some(asio::buffer(request.data(), request.size()), ec);
size_t bytes = socket.available();
std::cout << "Bytes Available: " << bytes << std::endl;
if (bytes > 0)
{
std::vector<char> Buffer(bytes);
socket.read_some(asio::buffer(Buffer.data(), Buffer.size()), ec);
}
for(auto c : Buffer)
std::cout << c;
}
First, we check if there are any bytes available from the socket for us to read. If there are, we read them into the Buffer vector, which is sized according to the amount of available bytes. Then, we call the equivalent socket_read_some() function, once again utilizing asio::buffer to wrap the standard vector. Lastly, we display the contents of this buffer in the console.
Well, look what we have here...
You may say that the server denied our request, and returned nothing, but that’s not true :)
The problem here is that the moment we sent our request to the server, we checked for available bytes. However, this approach is not efficient because the request requires time to be sent, processed, and returned. Consequently, there wasn’t enough time for the server to generate a response. You can verify this by debugging the code and executing it line by line. Then, you will certainly observe a response when enough time is given for the request and server!
To address this issue, we can attempt a brute-force delay to see if it helps. We can utilize chrono for this purpose. It’s important to note that hard-coded delays like this are generally not recommended. However, in our case, we are merely checking if it works or not.
#include <iostream>
#include <chrono>
#ifdef _WIN32
#define _WIN32_WINNT 0x0A00 // Windows 10
#endif
#define ASIO_STANDALONE
#include <asio.hpp>
int main()
{
// Serves as a mechanism for reporting and handling errors that may occur during asynchronous operations
asio::error_code ec;
asio::ip::tcp::endpoint ep(asio::ip::make_address("93.184.216.34", ec), 80);
// The platform specific interface
asio::io_context context;
asio::ip::tcp::socket socket(context);
// The socket will try to connect
socket.connect(ep, ec);
if (!ec)
std::cout << "Connected!" << std::endl;
else
std::cout << "Failed to connect to address:\n" << ec.message() << std::endl;
if (socket.is_open())
{
std::string request =
"GET /index.html HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n\r\n";
socket.write_some(asio::buffer(request.data(), request.size()), ec);
std::this_thread::sleep_for(std::chrono::milliseconds(300));
size_t bytes = socket.available();
std::cout << "Bytes Available: " << bytes << std::endl;
if (bytes > 0)
{
std::vector<char> Buffer(bytes);
socket.read_some(asio::buffer(Buffer.data(), Buffer.size()), ec);
for (auto c : Buffer)
std::cout << c;
}
}
std::cin.get();
return 0;
}
The duration of 300ms appears to be sufficient for the request to be sent, processed, and for the server to respond with the desired data!
The fact that we see our request being responded to, doesn’t change the fact that our program waits 300ms every time it does any kind of network transaction, but fear not, my dear friend, for Asio comes to our rescue in such scenarios!
if (socket.is_open())
{
std::string request =
"GET /index.html HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n\r\n";
socket.write_some(asio::buffer(request.data(), request.size()), ec);
socket.wait(socket.wait_read);
size_t bytes = socket.available();
std::cout << "Bytes Available: " << bytes << std::endl;
if (bytes > 0)
{
std::vector<char> Buffer(bytes);
socket.read_some(asio::buffer(Buffer.data(), Buffer.size()), ec);
for (auto c : Buffer)
std::cout << c;
}
}
The socket.wait(socket.wait_read) waits or blocks our program until there is available data to read. You can try and change the IP address to check a different website that can deliver more data!
Architecture
Now, we dive into the heart of our project - the architecture of the framework. This framework is specifically tailored to emphasize the development of client/server chat applications, while also remaining open to future enhancements that may extend its capabilities to support other types of client/server applications.
At the heart of the framework lies a message-based communication model. All data transactions within the framework are encapsulated into messages, each comprising a header that includes an identifier (ID) and size (in bytes), along with a variable-sized body. The header ensures structured communication, allowing the framework to process messages efficiently. The framework consists of several key components that work together to facilitate efficient and structured communication between clients and the server. Below, I present the SConnect framework’s architecture.
SConnect Framework:
- Provides a solid foundation for building client/server chat applications.
- Emphasis is placed on delivering real-time chat functionalities.
Chat Client (Client-side):
- Chat Client component represents the client-side of the application.
- Handles interactions with the server and other clients.
- Connects to the server and sends/receives chat messages.
- Encapsulates and formats data into messages for communication.
Chat Server (Server-side):
- Chat Server component represents the server-side of the application.
- Manages incoming client connections and maintains multiple client sessions concurrently.
- Handles client messages and broadcasts them to all connected clients in real-time.
Messages (Header-based):
- Each message consists of a header containing an ID and size (in bytes), along with a variable-sized body.
- Provides a well-defined format for data encapsulation and processing.
Networking Layer (Using Asio):
- The Networking Layer integrates Asio.
- Utilizes asynchronous, event-driven models to handle concurrency without explicit threading.
Building SConnect
Now that we explored how the architecture utilizes a message-based communication model, encapsulating data transactions into messages with structured headers, making it an efficient solution for client/server applications, we’ll dive into the actual development process.
First and foremost
I would like to begin by focusing on ensuring SConnect’s portability and cross-platform support. As mentioned earlier, the SConnect framework is designed to be a portable solution for client/server applications, with an emphasis on chat functionality. To achieve this portability, I’ll be using CMake as the orchestrator of my building process and Google Test for Unit Testing, while validating the functionality of SConnect across multiple platforms!
CMakeLists: The Blueprint for Portability
To make SConnect portable, we begin by crafting a well-structured CMakeLists.txt file -a blueprint that guides CMake in constructing the build process for our framework. Within this file, we specify the project name, the minimum required CMake version, and the C++ standard we’ll be using, ensuring compatibility across various compilers.
cmake_minimum_required (VERSION 3.8)
# Enable Hot Reload for MSVC compilers if supported.
if (POLICY CMP0141)
cmake_policy(SET CMP0141 NEW)
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<IF:$<AND:$<C_COMPILER_ID:MSVC>,$<CXX_COMPILER_ID:MSVC>>,$<$<CONFIG:Debug,RelWithDebInfo>:EditAndContinue>,$<$<CONFIG:Debug,RelWithDebInfo>:ProgramDatabase>>")
endif()
project ("SConnect")
set(SOURCE_FILES_SERVER
src/Client.cpp
src/Server.cpp
src/ServerTest.cpp
)
set(SOURCE_FILES_CLIENT
src/Client.cpp
src/Server.cpp
src/ClientTest.cpp
)
add_executable (Server ${SOURCE_FILES_SERVER})
add_executable (Client ${SOURCE_FILES_CLIENT})
if (CMAKE_VERSION VERSION_GREATER 3.12)
set_property(TARGET Server PROPERTY CXX_STANDARD 20)
set_property(TARGET Client PROPERTY CXX_STANDARD 20)
endif()
target_include_directories(Server PRIVATE "include")
target_include_directories(Client PRIVATE "include")
# Set Asio include directory
# Replace <path_to_asio> with the path where you have Asio installed or downloaded on your machine.
set(ASIO_INCLUDE_DIR "<path_to_asio>")
target_include_directories(Server PRIVATE ${ASIO_INCLUDE_DIR})
target_include_directories(Client PRIVATE ${ASIO_INCLUDE_DIR})
# Optionally set the build type (Debug or Release) explicitly
# Uncomment and use the following line if needed:
# set(CMAKE_BUILD_TYPE "Debug")
Note:
When developing a client-server application using the SConnect framework, it’s essential to test the functionality of both the server and clients. To achieve this, we can create multiple executables!
SConnect Framework
-
Message
As I said earlier, all data transactions in the SConnect framework are encapsulated into messages. The message structure consists of two fundamental components: the header and the body, which together ensure efficient and organized communication. The header contains an identifier that identifies the type or purpose of the message and size, which represents the total number of bytes in the entire message, including both the header and the body. On the other hand, the body holds the actual content being transmitted.
#pragma once
#include <cstdint>
#include <string>
namespace SConnect {
// Message structure for the chat application
struct ChatMessage {
// Message header
struct Header {
uint32_t message_id; // Identifier for the type of message
uint32_t body_size; // Size of the message body in bytes
};
Header header;
std::string body;
};
}
To achieve data transmission, the SConnect framework leverages the processes of serialization and deserialization. During serialization, the message’s header and body are converted into a linear stream of bytes, also known as a “byte stream.” This byte stream represents a compact and platform-independent representation of the message, making it suitable for transmission over a network or storage in a file. Upon receipt of the byte stream, deserialization comes into play. The deserialization process involves extracting the necessary information from the byte stream and reconstructing the original message on the receiving end. By properly interpreting the byte stream, the recipient can recreate the original message structure, including its header and body, in its entirety.
Note:
The message structure is fully defined in the header file, so there’s no need for a separate source file!
-
Client
The ChatClient class is a crucial component in the SConnect framework, representing a client that interacts with the server.
#pragma once
#include "SConnect.h"
#include "Message.h"
#include <memory>
namespace SConnect {
class ChatClient {
public:
ChatClient(asio::io_context& io_context, const std::string& server_address, const std::string& server_port);
// Connects to the server.
void Connect();
// Sends a chat message to the server.
void Send(const ChatMessage& message);
// Starts asynchronously receiving messages from the server.
void Receive();
// Handles an incoming message from the server.
void HandleIncomingMessage(const ChatMessage& message);
// Returns the socket associated with the ChatClient.
asio::ip::tcp::socket& GetSocket() {
return m_socket;
}
private:
asio::io_context& m_ioContext;
asio::ip::tcp::socket m_socket;
asio::ip::tcp::resolver m_resolver;
std::string m_serverAddress;
std::string m_serverPort;
std::vector<char> m_receiveBuffer;
void log(const std::string& message) const {
std::cout << "[ChatClient] " << message << std::endl;
}
};
}
We discussed io_context, and socket before, but the new thing is the resolver, which is used to resolve the server’s address and port information. It is responsible for converting human-readable server addresses (e.g., “localhost”) into their corresponding IP addresses, enabling successful connection to the server. Lastly, there’s the m_receiveBuffer which is the buffer used to receive incoming messages from the server. It temporarily holds the received bytes until the complete message is deserialized and processed.
#include "Client.h"
namespace SConnect {
// Constructor implementation
ChatClient::ChatClient(asio::io_context& io_context, const std::string& server_address, const std::string& server_port)
: m_ioContext(io_context), m_socket(io_context), m_resolver(io_context), m_serverAddress(server_address), m_serverPort(server_port) {
}
void ChatClient::Connect() {
asio::ip::tcp::resolver::results_type endpoints = m_resolver.resolve(m_serverAddress, m_serverPort);
asio::connect(m_socket, endpoints);
}
void ChatClient::Send(const ChatMessage& message) {
// Prepare the serialized message
std::vector<char> serialized_message;
serialized_message.resize(sizeof(ChatMessage::Header) + message.body.size());
// Copy the header into the serialized message
ChatMessage::Header header;
header.message_id = message.header.message_id;
header.body_size = static_cast<uint32_t>(message.body.size());
std::memcpy(serialized_message.data(), &header, sizeof(ChatMessage::Header));
// Copy the body into the serialized message
std::memcpy(serialized_message.data() + sizeof(ChatMessage::Header), message.body.data(), message.body.size());
// Send the serialized message over the socket
asio::write(m_socket, asio::buffer(serialized_message));
}
void ChatClient::Receive() {
m_receiveBuffer.resize(sizeof(ChatMessage::Header)); // Reserve space for the message header
asio::async_read(m_socket, asio::buffer(m_receiveBuffer),
[this](const asio::error_code& ec, std::size_t bytes_received) {
if (!ec) {
// Extract the header from the receive buffer
ChatMessage::Header header;
std::memcpy(&header, m_receiveBuffer.data(), sizeof(ChatMessage::Header));
// Resize the buffer to fit the entire message
m_receiveBuffer.resize(sizeof(ChatMessage::Header) + header.body_size);
// Asynchronously read the rest of the message (body)
asio::async_read(m_socket, asio::buffer(m_receiveBuffer.data() + sizeof(ChatMessage::Header), header.body_size),
[this, header](const asio::error_code& inner_ec, std::size_t bytes_received) {
if (!inner_ec) {
// Create a ChatMessage object from the received data
ChatMessage received_message;
received_message.header = header;
received_message.body.assign(m_receiveBuffer.begin() + sizeof(ChatMessage::Header), m_receiveBuffer.end());
// Handle the incoming message
HandleIncomingMessage(received_message);
// Continue receiving more messages
Receive();
}
});
}
});
}
void ChatClient::HandleIncomingMessage(const ChatMessage& message) {
std::cout << "Received: " << message.body << std::endl;
}
}
The Connect() method asynchronously establishes a connection to the server. The Send() transmits chat messages to the server. It takes a ChatMessage object as input, which contains the message’s content and metadata. The message is serialized into a byte stream and sent to the server using the TCP socket with asio::write(). Lastly, we have two important methods, Receive(), which starts asynchronously receiving messages from the server. As messages arrive, they are stored in the m_receiveBuffer, and the client’s HandleIncomingMessage() function is called to process each message.
Note:
Asio library is integrated in SConnect.h, to avoid repetitive inclusions in client and server components.
-
Server
The ChatServer class is also a fundamental component in the SConnect framework, representing the server that handles incoming client connections and message broadcasting.
#pragma once
#include "Client.h"
namespace SConnect {
class ChatServer {
public:
ChatServer(asio::io_context& io_context, const std::string& listen_port);
// Starts accepting client connections asynchronously.
void StartAccept();
// Handles a newly accepted client connection.
void HandleAccept(std::shared_ptr<ChatClient> client, const asio::error_code& error);
// Handles an incoming message from a client.
void HandleClientMessage(std::shared_ptr<ChatClient> client, const ChatMessage& message);
private:
asio::io_context& m_ioContext;
asio::ip::tcp::acceptor m_acceptor;
std::vector<std::shared_ptr<ChatClient>> m_clients;
void log(const std::string& message) const {
std::cout << "[ChatServer] " << message << std::endl;
}
};
}
Two things to focus on here, the first is the acceptor. The asio::ip::tcp::acceptor object represents the server’s TCP acceptor. It listens for incoming client connections and accepts them, establishing the communication channel with each client. Lastly, the m_clients, which represents a std::vector that holds shared pointers to ChatClient objects, representing the connected clients. It enables the server to keep track of all active clients and broadcast messages to them.
#include "Server.h"
namespace SConnect {
// Constructor implementation
ChatServer::ChatServer(asio::io_context& io_context, const std::string& listen_port)
: m_ioContext(io_context), m_acceptor(io_context, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), std::stoi(listen_port))) {
log("Server started. Listening on port: " + listen_port);
StartAccept();
}
void ChatServer::StartAccept() {
std::shared_ptr<ChatClient> new_client = std::make_shared<ChatClient>(m_ioContext, "", "");
m_acceptor.async_accept(new_client->GetSocket(), [this, new_client](const asio::error_code& ec) {
if (!ec) {
m_clients.push_back(new_client);
log("New client is connected.Total clients : " + std::to_string(m_clients.size()));
new_client->Receive(); // Start receiving messages from the client
}
StartAccept(); // Accept the next client connection
});
}
void ChatServer::HandleAccept(std::shared_ptr<ChatClient> client, const asio::error_code& error) {
// This function can be left empty for now, updates to the framework will take place later!
}
void ChatServer::HandleClientMessage(std::shared_ptr<ChatClient> client, const ChatMessage& message) {
// Broadcast the message to all connected clients
for (const auto& other_client : m_clients) {
if (other_client != client) {
other_client->Send(message);
}
}
}
}
The constructor here initializes the ChatServer object with the provided context and listening port. It sets up the TCP acceptor, preparing the server to accept incoming client connections. The StartAccept() asynchronously starts accepting client connections. As clients connect to the server, new ChatClient objects are created, and the connection process for each client is handled. Lastly, the HandleClientMessage() is responsible for processing incoming messages from each connected client. It takes a shared pointer to the client and a ChatMessage object as parameters, enabling the server to handle different message types based on their identifiers.
Test Chat Application
In this test chat application, I showcase the capabilities of the SConnect framework for building real-time client/server chat systems. SConnect is an efficient and scalable solution that leverages the Asio networking library to facilitate seamless communication between clients and the server.
The Server side of the chat application is implemented using the ChatServer class from the SConnect framework. This class listens for incoming client connections and handles the message broadcasting process. With simple yet effective code, the server is set up to start accepting connections on port, 12345.
#include "Server.h"
int main() {
try {
asio::io_context io_context;
SConnect::ChatServer server(io_context, "12345");
io_context.run();
}
catch (const std::exception& e) {
std::cerr << "Server exception: " << e.what() << std::endl;
}
return 0;
}
On the Client side, the ChatClient class handles establishing a connection with the server and sending messages. The client code is interactive, allowing users to type messages in the console and have them instantly sent to the server.
#include "Client.h"
int main() {
try {
asio::io_context io_context;
SConnect::ChatClient client(io_context, "localhost", "12345");
client.Connect();
// Send a message to the server
std::string message_text;
while (true) {
std::cout << "Send a message (type exit to terminate): ";
std::getline(std::cin, message_text); // Read user input from the console
// Check if the user wants to exit
if (message_text == "exit") {
break;
}
// Send the message to the server
SConnect::ChatMessage message;
message.body = message_text;
client.Send(message);
}
io_context.run();
}
catch (const std::exception& e) {
std::cerr << "Client exception: " << e.what() << std::endl;
}
return 0;
}
In Action!
Source Code
You can find all the code here.
Last Edited: July 12, 2023