feat: implemented full connection protocol (both)

This commit is contained in:
Sara Gerretsen 2025-10-12 18:00:37 +02:00
parent acb7351ea7
commit 8086924141
12 changed files with 268 additions and 73 deletions

View file

@ -0,0 +1,72 @@
[gd_scene load_steps=2 format=3 uid="uid://bbvpj46frv0ho"]
[sub_resource type="GDScript" id="GDScript_78ugl"]
script/source = "extends ClientNode
@onready
var client_status := %ClientStatus
func _ready():
connect_to_server(\"localhost\")
func _on_connection_changed(connected: int) -> void:
if (connected == NetworkData.CONNECTION_DISCONNECTED):
client_status.text = \"DISCONNECTED\"
elif (connected == NetworkData.CONNECTION_CONNECTED):
client_status.text = \"CONNECTED\"
elif (connected == NetworkData.CONNECTION_AUTHENTICATED):
client_status.text = \"AUTHENTICATED\"
"
[node name="Control" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="ServerNode" type="ServerNode" parent="."]
[node name="ClientNode" type="ClientNode" parent="."]
script = SubResource("GDScript_78ugl")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="ServerInfo" type="PanelContainer" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Header" type="Label" parent="HBoxContainer/ServerInfo"]
layout_mode = 2
text = "Server: OFF"
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/ServerInfo/Header"]
layout_mode = 0
offset_top = -312.0
offset_right = 574.0
offset_bottom = 336.0
[node name="ClientInfo" type="PanelContainer" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/ClientInfo"]
layout_mode = 2
[node name="Header" type="Label" parent="HBoxContainer/ClientInfo/VBoxContainer"]
layout_mode = 2
text = "Client:"
[node name="ClientStatus" type="Label" parent="HBoxContainer/ClientInfo/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "DISCONNECTED"
[connection signal="connection_changed" from="ClientNode" to="ClientNode" method="_on_connection_changed"]

View file

@ -11,6 +11,6 @@ config_version=5
[application]
config/name="you_done_it"
run/main_scene="uid://dosb4sb7pvss4"
run/main_scene="uid://bbvpj46frv0ho"
config/features=PackedStringArray("4.5", "Forward Plus")
config/icon="res://icon.svg"

View file

@ -1,8 +1,19 @@
#include "client_node.h"
#include "ydi_client.h"
String const ClientNode::sig_connection_changed{ "connection_changed" };
void ClientNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("connect_to_server"), &self_type::connect_to_server);
ClassDB::bind_method(D_METHOD("connect_to_server", "server_url"), &self_type::connect_to_server);
ADD_SIGNAL(MethodInfo(sig_connection_changed, PropertyInfo(Variant::INT, "connected", PROPERTY_HINT_ENUM, "Disconnected,Connected,Authenticated")));
}
void ClientNode::process() {
NetworkData::ConnectionStatus const new_status{ ydi::client::status() };
if (new_status != this->state) {
this->state = new_status;
emit_signal(sig_connection_changed, new_status);
}
}
void ClientNode::exit_tree() {
@ -14,6 +25,12 @@ void ClientNode::_notification(int what) {
return;
}
switch (what) {
case NOTIFICATION_ENTER_TREE:
set_process(true);
return;
case NOTIFICATION_PROCESS:
process();
return;
case NOTIFICATION_EXIT_TREE:
exit_tree();
return;

View file

@ -1,10 +1,12 @@
#pragma once
#include "ydi_networking.h"
#include <scene/main/node.h>
class ClientNode : Node {
GDCLASS(ClientNode, Node);
static void _bind_methods();
void process();
void exit_tree();
protected:
@ -12,4 +14,10 @@ protected:
public:
void connect_to_server(String const &url);
private:
NetworkData::ConnectionStatus state{ NetworkData::CONNECTION_DISCONNECTED };
public:
static String const sig_connection_changed;
};

View file

@ -2,9 +2,16 @@
#include "ydi_server.h"
#include <core/config/engine.h>
String const ServerNode::sig_clue_revealed{ "clue_revealed" };
String const ServerNode::sig_connection_established{ "connection_established" };
String const ServerNode::sig_connection_lost{ "connection_lost" };
void ServerNode::_bind_methods() {
ADD_SIGNAL(MethodInfo("new_clue", PropertyInfo(Variant::INT, "id")));
ADD_SIGNAL(MethodInfo(sig_clue_revealed, PropertyInfo(Variant::INT, "id")));
ADD_SIGNAL(MethodInfo(sig_connection_established));
ADD_SIGNAL(MethodInfo(sig_connection_lost));
}
void ServerNode::enter_tree() {
ydi::server::open();
}
@ -13,9 +20,14 @@ void ServerNode::process(double delta) {
Vector<NetworkData::ClueID> new_clues{};
if (ydi::server::receive::new_clues(new_clues)) {
for (NetworkData::ClueID clue : new_clues) {
emit_signal("new_clue", clue);
emit_signal(sig_clue_revealed, clue);
}
}
bool new_is_connected{ ydi::server::has_client() };
if (this->is_connected != new_is_connected) {
this->is_connected = new_is_connected;
emit_signal(this->is_connected ? sig_connection_established : sig_connection_lost);
}
}
void ServerNode::exit_tree() {
@ -28,6 +40,7 @@ void ServerNode::_notification(int what) {
}
switch (what) {
case NOTIFICATION_ENTER_TREE:
set_process(true);
enter_tree();
return;
case NOTIFICATION_PROCESS:

View file

@ -11,4 +11,12 @@ class ServerNode : public Node {
protected:
void _notification(int what);
public:
static String const sig_clue_revealed;
static String const sig_connection_established;
static String const sig_connection_lost;
private:
bool is_connected{ false };
};

View file

@ -15,7 +15,7 @@ struct Connection {
std::optional<zmq::socket_t> socket{ std::nullopt };
std::recursive_mutex mtx;
std::atomic<NetworkData::ConnectionStatus> status;
std::atomic<bool> stop_threads;
std::atomic<bool> stop_threads{ false };
};
std::optional<Connection> connection{ std::nullopt };
@ -25,40 +25,54 @@ std::optional<std::thread> receive_thread{ std::nullopt };
void handle_ok(zmq::multipart_t const &message) {
NetworkData::MessageType type{ to_message_type(message[1]) };
switch (type) {
default: // no need to handle every OK, just some relevant ones
return;
case NetworkData::MSG_CONNECT:
connection->status = NetworkData::CONNECTION_AUTHENTICATED;
return;
default: // no need to handle every OK, just some relevant ones
return;
}
}
void handle_message(zmq::multipart_t const &message) {
print_line("Client handle_message:");
print_message_contents(message);
NetworkData::MessageType type{ to_message_type(message[0]) };
switch (type) {
case NetworkData::MSG_OK:
print_line("Client: received OK");
handle_ok(message);
return;
case NetworkData::MSG_HEART:
print_line("Client: Received HEART, sending BEAT");
multipart(NetworkData::MSG_BEAT).send(*connection->socket);
return;
default:
print_error(vformat("Client: Received unhandled message: ", type, message[0].to_string().c_str()));
print_line("Client: Message not handled");
return;
}
}
void receive_thread_entry() {
{
std::scoped_lock lock{ connection->mtx };
multipart(NetworkData::MSG_CONNECT).send(*connection->socket);
}
zmq::multipart_t message{};
while (!connection->stop_threads) {
using namespace std::chrono_literals;
std::this_thread::sleep_for(10ms);
std::scoped_lock lock{ connection->mtx };
if (message.recv(*connection->socket)) {
if (message.recv(*connection->socket, (int)zmq::recv_flags::dontwait)) {
handle_message(message);
}
}
}
void connect(String const &url) {
if (connection) {
print_line("Client: Detected attempt to open duplicate client connection, exiting without action");
return;
}
connection.emplace();
print_line("Client: Connecting to ", url);
try {
@ -94,12 +108,14 @@ void connect(String const &url) {
print_line("Client: Failed to connect to server");
}
print_line("Client: connected to server");
connection->status = NetworkData::CONNECTION_CONNECTED;
receive_thread.emplace(receive_thread_entry);
print_line("Client: Connection complete!");
}
void disconnect() {
if (connection) {
std::scoped_lock lock{ connection->mtx };
connection->stop_threads = true;
if (receive_thread && receive_thread->joinable()) {
receive_thread->join();
}
@ -114,4 +130,15 @@ void disconnect() {
}
connection.reset();
}
NetworkData::ConnectionStatus status() {
return connection->status;
}
namespace send {
void reveal_clue(NetworkData::ClueID id) {
std::scoped_lock lock{ connection->mtx };
multipart(NetworkData::MSG_REVEAL, id).send(*connection->socket);
}
} //namespace send
} //namespace ydi::client

View file

@ -6,6 +6,7 @@
namespace ydi::client {
void connect(String const &url);
void disconnect();
NetworkData::ConnectionStatus status();
namespace send {
void reveal_clue(NetworkData::ClueID id);
} //namespace send

View file

@ -1,45 +1,50 @@
#include "ydi_networking.h"
#include "core/string/print_string.h"
#include <core/core_bind.h>
#include <core/object/class_db.h>
MAKE_TYPE_INFO(NetworkData::ClueID, Variant::INT);
MAKE_TYPE_INFO(NetworkData::ConnectionStatus, Variant::INT);
void NetworkData::_bind_methods() {
BIND_ENUM_CONSTANT(CLUE_FIRST);
BIND_ENUM_CONSTANT(CLUE_SECOND);
BIND_ENUM_CONSTANT(CLUE_MAX);
BIND_ENUM_CONSTANT(CONNECTION_DISCONNECTED);
BIND_ENUM_CONSTANT(CONNECTION_CONNECTED);
BIND_ENUM_CONSTANT(CONNECTION_AUTHENTICATED);
}
namespace ydi {
NetworkData::MessageType to_message_type(zmq::message_t const &msg) {
int as_int{ std::stoi(msg.str()) };
if (as_int >= 0 && as_int < NetworkData::MSG_INVALID) {
return (NetworkData::MessageType)as_int;
} else {
return NetworkData::MSG_INVALID;
int to_int(zmq::message_t const &msg, int failure) {
try {
return std::stoi(msg.to_string());
} catch (...) {
return failure;
}
}
NetworkData::MessageType to_message_type(zmq::message_t const &msg) {
return (NetworkData::MessageType)to_int(msg, (int)NetworkData::MSG_INVALID);
}
NetworkData::NOKReason to_nok_reason(zmq::message_t const &msg) {
int as_int{ std::stoi(msg.str()) };
if (as_int >= 0 && as_int < NetworkData::NOK_REASON_INVALID) {
return (NetworkData::NOKReason)as_int;
} else {
return NetworkData::NOK_REASON_INVALID;
}
return (NetworkData::NOKReason)to_int(msg, (int)NetworkData::NOK_INVALID_REASON);
}
NetworkData::ClueID to_clue_id(zmq::message_t const &msg) {
int as_int{ std::stoi(msg.str()) };
if (as_int >= 0 && as_int < NetworkData::CLUE_MAX) {
return (NetworkData::ClueID)as_int;
} else {
return NetworkData::CLUE_MAX;
return (NetworkData::ClueID)to_int(msg, NetworkData::CLUE_MAX);
}
void print_message_contents(zmq::multipart_t const &mpart) {
print_line("multipart message:");
for (zmq::message_t const &msg : mpart) {
print_line(" - ", msg.to_string().c_str());
}
}
void extend_multipart(zmq::multipart_t &mpart, NetworkData::MessageType type) {
mpart.addstr(std::to_string(type));
mpart.addstr(std::to_string((int)type));
}
void extend_multipart(zmq::multipart_t &mpart, NetworkData::ClueID id) {
@ -67,5 +72,15 @@ void extend_multipart(zmq::multipart_t &mpart, zmq::multipart_t const &right) {
mpart.append(right.clone());
}
void extend_multipart(zmq::multipart_t &mpart) {}
void extend_multipart(zmq::multipart_t &msg, std::pair<zmq::multipart_t::iterator, zmq::multipart_t::iterator> range) {
for (; range.first != range.second; ++range.first) {
msg.addstr(range.first->to_string());
}
}
void extend_multipart(zmq::multipart_t &msg, std::pair<zmq::multipart_t::const_iterator, zmq::multipart_t::const_iterator> range) {
for (; range.first != range.second; ++range.first) {
msg.addstr(range.first->to_string());
}
}
} //namespace ydi

View file

@ -41,15 +41,19 @@ public:
enum NOKReason {
NOK_UNAUTHENTICATED,
NOK_UNKNOWN_MSG,
NOK_REASON_INVALID //!< this means the value could not be parsed as a NOK reason, not that the reason is an invalid message.INVALID
NOK_OUT_OF_CONTEXT, //!< this means a received message is not valid in the recipient's current context
NOK_INVALID_REASON //!< this means the value could not be parsed as a NOK reason, not that the reason is an invalid message.
};
};
namespace ydi {
int to_int(zmq::message_t const &msg, int failure = 0);
NetworkData::MessageType to_message_type(zmq::message_t const &msg);
NetworkData::NOKReason to_nok_reason(zmq::message_t const &msg);
NetworkData::ClueID to_clue_id(zmq::message_t const &msg);
void print_message_contents(zmq::multipart_t const &mpart);
void extend_multipart(zmq::multipart_t &mpart, NetworkData::MessageType type);
void extend_multipart(zmq::multipart_t &mpart, NetworkData::ClueID type);
@ -59,11 +63,18 @@ void extend_multipart(zmq::multipart_t &mpart, char const *cstr);
void extend_multipart(zmq::multipart_t &mpart, int const &arg);
void extend_multipart(zmq::multipart_t &mpart, zmq::multipart_t const &right);
void extend_multipart(zmq::multipart_t &mpart);
void extend_multipart(zmq::multipart_t &mpart, std::pair<zmq::multipart_t::iterator, zmq::multipart_t::iterator> range);
void extend_multipart(zmq::multipart_t &mpart, std::pair<zmq::multipart_t::const_iterator, zmq::multipart_t::const_iterator> range);
template <typename TArg>
void extend_multipart_r(zmq::multipart_t &mpart, TArg const &arg) {
extend_multipart(mpart, arg);
}
template <typename TArg, typename... TArgs>
void extend_multipart(zmq::multipart_t &mpart, TArg const &arg, TArgs const &...args) {
void extend_multipart_r(zmq::multipart_t &mpart, TArg const &arg, TArgs const &...args) {
extend_multipart(mpart, arg);
extend_multipart(mpart, args...);
extend_multipart_r(mpart, args...);
}
template <typename TArg>
@ -76,7 +87,7 @@ zmq::multipart_t multipart(TArg const &arg) {
template <typename TArg, typename... TArgs>
zmq::multipart_t multipart(TArg const &arg, TArgs const &...args) {
zmq::multipart_t mpart{ multipart(arg) };
extend_multipart(mpart, args...);
extend_multipart_r(mpart, args...);
return mpart;
}
} //namespace ydi

View file

@ -33,42 +33,17 @@ void handle_reveal_clue(zmq::multipart_t const &message) {
}
void handle_authorised_message(std::string_view const &sender, NetworkData::MessageType type, zmq::multipart_t &message) {
if (type == NetworkData::MSG_BEAT) {
service->lastBeat = Time::get_singleton()->get_unix_time_from_system();
} else if (type == NetworkData::MSG_REVEAL) {
handle_reveal_clue(message);
} else {
multipart(sender, NetworkData::MSG_NOK, NetworkData::NOK_UNKNOWN_MSG, message).send(*service->socket);
}
}
void handle_message(zmq::multipart_t &message) {
std::string_view const sender{ message.at(0).to_string_view() };
NetworkData::MessageType type{ to_message_type(message.at(1)) };
std::scoped_lock lock{ service->mtx };
if (service->client) {
if (sender == service->client) {
handle_authorised_message(sender, type, message);
} else {
multipart(sender, "NOK", "UNAUTHORIZED_REQUEST").send(*service->socket);
}
} else if (type == NetworkData::MSG_CONNECT) {
service->client.emplace(sender);
multipart(sender, NetworkData::MSG_OK, message).send(*service->socket);
} else {
multipart(sender, NetworkData::MSG_NOK, "UNAUTHORIZED_REQUEST", message).send(*service->socket);
}
}
void receive_thread_entry() {
using namespace std::chrono_literals;
zmq::multipart_t incoming{};
while (service->stop_threads) {
std::this_thread::sleep_for(20ms);
std::scoped_lock lock{ service->mtx };
if (incoming.recv(*service->socket)) {
handle_message(incoming);
}
switch (type) {
default:
print_line("Server: Encountered unknown message type, sending NOK_UNKNOWN_MSG");
multipart(sender, NetworkData::MSG_NOK, NetworkData::NOK_UNKNOWN_MSG, message).send(*service->socket);
return;
case NetworkData::MSG_BEAT:
service->lastBeat = Time::get_singleton()->get_unix_time_from_system();
return;
case NetworkData::MSG_REVEAL:
handle_reveal_clue(message);
return;
}
}
@ -80,16 +55,53 @@ void ping_thread_entry() {
return;
}
}
static zmq::multipart_t ping{ multipart(*service->client, "HEART") };
while (!service->stop_threads) {
std::this_thread::sleep_for(1s);
std::scoped_lock lock{ service->mtx };
ping.send(*service->socket);
print_line("Server: Send HEART");
multipart(*service->client, NetworkData::MSG_HEART).send(*service->socket);
service->lastHeart = Time::get_singleton()->get_unix_time_from_system();
}
}
void handle_message(zmq::multipart_t &message) {
print_line("Server handle_message:");
print_message_contents(message);
std::string_view const sender{ message.at(0).to_string_view() };
NetworkData::MessageType type{ to_message_type(message.at(1)) };
std::scoped_lock lock{ service->mtx };
if (service->client) {
if (sender == service->client) {
handle_authorised_message(sender, type, message);
} else {
multipart(sender, NetworkData::MSG_NOK, NetworkData::NOK_UNAUTHENTICATED, std::pair{ message.begin() + 1, message.end() }).send(*service->socket);
}
} else if (type == NetworkData::MSG_CONNECT) {
service->client.emplace(sender);
multipart(sender, NetworkData::MSG_OK, std::pair{ message.begin() + 1, message.end() }).send(*service->socket);
ping_thread.emplace(ping_thread_entry);
} else {
multipart(sender, NetworkData::MSG_NOK, NetworkData::NOK_OUT_OF_CONTEXT, std::pair{ message.begin() + 1, message.end() }).send(*service->socket);
}
}
void receive_thread_entry() {
using namespace std::chrono_literals;
zmq::multipart_t incoming{};
while (!service->stop_threads) {
std::this_thread::sleep_for(20ms);
std::scoped_lock lock{ service->mtx };
if (incoming.recv(*service->socket, (int)zmq::recv_flags::dontwait)) {
handle_message(incoming);
}
}
}
void open() {
if (service) {
print_error("Server: Detected attempt to open duplicate Server, exiting without action.");
return;
}
print_line("Server: Starting");
service.emplace();
try {
@ -123,12 +135,13 @@ void open() {
}
print_line("Server: Bound socket");
receive_thread.emplace(receive_thread_entry);
assert(receive_thread->joinable());
print_line("Server: Startup complete!");
}
void close() {
if (service) {
print_line("Server: Shutting down...");
std::scoped_lock lock{ service->mtx };
service->stop_threads = true;
print_line("Server: Stopping Threads...");
if (receive_thread && receive_thread->joinable()) {
@ -154,10 +167,19 @@ void close() {
}
}
bool has_client() {
if (service) {
std::scoped_lock lock{ service->mtx };
return service->client.has_value();
} else {
return false;
}
}
namespace receive {
bool new_clues(Vector<NetworkData::ClueID> &out) {
std::scoped_lock lock{ service->mtx };
bool has_new{ !service->new_clues.is_empty() };
bool const has_new{ !service->new_clues.is_empty() };
if (has_new) {
out.append_array(service->new_clues);
service->new_clues.clear();

View file

@ -10,6 +10,7 @@
namespace ydi::server {
void open();
void close();
bool has_client();
namespace receive {
bool new_clues(Vector<NetworkData::ClueID> &out);