#1 - quicr module

This commit is contained in:
Martin Slachta
2026-07-22 17:34:44 +02:00
parent a04f0dc262
commit f4174eb0c7
177 changed files with 5309 additions and 2265 deletions
@@ -26,8 +26,8 @@ target_link_libraries(${PROJECT_NAME}
PUBLIC
tw::network
tl::expected
tw::messaging
tw::protocol
tw::message_protocol
protobuf::libprotobuf
tw::chat::lib
spdlog::spdlog
@@ -1,16 +1,20 @@
#pragma once
#include "MessageSession.hpp"
#include "ProtobufMessages.hpp"
#include "SendChatMessage.hpp"
#include "ChatClientError.hpp"
#include "models/ChatMessage.hpp"
#include "message_protocol/MessageEndpoint.hpp"
#include <functional>
#include <memory>
#include <string>
namespace tw::chat {
class ChatClient {
tw::MessageSession m_session;
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
msg::MessageConnection* m_server;
ProtobufMessages m_messages;
std::function<void(ChatMessage)> m_on_message;
std::function<void(tl::expected<void, ChatClientError>)> m_on_send_response;
@@ -22,45 +22,68 @@ tl::expected<void, tw::chat::ChatClientError> from_error_code(mmo::chat::ChatErr
namespace tw::chat {
namespace {
std::unique_ptr<tw::msg::MessageEndpoint> create_endpoint() {
auto endpoint_r = tw::msg::MessageEndpoint::create();
if (!endpoint_r) {
throw std::runtime_error("Chat client failed to create an endpoint: " +
endpoint_r.error().message());
}
return std::move(endpoint_r.value());
}
tw::msg::MessageConnection* connect_to_server(tw::msg::MessageEndpoint* endpoint,
const std::string& server_address,
int16_t port) {
auto server_r = endpoint->connect(server_address, port);
if (!server_r) {
throw std::runtime_error("Chat client failed to connect: " + server_r.error().message());
}
return server_r.value();
}
}
ChatClient::ChatClient(const std::string& server_address, int16_t port)
: m_session(net::Address(server_address, port))
: m_endpoint(create_endpoint())
, m_server(connect_to_server(m_endpoint.get(), server_address, port))
, m_messages(m_endpoint.get())
{
m_session.set_handler(CHAT_MESSAGE_BROADCAST_REQUEST, [this](std::span<const std::byte> data) {
if (!m_on_message) return;
mmo::chat::ChatMessageBroadcastRequest proto;
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
ChatMessage msg;
msg.channel_id = proto.channel_id();
msg.client_id = proto.sender_id();
msg.message = proto.message();
msg.timestamp = ChatMessage::Clock::now();
m_on_message(std::move(msg));
});
m_messages.set_handler<mmo::chat::ChatMessageBroadcastRequest>(
[this](msg::PeerId, const mmo::chat::ChatMessageBroadcastRequest& proto) {
if (!m_on_message) return;
ChatMessage msg;
msg.channel_id = proto.channel_id();
msg.client_id = proto.sender_id();
msg.message = proto.message();
msg.timestamp = ChatMessage::Clock::now();
m_on_message(std::move(msg));
});
m_session.set_handler(CHAT_SEND_MESSAGE_RESPONSE, [this](std::span<const std::byte> data) {
if (!m_on_send_response) return;
mmo::chat::SendChatMessageResponse proto;
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
m_on_send_response(from_error_code(proto.error()));
});
m_messages.set_handler<mmo::chat::SendChatMessageResponse>(
[this](msg::PeerId, const mmo::chat::SendChatMessageResponse& proto) {
if (!m_on_send_response) return;
m_on_send_response(from_error_code(proto.error()));
});
m_session.set_handler(CHAT_JOIN_CHANNEL_RESPONSE, [this](std::span<const std::byte> data) {
if (!m_on_join_response) return;
mmo::chat::JoinChannelResponse proto;
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
m_on_join_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
});
m_messages.set_handler<mmo::chat::JoinChannelResponse>(
[this](msg::PeerId, const mmo::chat::JoinChannelResponse& proto) {
if (!m_on_join_response) return;
m_on_join_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
});
m_session.set_handler(CHAT_LEAVE_CHANNEL_RESPONSE, [this](std::span<const std::byte> data) {
if (!m_on_leave_response) return;
mmo::chat::LeaveChannelResponse proto;
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
m_on_leave_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
});
m_messages.set_handler<mmo::chat::LeaveChannelResponse>(
[this](msg::PeerId, const mmo::chat::LeaveChannelResponse& proto) {
if (!m_on_leave_response) return;
m_on_leave_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
});
}
void ChatClient::update() {
m_session.update();
m_endpoint->update();
}
tl::expected<void, ChatClientError> ChatClient::send_mesg(SendChatMessage message) {
@@ -68,11 +91,7 @@ tl::expected<void, ChatClientError> ChatClient::send_mesg(SendChatMessage messag
mesg.set_channel_id(message.channel_id);
mesg.set_message(message.message);
std::vector<std::byte> buf(mesg.ByteSizeLong());
(void)mesg.SerializeToArray(buf.data(), static_cast<int>(buf.size()));
auto send_r = m_session.send(Message<mmo::chat::SendChatMessageRequest>::value,
std::span(buf), true);
auto send_r = m_messages.send(m_server, mesg, true);
if (!send_r)
return tl::make_unexpected(ChatClientError::PermissionDenied);
return {};
@@ -13,8 +13,9 @@ target_include_directories(${PROJECT_NAME}
target_link_libraries(${PROJECT_NAME}
PRIVATE
tw::chat::service
tw::messaging
tw::protocol
tw::message_protocol
tw::network
tw::quicr
spdlog::spdlog
)
@@ -3,7 +3,7 @@
#include "Chat.pb.h"
#include "MessageRegistry.hpp"
#include <spdlog/spdlog.h>
#include <cstring>
#include <stdexcept>
namespace tw::chat {
@@ -16,108 +16,68 @@ static mmo::chat::ChatErrorCode to_error_code(tl::expected<void, ChatServerError
return mmo::chat::CHAT_ERROR_CODE_CHANNEL_NOT_FOUND;
}
template<typename T>
static std::vector<std::byte> serialize(const T& msg) {
std::vector<std::byte> buf(msg.ByteSizeLong());
(void)msg.SerializeToArray(buf.data(), static_cast<int>(buf.size()));
return buf;
static std::unique_ptr<msg::MessageEndpoint> bind_endpoint(int port) {
auto endpoint_r = msg::MessageEndpoint::bind(port);
if (!endpoint_r) {
throw std::runtime_error("Chat server failed to bind to port " + std::to_string(port) +
": " + endpoint_r.error().message());
}
return std::move(endpoint_r.value());
}
ChatServerController::ChatServerController(int port)
: m_endpoint(net::quicr::QuicrEndpoint::create_and_bind(port).value())
, m_listener(net::quicr::QuicrConnectionListener::listen(m_endpoint.get()).value())
: m_endpoint(bind_endpoint(port))
, m_messages(m_endpoint.get())
, m_service([this](uint64_t id, const ChatMessage& msg) { broadcast(id, msg); })
{
m_endpoint->set_on_peer_connected([](msg::PeerId client_id) {
spdlog::info("Chat client connected: {}", client_id);
});
register_handlers();
spdlog::info("Chat server listening on port {}", port);
}
void ChatServerController::register_handlers() {
m_handlers[Message<mmo::chat::SendChatMessageRequest>::value] =
[this](uint64_t client_id, std::span<const std::byte> data) {
mmo::chat::SendChatMessageRequest msg;
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
m_messages.set_handler<mmo::chat::SendChatMessageRequest>(
[this](msg::PeerId client_id, const mmo::chat::SendChatMessageRequest& msg) {
mmo::chat::SendChatMessageResponse r;
r.set_channel_id(msg.channel_id());
r.set_error(to_error_code(m_service.send_message(client_id, msg.channel_id(), msg.message())));
send_to(client_id, Message<mmo::chat::SendChatMessageResponse>::value, serialize(r));
};
(void)m_messages.send_to(client_id, r, false);
});
m_handlers[Message<mmo::chat::JoinChannelRequest>::value] =
[this](uint64_t client_id, std::span<const std::byte> data) {
mmo::chat::JoinChannelRequest msg;
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
m_messages.set_handler<mmo::chat::JoinChannelRequest>(
[this](msg::PeerId client_id, const mmo::chat::JoinChannelRequest& msg) {
m_service.join_channel(client_id, msg.channel_id());
mmo::chat::JoinChannelResponse r;
r.set_channel_id(msg.channel_id());
r.set_error(mmo::chat::CHAT_ERROR_CODE_OK);
send_to(client_id, Message<mmo::chat::JoinChannelResponse>::value, serialize(r));
};
(void)m_messages.send_to(client_id, r, false);
});
m_handlers[Message<mmo::chat::LeaveChannelRequest>::value] =
[this](uint64_t client_id, std::span<const std::byte> data) {
mmo::chat::LeaveChannelRequest msg;
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
m_messages.set_handler<mmo::chat::LeaveChannelRequest>(
[this](msg::PeerId client_id, const mmo::chat::LeaveChannelRequest& msg) {
m_service.leave_channel(client_id, msg.channel_id());
mmo::chat::LeaveChannelResponse r;
r.set_channel_id(msg.channel_id());
r.set_error(mmo::chat::CHAT_ERROR_CODE_OK);
send_to(client_id, Message<mmo::chat::LeaveChannelResponse>::value, serialize(r));
};
(void)m_messages.send_to(client_id, r, false);
});
}
void ChatServerController::update() {
m_endpoint->poll();
net::quicr::QuicrConnection* conn = nullptr;
while ((conn = m_listener->listen())) {
m_connections.emplace(conn->self_id(), conn);
spdlog::info("Chat client connected: {}", conn->self_id());
}
for (auto& [client_id, conn] : m_connections) {
auto r = conn->read_into(m_recv_buf);
if (!r || *r == 0) continue;
dispatch(client_id, std::span(m_recv_buf.data(), *r));
}
m_endpoint->update();
}
void ChatServerController::dispatch(uint64_t client_id, std::span<const std::byte> data) {
constexpr size_t HEADER = sizeof(uint32_t) * 2;
if (data.size() < HEADER) {
spdlog::warn("ChatServerController: dropped short datagram ({} bytes)", data.size());
return;
}
uint32_t type{};
std::memcpy(&type, data.data(), sizeof(type));
if (type >= m_handlers.size() || !m_handlers[type]) {
spdlog::warn("ChatServerController: no handler for type {}", type);
return;
}
m_handlers[type](client_id, data.subspan(HEADER));
}
void ChatServerController::send_to(uint64_t client_id, uint32_t type,
std::span<const std::byte> payload, bool reliable) {
auto it = m_connections.find(client_id);
if (it == m_connections.end()) return;
constexpr uint32_t SEQ_NONE = 0;
std::vector<std::byte> buf(sizeof(type) + sizeof(SEQ_NONE) + payload.size());
std::memcpy(buf.data(), &type, sizeof(type));
std::memcpy(buf.data() + sizeof(type), &SEQ_NONE, sizeof(SEQ_NONE));
std::memcpy(buf.data() + sizeof(type) + sizeof(SEQ_NONE), payload.data(), payload.size());
(void)it->second->send_message(std::span(buf), reliable);
}
void ChatServerController::broadcast(uint64_t client_id, const ChatMessage& msg) {
void ChatServerController::broadcast(msg::PeerId client_id, const ChatMessage& msg) {
mmo::chat::ChatMessageBroadcastRequest bcast;
bcast.set_channel_id(msg.channel_id);
bcast.set_sender_id(msg.client_id);
bcast.set_message(msg.message);
send_to(client_id, Message<mmo::chat::ChatMessageBroadcastRequest>::value,
serialize(bcast), true);
(void)m_messages.send_to(client_id, bcast, true);
}
} // namespace tw::chat
@@ -1,28 +1,18 @@
#pragma once
#include "ChatService.hpp"
#include "protocol/quicr/QuicrEndpoint.hpp"
#include "protocol/quicr/QuicrConnectionListener.hpp"
#include "ProtobufMessages.hpp"
#include "message_protocol/MessageEndpoint.hpp"
#include <array>
#include <cstdint>
#include <functional>
#include <span>
#include <unordered_map>
#include <vector>
#include <memory>
namespace tw::chat {
class ChatServerController {
static constexpr size_t MAX_TYPES = 32;
std::unique_ptr<net::quicr::QuicrEndpoint> m_endpoint;
std::unique_ptr<net::quicr::QuicrConnectionListener> m_listener;
std::unordered_map<uint64_t, net::quicr::QuicrConnection*> m_connections;
std::vector<std::byte> m_recv_buf{64 * 1024};
ChatService m_service;
std::array<std::function<void(uint64_t, std::span<const std::byte>)>, MAX_TYPES> m_handlers{};
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
ProtobufMessages m_messages;
ChatService m_service;
public:
explicit ChatServerController(int port = CHAT_DEFAULT_PORT);
@@ -31,10 +21,7 @@ public:
private:
void register_handlers();
void dispatch(uint64_t client_id, std::span<const std::byte> data);
void send_to(uint64_t client_id, uint32_t type, std::span<const std::byte> payload,
bool reliable = false);
void broadcast(uint64_t client_id, const ChatMessage& msg);
void broadcast(msg::PeerId client_id, const ChatMessage& msg);
};
} // namespace tw::chat
@@ -5,7 +5,8 @@ add_executable(${PROJECT_NAME} ChatMockClient.cpp)
target_link_libraries(${PROJECT_NAME}
PRIVATE
tw::protocol
tw::messaging
tw::message_protocol
tw::network
tw::quicr
spdlog::spdlog
)
@@ -1,7 +1,8 @@
#include "Address.hpp"
#include "MessageSession.hpp"
#include "ProtobufMessages.hpp"
#include "MessageRegistry.hpp"
#include "Chat.pb.h"
#include "message_protocol/MessageEndpoint.hpp"
#include <spdlog/spdlog.h>
#include <atomic>
@@ -56,30 +57,42 @@ int main(int argc, char* argv[]) {
return 1;
}
tw::MessageSession session(tw::net::Address{std::string{host}, port});
auto endpoint_r = tw::msg::MessageEndpoint::create();
if (!endpoint_r) {
spdlog::error("Failed to create an endpoint: {}", endpoint_r.error().message());
return 1;
}
auto& endpoint = endpoint_r.value();
auto server_r = endpoint->connect(host, port);
if (!server_r) {
spdlog::error("Failed to connect: {}", server_r.error().message());
return 1;
}
auto* server = server_r.value();
tw::ProtobufMessages messages(endpoint.get());
spdlog::info("Connecting to {}:{}...", host, port);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!session.is_established()) {
while (!server->is_established()) {
if (std::chrono::steady_clock::now() > deadline) {
spdlog::error("Connection timed out");
return 1;
}
session.update();
endpoint->update();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
spdlog::info("Connected. Joining channel {}...", channel_id);
session.set_handler(tw::Message<mmo::chat::ChatMessageBroadcastRequest>::value,
[](std::span<const std::byte> data) {
mmo::chat::ChatMessageBroadcastRequest bcast;
bcast.ParseFromArray(data.data(), static_cast<int>(data.size()));
messages.set_handler<mmo::chat::ChatMessageBroadcastRequest>(
[](tw::msg::PeerId, const mmo::chat::ChatMessageBroadcastRequest& bcast) {
std::println("[ch:{}] <{}> {}", bcast.channel_id(), bcast.sender_id(), bcast.message());
});
mmo::chat::JoinChannelRequest join;
join.set_channel_id(channel_id);
(void)session.request(
(void)server->request(
tw::Message<mmo::chat::JoinChannelRequest>::value,
serialize(join),
[channel_id](std::span<const std::byte> data) {
@@ -102,7 +115,7 @@ int main(int argc, char* argv[]) {
mmo::chat::SendChatMessageRequest msg;
msg.set_channel_id(channel_id);
msg.set_message(line);
(void)session.request(
(void)server->request(
tw::Message<mmo::chat::SendChatMessageRequest>::value,
serialize(msg),
[channel_id](std::span<const std::byte> data) {
@@ -113,7 +126,7 @@ int main(int argc, char* argv[]) {
}
}
session.update();
endpoint->update();
}
return 0;