Initial Commit

This commit is contained in:
Yousef Khadadeh 2025-07-06 20:43:53 +03:00
commit 6088fc29ed
978 changed files with 353647 additions and 0 deletions

View file

@ -0,0 +1,46 @@
#include "ByteStream.hpp"
#include <cereal/archives/portable_binary.hpp>
#include <cereal/types/string.hpp>
#include "NetLibrary.hpp"
ByteMemBuf::ByteMemBuf(char* p, size_t l) {
setg(p, p, p + l);
}
ByteMemStream::ByteMemStream(char* p, size_t l):
std::istream(&buffer),
buffer(p, l)
{
rdbuf(&buffer);
}
std::vector<std::shared_ptr<std::stringstream>> fragment_message(std::string_view v, uint64_t stride) {
if(v.length() <= stride)
return {};
uint64_t nextByte = 0;
std::vector<std::shared_ptr<std::stringstream>> toRet;
toRet.emplace_back(std::make_shared<std::stringstream>());
{
cereal::PortableBinaryOutputArchive m(*toRet.back());
m((MessageCommandType)0, (uint64_t)v.length(), cereal::binary_data(v.data() + nextByte, stride));
}
nextByte += stride;
for(;;) {
if((v.length() - nextByte) <= stride) {
toRet.emplace_back(std::make_shared<std::stringstream>());
{
cereal::PortableBinaryOutputArchive m(*toRet.back());
m((MessageCommandType)0, cereal::binary_data(v.data() + nextByte, (uint64_t)(v.length() - nextByte)));
}
break;
}
toRet.emplace_back(std::make_shared<std::stringstream>());
{
cereal::PortableBinaryOutputArchive m(*toRet.back());
m((MessageCommandType)0, cereal::binary_data(v.data() + nextByte, stride));
}
nextByte += stride;
}
return toRet;
}

View file

@ -0,0 +1,19 @@
#pragma once
#include <iostream>
#include <sstream>
#include <memory>
#include <vector>
// https://tuttlem.github.io/2014/08/18/getting-istream-to-work-off-a-byte-array.html
class ByteMemBuf : public std::basic_streambuf<char> {
public:
ByteMemBuf(char* p, size_t l);
};
class ByteMemStream : public std::istream {
public:
ByteMemStream(char* p, size_t l);
private:
ByteMemBuf buffer;
};
std::vector<std::shared_ptr<std::stringstream>> fragment_message(std::string_view v, uint64_t stride);

View file

@ -0,0 +1,165 @@
#include "NetClient.hpp"
#include "NetLibrary.hpp"
#include "NetServer.hpp"
#include <chrono>
#include <Helpers/Logger.hpp>
NetClient::NetClient(const std::string& serverFullID) {
receiveQueue = std::make_shared<ReceiveQueue>();
this->serverFullID = serverFullID;
lastMessageTime = std::chrono::steady_clock::now();
}
NetClient::NetClient(NetServer& server) {
receiveQueue = std::make_shared<ReceiveQueue>();
directConnectServer = &server;
server.directConnectClient = this;
server.directConnectClientData = std::make_shared<NetServer::ClientData>();
server.directConnectClientData->lastMessageTime = std::chrono::steady_clock::now();
server.clients.emplace_back(server.directConnectClientData);
lastMessageTime = std::chrono::steady_clock::now();
}
void NetClient::init_channel(const std::string& channelName, std::shared_ptr<rtc::DataChannel> channel) {
if(isDisconnected)
return;
auto commandChannelOnMessage = [channelName, rQueueWeak = make_weak_ptr(receiveQueue)](rtc::message_variant data) {
auto rQueue = rQueueWeak.lock();
if(rQueue) {
std::scoped_lock rQueueLock(rQueue->mut);
rQueue->messages.emplace(channelName, std::get<rtc::binary>(data));
}
};
channel->onMessage(commandChannelOnMessage);
channel->onClosed([cWeak = make_weak_ptr(shared_from_this())]() {
auto cLock = cWeak.lock();
if(cLock)
cLock->isDisconnected = true;
});
channels[channelName] = channel;
Logger::get().log("INFO", "Client channel setup: " + channelName);
lastMessageTime = std::chrono::steady_clock::now();
}
void NetClient::update() {
if(isDisconnected)
return;
if(std::chrono::steady_clock::now() - lastMessageTime >= NetLibrary::TIMEOUT_DURATION) {
Logger::get().log("INFO", "Connection timed out");
isDisconnected = true;
return;
}
parse_received_messages();
send_queued_messages();
}
void NetClient::parse_received_messages() {
std::scoped_lock rQueueLock(receiveQueue->mut);
while(!receiveQueue->messages.empty()) {
auto& receivedMessage = receiveQueue->messages.front();
lastMessageTime = std::chrono::steady_clock::now();
size_t messageOrderDisplacement = 0;
if(receivedMessage.channel == UNRELIABLE_COMMAND_CHANNEL || receivedMessage.channel == RELIABLE_COMMAND_CHANNEL) {
MessageOrder messageOrder = NetLibrary::get_message_order(receivedMessage.data);
if(nextMessageOrderToExpect > messageOrder && receivedMessage.channel == UNRELIABLE_COMMAND_CHANNEL) {
receiveQueue->messages.pop();
continue;
}
nextMessageOrderToExpect = std::max(messageOrder + 1, nextMessageOrderToExpect);
messageOrderDisplacement = sizeof(MessageOrder);
}
ByteMemStream strm((char*)(receivedMessage.data.data() + messageOrderDisplacement), receivedMessage.data.size() - messageOrderDisplacement);
cereal::PortableBinaryInputArchive inArchive(strm);
auto& spfm = pfm[receivedMessage.channel];
MessageCommandType commandID;
inArchive(commandID);
if(commandID == 0) {
if(spfm.partialFragmentMessage.size() == 0) {
spfm.partialFragmentMessageLoc = 0;
uint64_t messageSize;
inArchive(messageSize);
spfm.partialFragmentMessage.resize(messageSize);
inArchive(cereal::binary_data(spfm.partialFragmentMessage.data() + spfm.partialFragmentMessageLoc, NetLibrary::FRAGMENT_MESSAGE_STRIDE));
spfm.partialFragmentMessageLoc += NetLibrary::FRAGMENT_MESSAGE_STRIDE;
}
else if(spfm.partialFragmentMessage.size() - spfm.partialFragmentMessageLoc <= NetLibrary::FRAGMENT_MESSAGE_STRIDE) {
inArchive(cereal::binary_data(spfm.partialFragmentMessage.data() + spfm.partialFragmentMessageLoc, spfm.partialFragmentMessage.size() - spfm.partialFragmentMessageLoc));
ByteMemStream completeStrm(spfm.partialFragmentMessage.data(), spfm.partialFragmentMessage.size());
cereal::PortableBinaryInputArchive completeArchive(completeStrm);
MessageCommandType completeCommandID;
completeArchive(completeCommandID);
recvCallbacks[completeCommandID](completeArchive);
spfm.partialFragmentMessage.clear();
spfm.partialFragmentMessageLoc = 0;
}
else {
inArchive(cereal::binary_data(spfm.partialFragmentMessage.data() + spfm.partialFragmentMessageLoc, NetLibrary::FRAGMENT_MESSAGE_STRIDE));
spfm.partialFragmentMessageLoc += NetLibrary::FRAGMENT_MESSAGE_STRIDE;
}
}
else
recvCallbacks[commandID](inArchive);
receiveQueue->messages.pop();
}
}
void NetClient::send_queued_messages() {
for(auto& [channelName, messageQueue] : messageQueues) {
bool addMessageOrder = channelName == UNRELIABLE_COMMAND_CHANNEL || channelName == RELIABLE_COMMAND_CHANNEL;
if(directConnectServer) {
auto& c = directConnectServer->directConnectClientData;
std::scoped_lock rMessLock(c->receivedMessagesMutex);
while(!messageQueue.empty()) {
std::string toInsert;
if(addMessageOrder)
toInsert = NetLibrary::attach_message_order_and_update(nextMessageOrderToSend, messageQueue.front()->str());
else
toInsert = messageQueue.front()->str();
rtc::binary b((std::byte*)toInsert.c_str(), (std::byte*)(toInsert.c_str() + toInsert.length()));
c->receivedMessages.emplace(channelName, b);
messageQueue.pop();
}
}
else {
auto& channel = channels[channelName];
if(channel) {
if(channel->isOpen()) {
while(!messageQueue.empty()) {
#ifdef __EMSCRIPTEN__
if(channel->bufferedAmount() >= NetLibrary::MAX_BUFFERED_DATA_PER_CHANNEL)
break;
#endif
if(addMessageOrder)
send_str_as_bytes(channel, NetLibrary::attach_message_order_and_update(nextMessageOrderToSend, messageQueue.front()->str()));
else
send_str_as_bytes(channel, messageQueue.front()->str());
messageQueue.pop();
}
#ifdef __EMSCRIPTEN__
if(channel->bufferedAmount() >= NetLibrary::MAX_BUFFERED_DATA_PER_CHANNEL && channelName == UNRELIABLE_COMMAND_CHANNEL)
messageQueue = std::queue<std::shared_ptr<std::stringstream>>();
#endif
}
}
}
}
}
bool NetClient::is_disconnected() {
return isDisconnected;
}
void NetClient::send_str_as_bytes(std::shared_ptr<rtc::DataChannel> channel, const std::string& str) {
channel->send((const std::byte*)str.c_str(), str.size());
}
void NetClient::add_recv_callback(MessageCommandType commandID, const NetClientRecvCallback& callback) {
recvCallbacks[commandID] = callback;
}

View file

@ -0,0 +1,83 @@
#pragma once
#include <memory>
#include <string>
#include <rtc/rtc.hpp>
#include <cereal/cereal.hpp>
#include <unordered_map>
#include <functional>
#include <cereal/cereal.hpp>
#include <cereal/archives/portable_binary.hpp>
#include <queue>
#include "ByteStream.hpp"
#include "NetLibrary.hpp"
typedef std::function<void(cereal::PortableBinaryInputArchive&)> NetClientRecvCallback;
class NetClient : public std::enable_shared_from_this<NetClient> {
public:
NetClient(const std::string& serverFullID);
NetClient(NetServer& server);
template <typename... Args> void send_items_to_server(const std::string& channel, Args&&... items) {
if(isDisconnected)
return;
auto ss(std::make_shared<std::stringstream>());
{
cereal::PortableBinaryOutputArchive m(*ss);
(m(items), ...);
}
std::vector<std::shared_ptr<std::stringstream>> fragmentedMessage = fragment_message(ss->view(), NetLibrary::FRAGMENT_MESSAGE_STRIDE);
auto& messageQueue = messageQueues[channel];
if(fragmentedMessage.empty())
messageQueue.emplace(ss);
else if(channel != UNRELIABLE_COMMAND_CHANNEL) { // Just drop unreliable messages that are fragmented
for(auto& ss2 : fragmentedMessage)
messageQueue.emplace(ss2);
}
}
void update();
void add_recv_callback(MessageCommandType commandID, const NetClientRecvCallback& callback);
bool is_disconnected();
private:
std::string localID;
void init_channel(const std::string& channelName, std::shared_ptr<rtc::DataChannel> channel);
void parse_received_messages();
void send_queued_messages();
void send_str_as_bytes(std::shared_ptr<rtc::DataChannel> channel, const std::string& str);
std::unordered_map<std::string, std::queue<std::shared_ptr<std::stringstream>>> messageQueues;
struct ReceiveQueue {
std::mutex mut;
struct ReceivedMessage {
std::string channel;
rtc::binary data;
};
std::queue<ReceivedMessage> messages;
};
std::shared_ptr<ReceiveQueue> receiveQueue;
std::unordered_map<std::string, std::shared_ptr<rtc::DataChannel>> channels;
struct PartialFragmentMessage {
std::string partialFragmentMessage;
uint64_t partialFragmentMessageLoc = 0;
};
std::unordered_map<std::string, PartialFragmentMessage> pfm;
std::unordered_map<MessageCommandType, NetClientRecvCallback> recvCallbacks;
NetServer* directConnectServer = nullptr;
std::atomic<bool> isDisconnected = false;
MessageOrder nextMessageOrderToSend = 0;
MessageOrder nextMessageOrderToExpect = 0;
std::string serverFullID;
bool fullyRegistered = false;
std::chrono::steady_clock::time_point lastMessageTime;
friend class NetLibrary;
friend class NetServer;
};

View file

@ -0,0 +1,389 @@
#include "NetLibrary.hpp"
#include <fstream>
#include <nlohmann/json.hpp>
#include <rtc/peerconnection.hpp>
#include "../Logger.hpp"
#include <rtc/rtc.hpp>
#include <variant>
#include <vector>
#include <Helpers/Random.hpp>
#include "NetClient.hpp"
#include "NetServer.hpp"
#include <bit>
std::atomic<bool> NetLibrary::alreadyInitialized = false;
std::string NetLibrary::signalingAddr;
std::shared_ptr<rtc::WebSocket> NetLibrary::ws;
std::unordered_map<std::string, std::shared_ptr<NetLibrary::PeerData>> NetLibrary::peers;
std::string NetLibrary::globalID;
rtc::Configuration NetLibrary::config;
std::mutex NetLibrary::serverListMutex;
std::mutex NetLibrary::clientListMutex;
std::mutex NetLibrary::peerListMutex;
std::vector<std::weak_ptr<NetClient>> NetLibrary::clients;
std::vector<std::weak_ptr<NetServer>> NetLibrary::servers;
std::promise<void> NetLibrary::wsPromise;
void NetLibrary::init() {
if(alreadyInitialized)
return;
using json = nlohmann::json;
std::ifstream f("data/config/p2p.json");
std::vector<std::string> stunServerList;
std::vector<std::string> turnServerList;
rtc::InitLogger(rtc::LogLevel::Info, [](rtc::LogLevel level, std::string message) {
Logger::get().log("INFO", "[LibDataChannel Log Level " + std::to_string(static_cast<int>(level)) + "] " + message);
});
if(f.is_open()) {
try {
json j;
f >> j;
j.at("signalingServer").get_to<std::string>(signalingAddr);
std::vector<std::string> stunList = j.at("stunList").get<std::vector<std::string>>();
for(std::string& s : stunList) {
if(s.substr(0, 5).compare("stun:"))
s.insert(0, "stun:");
config.iceServers.emplace_back(s);
}
}
catch(...) {
throw std::runtime_error("[NetLibrary::init] Invalid config/p2p.json");
}
}
else
throw std::runtime_error("[NetLibrary::init] Could not open config/p2p.json");
f.close();
// Not sure why, but TLS verification fails on mac
// Should be fixed later, but for now this is fine, as the signaling server doesn't have
// critical information
#ifdef __APPLE__
rtc::WebSocket::Configuration wsConfig;
wsConfig.disableTlsVerification = true;
ws = std::make_shared<rtc::WebSocket>(wsConfig);
#else
ws = std::make_shared<rtc::WebSocket>();
#endif
auto wsFuture = wsPromise.get_future();
ws->onOpen([]() {
Logger::get().log("INFO", "Websocket connected, signaling ready");
wsPromise.set_value();
});
ws->onError([](std::string s) {
Logger::get().log("INFO", "Websocket error: " + s);
wsPromise.set_exception(std::make_exception_ptr(std::runtime_error(s)));
});
ws->onClosed([]() {
Logger::get().log("INFO", "Websocket closed");
destroy();
});
ws->onMessage([wws = make_weak_ptr(ws)](auto data) {
if(!std::holds_alternative<std::string>(data))
return;
json message = json::parse(std::get<std::string>(data));
auto itID = message.find("id");
if(itID == message.end())
return;
auto id = itID->get<std::string>();
auto itType = message.find("type");
auto type = itType->get<std::string>();
std::shared_ptr<rtc::PeerConnection> peerConnection;
{
std::scoped_lock peerListLock(peerListMutex);
auto peerIt = peers.find(id);
if(peerIt != peers.end())
peerConnection = peerIt->second->connection;
else if(type == "offer") {
Logger::get().log("INFO", "Websocket answering to " + id);
auto peer = setup_peer_connection(config, wws, id);
peers[id] = peer;
peerConnection = peer->connection;
}
}
if(!peerConnection)
return;
if(type == "offer" || type == "answer") {
auto sdp = message["description"].get<std::string>();
peerConnection->setRemoteDescription(rtc::Description(sdp, type));
}
else if(type == "candidate") {
auto sdp = message["candidate"].get<std::string>();
auto mid = message["mid"].get<std::string>();
peerConnection->addRemoteCandidate(rtc::Candidate(sdp, mid));
}
});
get_global_id();
Logger::get().log("INFO", "NetLibrary global id: " + globalID);
const std::string wsPrefix = signalingAddr.find("://") == std::string::npos ? "ws://" : "";
const std::string url = wsPrefix + signalingAddr + "/" + globalID;
Logger::get().log("INFO", "Websocket URL is: " + url);
ws->open(url);
Logger::get().log("INFO", "NetLibrary waiting for signaling to be connected...");
//wsFuture.get();
alreadyInitialized = true;
}
std::string NetLibrary::get_random_server_local_id() {
return Random::get().alphanumeric_str(LOCALID_LEN);
}
const std::string& NetLibrary::get_global_id() {
if(globalID.empty())
globalID = Random::get().alphanumeric_str(GLOBALID_LEN);
return globalID;
}
void NetLibrary::register_server(std::shared_ptr<NetServer> server) {
std::scoped_lock serverListLock(serverListMutex);
servers.emplace_back(server);
}
void NetLibrary::register_client(std::shared_ptr<NetClient> client) {
client->localID = Random::get().alphanumeric_str(LOCALID_LEN);
std::scoped_lock serverListLock(clientListMutex);
clients.emplace_back(client);
}
void NetLibrary::finish_client_registration(std::shared_ptr<NetClient> client) {
try {
std::string serverGlobalID = client->serverFullID.substr(0, GLOBALID_LEN);
std::string serverLocalID = client->serverFullID.substr(GLOBALID_LEN);
std::scoped_lock peerListLock(peerListMutex);
if(!peers.contains(serverGlobalID)) {
auto peer = setup_peer_connection(config, ws, serverGlobalID);
peers[serverGlobalID] = peer;
peer->channel = peer->connection->createDataChannel(MAIN_NETLIBRARY_CHANNEL);
setup_main_data_channel(peer, serverGlobalID);
}
peers[serverGlobalID]->messageQueue.emplace(client->localID + serverLocalID);
client->fullyRegistered = true;
}
catch(const std::exception& e) {
Logger::get().log("INFO", "Invalid client registration: " + std::string(e.what()));
client->isDisconnected = true;
}
}
void NetLibrary::update() {
{
std::scoped_lock clientListLock(clientListMutex);
std::erase_if(clients, [](auto& cWeak) {
auto cLock = cWeak.lock();
if(!cLock)
return true;
if(!cLock->fullyRegistered && ws->isOpen())
finish_client_registration(cLock);
return false;
});
}
{
std::scoped_lock serverListLock(serverListMutex);
std::erase_if(servers, [](auto& sWeak) {
return !sWeak.lock();
});
}
{
std::scoped_lock peerListLock(peerListMutex);
std::erase_if(peers, [](auto& a) {
auto& p = a.second;
rtc::PeerConnection::State s = p->connection->state();
if(s == rtc::PeerConnection::State::Disconnected || s == rtc::PeerConnection::State::Closed || s == rtc::PeerConnection::State::Failed)
return true;
if(p->channel) {
if(p->channel->isOpen()) {
while(!p->messageQueue.empty()) {
p->channel->send(p->messageQueue.front());
Logger::get().log("INFO", "Sent main channel message: " + p->messageQueue.front());
p->messageQueue.pop();
}
}
}
return false;
});
}
}
std::shared_ptr<NetLibrary::PeerData> NetLibrary::setup_peer_connection(const rtc::Configuration& config, std::weak_ptr<rtc::WebSocket> wws, std::string id) {
using json = nlohmann::json;
auto peer = std::make_shared<PeerData>();
peer->connection = std::make_shared<rtc::PeerConnection>(config);
peer->connection->onLocalDescription([wws, id](rtc::Description description) {
json message = {{"id", id},
{"type", description.typeString()},
{"description", std::string(description)}};
if (auto ws = wws.lock())
ws->send(message.dump());
});
peer->connection->onLocalCandidate([wws, id](rtc::Candidate candidate) {
json message = {{"id", id},
{"type", "candidate"},
{"candidate", std::string(candidate)},
{"mid", candidate.mid()}};
if (auto ws = wws.lock())
ws->send(message.dump());
});
peer->connection->onDataChannel([id, peerWeak = make_weak_ptr(peer)](std::shared_ptr<rtc::DataChannel> dc) {
auto peer = peerWeak.lock();
if(peer) {
if(!peer->channel) {
if(dc->label() == MAIN_NETLIBRARY_CHANNEL) {
peer->channel = dc;
setup_main_data_channel(peer, id);
}
else {
Logger::get().log("INFO", "Client connected with incorrect first channel name: " + dc->label() + ". Could be from a different version of the program");
dc->send("REFUSE");
peer->connection->close();
}
}
else
channel_created_in_client(dc);
}
});
return peer;
}
void NetLibrary::setup_main_data_channel(std::shared_ptr<PeerData> peer, const std::string& id) {
peer->channel->onOpen([id]() {
Logger::get().log("INFO", "Main data channel has opened on " + id);
});
peer->channel->onClosed([id]() {
Logger::get().log("INFO", "Main data channel has closed on " + id);
});
peer->channel->onMessage([id, pConWeak = make_weak_ptr(peer->connection)](auto data) {
if(!std::holds_alternative<std::string>(data)) { // Main channel messages will only carry text
Logger::get().log("INFO", "Main data channel received binary message, which is not expected");
return;
}
std::string message = std::get<std::string>(data);
if(message == "REFUSE") {
Logger::get().log("USERINFO", "Refused connection. Could be on a different version");
auto pConLock = pConWeak.lock();
if(pConLock)
pConLock->close();
}
else {
std::string clientLocalID = message.substr(0, LOCALID_LEN);
std::string serverLocalID = message.substr(LOCALID_LEN);
Logger::get().log("INFO", "Main data channel received message " + message);
auto pConLock = pConWeak.lock();
if(pConLock)
assign_client_connection_to_server(serverLocalID, clientLocalID, pConLock);
}
});
}
void NetLibrary::assign_client_connection_to_server(const std::string& serverLocalID, const std::string& clientLocalID, std::shared_ptr<rtc::PeerConnection> connection) {
std::scoped_lock serverListLock(serverListMutex);
auto serverIt = std::find_if(servers.begin(), servers.end(), [&serverLocalID](auto& s) {
auto sLock = s.lock();
if(!sLock)
return false;
return sLock->localID == serverLocalID;
});
if(serverIt != servers.end()) {
auto sLock = serverIt->lock();
if(sLock)
sLock->client_connected(connection, clientLocalID);
}
}
void NetLibrary::channel_created_in_client(std::shared_ptr<rtc::DataChannel> channel) {
std::string clientID = channel->label().substr(0, LOCALID_LEN);
std::string channelName = channel->label().substr(LOCALID_LEN);
std::scoped_lock clientListLock(clientListMutex);
auto clientIt = std::find_if(clients.begin(), clients.end(), [&clientID](auto& c) {
auto cLock = c.lock();
if(!cLock)
return false;
return cLock->localID == clientID;
});
if(clientIt != clients.end()) {
auto cLock = clientIt->lock();
if(cLock)
cLock->init_channel(channelName, channel);
}
}
std::string NetLibrary::attach_message_order_and_update(MessageOrder& order, const std::string& message) {
union {
MessageOrder o;
char b[sizeof(MessageOrder)];
} u;
u.o = (std::endian::native == std::endian::little) ? std::byteswap(order) : order;
order++;
return std::string(u.b, sizeof(MessageOrder)) + message;
}
MessageOrder NetLibrary::get_message_order(rtc::binary& message) {
union {
MessageOrder o;
char b[sizeof(MessageOrder)];
} u;
std::memcpy(u.b, message.data(), sizeof(MessageOrder));
if(std::endian::native == std::endian::little)
u.o = std::byteswap(u.o);
return u.o;
}
void NetLibrary::destroy() {
ws = nullptr;
{
std::scoped_lock peerListLock(peerListMutex);
peers.clear();
}
{
std::scoped_lock clientListLock(clientListMutex);
for(auto& c : clients) {
auto cLock = c.lock();
if(cLock)
cLock->isDisconnected = true;
}
clients.clear();
}
{
std::scoped_lock serverListLock(serverListMutex);
for(auto& s : servers) {
auto sLock = s.lock();
if(sLock)
sLock->isDisconnected = true;
}
servers.clear();
}
wsPromise = std::promise<void>();
//globalID.clear(); // Do we need to do this? (Causes problems when global ID changes from the user's side after they copy a lobby address
alreadyInitialized = false;
}

View file

@ -0,0 +1,73 @@
#pragma once
#include <string>
#include <unordered_map>
#include <rtc/rtc.hpp>
#include <queue>
typedef uint64_t MessageOrder;
class NetClient;
class NetServer;
template <class T> std::weak_ptr<T> make_weak_ptr(std::shared_ptr<T> ptr) { return ptr; }
#define RELIABLE_COMMAND_CHANNEL "reliableCommand"
#define UNRELIABLE_COMMAND_CHANNEL "unreliableCommand"
#define RESOURCE_COMMAND_CHANNEL "resourceCommand"
#define MAIN_NETLIBRARY_CHANNEL "main00001"
typedef uint8_t MessageCommandType;
class NetLibrary {
public:
static void init();
static void update();
static std::string get_random_server_local_id();
static const std::string& get_global_id();
static void register_server(std::shared_ptr<NetServer> server);
static void register_client(std::shared_ptr<NetClient> client);
static void destroy();
static constexpr size_t LOCALID_LEN = 5;
static constexpr size_t GLOBALID_LEN = 20;
private:
static constexpr size_t FRAGMENT_MESSAGE_STRIDE = 4096;
static constexpr size_t MAX_BUFFERED_DATA_PER_CHANNEL = 65500;
static constexpr std::chrono::seconds TIMEOUT_DURATION = std::chrono::seconds(15);
static void finish_client_registration(std::shared_ptr<NetClient> client);
static void assign_client_connection_to_server(const std::string& serverLocalID, const std::string& clientLocalID, std::shared_ptr<rtc::PeerConnection> connection);
static void channel_created_in_client(std::shared_ptr<rtc::DataChannel> channel);
static std::string attach_message_order_and_update(MessageOrder& order, const std::string& message);
static MessageOrder get_message_order(rtc::binary& message);
struct PeerData {
std::shared_ptr<rtc::PeerConnection> connection;
std::shared_ptr<rtc::DataChannel> channel;
std::queue<std::string> messageQueue;
};
static void setup_main_data_channel(std::shared_ptr<PeerData> peer, const std::string& id);
static std::shared_ptr<PeerData> setup_peer_connection(const rtc::Configuration& config, std::weak_ptr<rtc::WebSocket> wws, std::string id);
static std::atomic<bool> alreadyInitialized;
static std::string signalingAddr;
static std::shared_ptr<rtc::WebSocket> ws;
static std::unordered_map<std::string, std::shared_ptr<PeerData>> peers;
static std::string globalID;
static rtc::Configuration config;
static std::mutex clientListMutex;
static std::mutex serverListMutex;
static std::mutex peerListMutex;
static std::vector<std::weak_ptr<NetClient>> clients;
static std::vector<std::weak_ptr<NetServer>> servers;
friend class NetClient;
friend class NetServer;
static std::promise<void> wsPromise;
};

View file

@ -0,0 +1,201 @@
#include "NetServer.hpp"
#include "NetLibrary.hpp"
#include <chrono>
#include <rtc/peerconnection.hpp>
#include <Helpers/Random.hpp>
#include <Helpers/Logger.hpp>
#include "NetClient.hpp"
NetServer::NetServer(const std::string& serverLocalID) {
localID = serverLocalID;
Logger::get().log("INFO", "[NetServer::NetServer] Server has local id: " + serverLocalID);
}
void NetServer::update() {
if(isDisconnected)
return;
std::erase_if(clients, [&](auto& client) {
bool timedOut = std::chrono::steady_clock::now() - client->lastMessageTime > NetLibrary::TIMEOUT_DURATION;
if(client->setToDisconnect || timedOut) {
if(disconnectCallback)
disconnectCallback(*client);
if(timedOut)
Logger::get().log("INFO", "Connection timed out");
return true;
}
client->parse_received_messages(*this);
client->send_queued_messages(*this);
return false;
});
std::scoped_lock clientAddLock(clientsToAddMutex);
clients.insert(clients.begin(), clientsToAdd.begin(), clientsToAdd.end());
clientsToAdd.clear();
}
bool NetServer::is_disconnected() {
return isDisconnected;
}
void NetServer::client_connected(std::shared_ptr<rtc::PeerConnection> connection, const std::string& clientLocalID) {
if(isDisconnected)
return;
auto newClient = std::make_shared<ClientData>();
newClient->lastMessageTime = std::chrono::steady_clock::now();
{
std::scoped_lock clientAddLock(clientsToAddMutex);
clientsToAdd.emplace_back(newClient);
}
Logger::get().log("INFO", "Client connected and creating channels for: " + clientLocalID);
newClient->channels.emplace(RELIABLE_COMMAND_CHANNEL, connection->createDataChannel(clientLocalID + RELIABLE_COMMAND_CHANNEL));
newClient->channels.emplace(RESOURCE_COMMAND_CHANNEL, connection->createDataChannel(clientLocalID + RESOURCE_COMMAND_CHANNEL));
rtc::DataChannelInit unreliableCommandInit;
unreliableCommandInit.reliability.maxPacketLifeTime = std::chrono::milliseconds(2000);
unreliableCommandInit.reliability.unordered = true;
newClient->channels.emplace(UNRELIABLE_COMMAND_CHANNEL, connection->createDataChannel(clientLocalID + UNRELIABLE_COMMAND_CHANNEL, unreliableCommandInit));
newClient->channels[RELIABLE_COMMAND_CHANNEL]->onMessage([weakClient = make_weak_ptr(newClient)](rtc::message_variant data) {
std::shared_ptr<ClientData> clientLock = weakClient.lock();
if(clientLock) {
std::scoped_lock recvMessLock(clientLock->receivedMessagesMutex);
clientLock->receivedMessages.emplace(RELIABLE_COMMAND_CHANNEL, std::get<rtc::binary>(data));
}
});
// If any of the local channels are closed, we can assume the client has disconnected
newClient->channels[RELIABLE_COMMAND_CHANNEL]->onClosed([weakClient = make_weak_ptr(newClient)]() {
std::shared_ptr<ClientData> clientLock = weakClient.lock();
if(clientLock)
clientLock->setToDisconnect = true;
});
newClient->channels[UNRELIABLE_COMMAND_CHANNEL]->onMessage([weakClient = make_weak_ptr(newClient)](rtc::message_variant data) {
std::shared_ptr<ClientData> clientLock = weakClient.lock();
if(clientLock) {
std::scoped_lock recvMessLock(clientLock->receivedMessagesMutex);
clientLock->receivedMessages.emplace(UNRELIABLE_COMMAND_CHANNEL, std::get<rtc::binary>(data));
}
});
newClient->channels[RESOURCE_COMMAND_CHANNEL]->onMessage([weakClient = make_weak_ptr(newClient)](rtc::message_variant data) {
std::shared_ptr<ClientData> clientLock = weakClient.lock();
if(clientLock) {
std::scoped_lock recvMessLock(clientLock->receivedMessagesMutex);
clientLock->receivedMessages.emplace(RESOURCE_COMMAND_CHANNEL, std::get<rtc::binary>(data));
}
});
}
void NetServer::ClientData::send_queued_messages(NetServer& server) {
for(auto& [channelName, messageQueue] : messageQueues) {
bool addMessageOrder = channelName == UNRELIABLE_COMMAND_CHANNEL || channelName == RELIABLE_COMMAND_CHANNEL;
if(this == server.directConnectClientData.get()) {
NetClient* c = server.directConnectClient;
std::scoped_lock recvQueueLock(c->receiveQueue->mut);
while(!messageQueue.empty()) {
std::string toInsert;
if(addMessageOrder)
toInsert = NetLibrary::attach_message_order_and_update(nextMessageOrderToSend, messageQueue.front()->str());
else
toInsert = messageQueue.front()->str();
rtc::binary b((std::byte*)toInsert.c_str(), (std::byte*)(toInsert.c_str() + toInsert.length()));
c->receiveQueue->messages.emplace(channelName, b);
messageQueue.pop();
}
}
else {
auto& channel = channels[channelName];
if(channel) {
if(channel->isOpen()) {
while(!messageQueue.empty()) {
#ifdef __EMSCRIPTEN__
if(channel->bufferedAmount() >= NetLibrary::MAX_BUFFERED_DATA_PER_CHANNEL)
break;
#endif
if(addMessageOrder)
send_str_as_bytes(channel, NetLibrary::attach_message_order_and_update(nextMessageOrderToSend, messageQueue.front()->str()));
else
send_str_as_bytes(channel, messageQueue.front()->str());
messageQueue.pop();
}
#ifdef __EMSCRIPTEN__
if(channel->bufferedAmount() >= NetLibrary::MAX_BUFFERED_DATA_PER_CHANNEL && channelName == UNRELIABLE_COMMAND_CHANNEL)
messageQueue = std::queue<std::shared_ptr<std::stringstream>>();
#endif
}
}
}
}
}
void NetServer::ClientData::send_str_as_bytes(std::shared_ptr<rtc::DataChannel> channel, const std::string& str) {
channel->send((const std::byte*)str.c_str(), str.size());
}
void NetServer::ClientData::parse_received_messages(NetServer& server) {
std::scoped_lock recvMessLock(receivedMessagesMutex);
while(!receivedMessages.empty()) {
auto& receivedMessage = receivedMessages.front();
lastMessageTime = std::chrono::steady_clock::now();
size_t messageOrderDisplacement = 0;
if(receivedMessage.channel == UNRELIABLE_COMMAND_CHANNEL || receivedMessage.channel == RELIABLE_COMMAND_CHANNEL) {
MessageOrder messageOrder = NetLibrary::get_message_order(receivedMessage.data);
if(nextMessageOrderToExpect > messageOrder && receivedMessage.channel == UNRELIABLE_COMMAND_CHANNEL) {
receivedMessages.pop();
continue;
}
nextMessageOrderToExpect = std::max(messageOrder + 1, nextMessageOrderToExpect);
messageOrderDisplacement = sizeof(MessageOrder);
}
ByteMemStream strm((char*)(receivedMessage.data.data() + messageOrderDisplacement), receivedMessage.data.size() - messageOrderDisplacement);
cereal::PortableBinaryInputArchive inArchive(strm);
auto& spfm = pfm[receivedMessage.channel];
MessageCommandType commandID;
inArchive(commandID);
if(commandID == 0) {
if(spfm.partialFragmentMessage.size() == 0) {
spfm.partialFragmentMessageLoc = 0;
uint64_t messageSize;
inArchive(messageSize);
spfm.partialFragmentMessage.resize(messageSize);
inArchive(cereal::binary_data(spfm.partialFragmentMessage.data() + spfm.partialFragmentMessageLoc, NetLibrary::FRAGMENT_MESSAGE_STRIDE));
spfm.partialFragmentMessageLoc += NetLibrary::FRAGMENT_MESSAGE_STRIDE;
}
else if(spfm.partialFragmentMessage.size() - spfm.partialFragmentMessageLoc <= NetLibrary::FRAGMENT_MESSAGE_STRIDE) {
inArchive(cereal::binary_data(spfm.partialFragmentMessage.data() + spfm.partialFragmentMessageLoc, spfm.partialFragmentMessage.size() - spfm.partialFragmentMessageLoc));
ByteMemStream completeStrm(spfm.partialFragmentMessage.data(), spfm.partialFragmentMessage.size());
cereal::PortableBinaryInputArchive completeArchive(completeStrm);
MessageCommandType completeCommandID;
completeArchive(completeCommandID);
server.recvCallbacks[completeCommandID](*this, completeArchive);
spfm.partialFragmentMessage.clear();
spfm.partialFragmentMessageLoc = 0;
}
else {
inArchive(cereal::binary_data(spfm.partialFragmentMessage.data() + spfm.partialFragmentMessageLoc, NetLibrary::FRAGMENT_MESSAGE_STRIDE));
spfm.partialFragmentMessageLoc += NetLibrary::FRAGMENT_MESSAGE_STRIDE;
}
}
else
server.recvCallbacks[commandID](*this, inArchive);
receivedMessages.pop();
}
}
void NetServer::add_recv_callback(MessageCommandType commandID, const NetServerRecvCallback& callback) {
recvCallbacks[commandID] = callback;
}
void NetServer::add_disconnect_callback(const NetServerDisconnectCallback& callback) {
disconnectCallback = callback;
}
NetServer::~NetServer() {
if(directConnectClient)
directConnectClient->isDisconnected = true;
}

View file

@ -0,0 +1,134 @@
#pragma once
#include <memory>
#include <string>
#include <rtc/rtc.hpp>
#include <mutex>
#include <unordered_map>
#include <cereal/archives/portable_binary.hpp>
#include <cereal/cereal.hpp>
#include "ByteStream.hpp"
#include "NetLibrary.hpp"
class NetServer : public std::enable_shared_from_this<NetServer> {
public:
struct ClientData;
typedef std::function<void(ClientData&, cereal::PortableBinaryInputArchive&)> NetServerRecvCallback;
typedef std::function<void(ClientData&)> NetServerConnectCallback;
typedef std::function<void(ClientData&)> NetServerDisconnectCallback;
struct ClientData {
std::unordered_map<std::string, std::queue<std::shared_ptr<std::stringstream>>> messages;
struct PartialFragmentMessage {
std::string partialFragmentMessage;
uint64_t partialFragmentMessageLoc = 0;
};
std::unordered_map<std::string, PartialFragmentMessage> pfm;
MessageOrder nextMessageOrderToSend = 0;
MessageOrder nextMessageOrderToExpect = 0;
std::unordered_map<std::string, std::shared_ptr<rtc::DataChannel>> channels;
std::mutex receivedMessagesMutex;
struct ReceivedMessage {
std::string channel;
rtc::binary data;
};
std::queue<ReceivedMessage> receivedMessages;
std::unordered_map<std::string, std::queue<std::shared_ptr<std::stringstream>>> messageQueues;
uint64_t customID;
void send_queued_messages(NetServer& server);
void parse_received_messages(NetServer& server);
void send_str_as_bytes(std::shared_ptr<rtc::DataChannel> channel, const std::string& str);
std::atomic<bool> setToDisconnect = false;
std::chrono::steady_clock::time_point lastMessageTime;
friend class NetServer;
friend class NetClient;
};
NetServer(const std::string& serverLocalID);
~NetServer();
void update();
template <typename... Args> void send_items_to_client(ClientData& client, const std::string& channel, Args&&... items) {
auto ss(std::make_shared<std::stringstream>());
{
cereal::PortableBinaryOutputArchive m(*ss);
(m(items), ...);
}
std::vector<std::shared_ptr<std::stringstream>> fragmentedMessage = fragment_message(ss->view(), NetLibrary::FRAGMENT_MESSAGE_STRIDE);
auto& messageQueue = client.messageQueues[channel];
if(fragmentedMessage.empty())
messageQueue.emplace(ss);
else if(channel != UNRELIABLE_COMMAND_CHANNEL) { // Just drop unreliable messages that are fragmented
for(auto& ss2 : fragmentedMessage)
messageQueue.emplace(ss2);
}
}
template <typename... Args> void send_items_to_all_clients(const std::string& channel, Args&&... items) {
send_items_to_client_if([&](const ClientData& c) {
return true;
}, channel, items...);
}
template <typename... Args> void send_items_to_all_clients_except(ClientData& client, const std::string& channel, Args&&... items) {
send_items_to_client_if([&](const ClientData& c) {
return (&c) != (&client);
}, channel, items...);
}
template <typename... Args> void send_items_to_client_if(std::function<bool(const ClientData&)> clientChecker, const std::string& channel, Args&&... items) {
auto ss(std::make_shared<std::stringstream>());
{
cereal::PortableBinaryOutputArchive m(*ss);
(m(items), ...);
}
std::vector<std::shared_ptr<std::stringstream>> fragmentedMessage = fragment_message(ss->view(), NetLibrary::FRAGMENT_MESSAGE_STRIDE);
for(auto& client : clients) {
if(clientChecker(*client)) {
auto& messageQueue = client->messageQueues[channel];
if(fragmentedMessage.empty())
messageQueue.emplace(ss);
else if(channel != UNRELIABLE_COMMAND_CHANNEL) { // Just drop unreliable messages that are fragmented
for(auto& ss2 : fragmentedMessage)
messageQueue.emplace(ss2);
}
}
}
}
bool is_disconnected();
void add_recv_callback(MessageCommandType commandID, const NetServerRecvCallback& callback);
void add_disconnect_callback(const NetServerDisconnectCallback& callback);
private:
void parse_received_messages();
void send_queued_messages();
void client_connected(std::shared_ptr<rtc::PeerConnection> connection, const std::string& clientLocalID);
std::atomic<bool> isDisconnected = false;
std::string localID;
std::mutex clientList;
std::vector<std::shared_ptr<ClientData>> clients;
// Adding an add list, because i dont want to block the entire clients vector with a mutex
std::mutex clientsToAddMutex;
std::vector<std::shared_ptr<ClientData>> clientsToAdd;
std::unordered_map<MessageCommandType, NetServerRecvCallback> recvCallbacks;
NetServerDisconnectCallback disconnectCallback;
NetClient* directConnectClient = nullptr;
std::shared_ptr<ClientData> directConnectClientData;
friend class NetLibrary;
friend class NetClient;
};