#1 - quicr module

This commit is contained in:
Martin Slachta
2026-07-22 17:34:44 +02:00
parent a04f0dc262
commit e6dd954ded
149 changed files with 3997 additions and 2126 deletions
@@ -0,0 +1,127 @@
#pragma once
#include "message_protocol/MessageError.hpp"
#include "message_protocol/MessageHeader.hpp"
#include "message_protocol/MessageType.hpp"
#include "message_protocol/PeerId.hpp"
#include <tl/expected.hpp>
#include <chrono>
#include <cstddef>
#include <functional>
#include <span>
#include <unordered_map>
#include <vector>
namespace tw::net::quicr {
class QuicrConnection;
}
namespace tw::msg {
class MessageDispatcher;
/**
* Sends messages to one peer and routes the ones it sends back.
*
* Owned by the endpoint that created it and valid until the peer disconnects.
* Handlers for message addresses are registered on the endpoint and shared by
* every peer; a connection only holds the reply handlers for requests it made
* itself.
*/
class MessageConnection {
public:
using ReplyHandler = std::function<void(std::span<const std::byte>)>;
private:
struct PendingRequest {
ReplyHandler on_reply;
std::function<void()> on_timeout;
std::chrono::steady_clock::time_point expires_at;
};
net::quicr::QuicrConnection* m_connection;
MessageDispatcher* m_dispatcher;
PeerId m_peer_id;
std::vector<std::byte> m_send_buffer;
std::unordered_map<uint32_t, PendingRequest> m_pending;
uint32_t m_next_seq = 1;
uint64_t m_bytes_sent = 0;
uint64_t m_messages_sent = 0;
uint64_t m_messages_received = 0;
uint32_t next_seq();
tl::expected<void, MessageError> send_impl(MessageType type,
std::span<const std::byte> body,
uint32_t seq,
bool reliable);
public:
MessageConnection(PeerId peer_id,
net::quicr::QuicrConnection* connection,
MessageDispatcher* dispatcher);
MessageConnection(const MessageConnection&) = delete;
MessageConnection& operator=(const MessageConnection&) = delete;
PeerId peer_id() const {
return m_peer_id;
}
uint64_t bytes_sent() const {
return m_bytes_sent;
}
/** Messages handed over for sending since the connection was created. */
uint64_t messages_sent() const {
return m_messages_sent;
}
/** Messages routed from the peer since the connection was created. */
uint64_t messages_received() const {
return m_messages_received;
}
bool is_established() const;
tl::expected<void, MessageError> send(MessageType type,
std::span<const std::byte> body,
bool reliable = false);
/**
* Sends a message the caller has already written a header into, for
* callers that build the whole message in a buffer of their own.
*/
tl::expected<void, MessageError> send_framed(std::span<const std::byte> message,
bool reliable = false);
/**
* Sends `body` and calls `on_reply` with the reply carrying the same
* sequence number, or `on_timeout` if no reply arrives in time.
*/
tl::expected<void, MessageError> request(
MessageType type,
std::span<const std::byte> body,
ReplyHandler on_reply,
std::chrono::milliseconds timeout = std::chrono::seconds(5),
std::function<void()> on_timeout = nullptr,
bool reliable = true);
/**
* Reads everything the peer has sent, routing each message, and returns
* how many bytes were read. `scratch` is used to hold one message at a
* time and may be reused between peers.
*/
size_t receive(std::span<std::byte> scratch);
/** Routes one received message to its reply handler, or to the dispatcher. */
void on_message(std::span<const std::byte> message);
/** Fails every request whose reply did not arrive before `now`. */
void expire_requests(std::chrono::steady_clock::time_point now);
};
}
@@ -0,0 +1,47 @@
#pragma once
#include "message_protocol/MessageType.hpp"
#include "message_protocol/PeerId.hpp"
#include <functional>
#include <span>
#include <unordered_map>
namespace tw::msg {
/**
* Routes a message body to the handler registered for its address.
*
* Never inspects the body, so how it is encoded is entirely the caller's
* concern. At most one handler may be registered per address.
*/
class MessageDispatcher {
public:
using Handler = std::function<void(PeerId, std::span<const std::byte>)>;
private:
std::unordered_map<MessageType, Handler> m_handlers;
public:
/** Registers `handler` for `type`, replacing any handler already there. */
void set_handler(MessageType type, Handler handler) {
m_handlers[type] = std::move(handler);
}
bool has_handler(MessageType type) const {
return m_handlers.contains(type);
}
/** Invokes the handler for `type`. Returns false if there is none. */
bool dispatch(PeerId peer, MessageType type, std::span<const std::byte> body) {
auto handler = m_handlers.find(type);
if(handler == m_handlers.end()) {
return false;
}
handler->second(peer, body);
return true;
}
};
}
@@ -0,0 +1,110 @@
#pragma once
#include "message_protocol/MessageConnection.hpp"
#include "message_protocol/MessageDispatcher.hpp"
#include "message_protocol/MessageError.hpp"
#include "message_protocol/MessageType.hpp"
#include "message_protocol/PeerId.hpp"
#include <tl/expected.hpp>
#include <cstddef>
#include <functional>
#include <memory>
#include <span>
#include <string>
#include <unordered_map>
#include <vector>
namespace tw::net::quicr {
class QuicrEndpoint;
class QuicrConnectionListener;
}
namespace tw::msg {
/**
* Owns the connections to every peer and the handlers shared between them.
*
* An endpoint created with bind() accepts incoming peers; either kind may
* connect() outwards, so one endpoint can serve peers and reach out to others
* at the same time.
*
* update() must be called regularly. Nothing is received and no request ever
* times out between calls.
*/
class MessageEndpoint {
std::unique_ptr<net::quicr::QuicrEndpoint> m_endpoint;
std::unique_ptr<net::quicr::QuicrConnectionListener> m_listener;
MessageDispatcher m_dispatcher;
std::unordered_map<PeerId, std::unique_ptr<MessageConnection>> m_peers;
std::function<void(PeerId)> m_on_peer_connected;
PeerId m_next_peer_id = 1;
std::vector<std::byte> m_receive_buffer;
uint64_t m_bytes_received = 0;
explicit MessageEndpoint(std::unique_ptr<net::quicr::QuicrEndpoint> endpoint);
MessageConnection* add_peer(net::quicr::QuicrConnection* connection);
void accept_peers();
void receive();
public:
~MessageEndpoint();
MessageEndpoint(const MessageEndpoint&) = delete;
MessageEndpoint& operator=(const MessageEndpoint&) = delete;
/** Creates an endpoint that only connects outwards. */
static tl::expected<std::unique_ptr<MessageEndpoint>, MessageError> create();
/** Creates an endpoint that also accepts peers on `port`. */
static tl::expected<std::unique_ptr<MessageEndpoint>, MessageError> bind(int port);
tl::expected<MessageConnection*, MessageError> connect(const std::string& host, int port);
/** Registers `handler` for every peer. */
void set_handler(MessageType type, MessageDispatcher::Handler handler) {
m_dispatcher.set_handler(type, std::move(handler));
}
MessageDispatcher& dispatcher() {
return m_dispatcher;
}
/** Receives pending messages, accepts new peers and times out requests. */
void update();
MessageConnection* peer(PeerId id);
/**
* Calls `handler` for each peer that connects or is accepted, before any
* of that peer's messages are dispatched.
*/
void set_on_peer_connected(std::function<void(PeerId)> handler) {
m_on_peer_connected = std::move(handler);
}
std::vector<MessageConnection*> peers() const;
void broadcast(MessageType type, std::span<const std::byte> body, bool reliable = false);
/** Bytes received since the endpoint was created. */
uint64_t bytes_received() const {
return m_bytes_received;
}
/** Bytes handed to every peer for sending since the endpoint was created. */
uint64_t bytes_sent() const;
/** Messages handed to every peer for sending since the endpoint was created. */
uint64_t messages_sent() const;
/** Messages routed from every peer since the endpoint was created. */
uint64_t messages_received() const;
};
}
@@ -0,0 +1,37 @@
#pragma once
#include <string>
#include <utility>
namespace tw::msg {
enum class MessageErrorType {
NotConnected,
SendFailed,
BindFailed,
ConnectFailed,
};
struct MessageError {
MessageErrorType type;
std::string detail;
explicit MessageError(MessageErrorType type, std::string detail = {}) :
type(type),
detail(std::move(detail)) {
}
std::string message() const {
std::string text;
switch(type) {
case MessageErrorType::NotConnected: text = "Not connected to the peer"; break;
case MessageErrorType::SendFailed: text = "Failed to send the message"; break;
case MessageErrorType::BindFailed: text = "Failed to bind the endpoint"; break;
case MessageErrorType::ConnectFailed: text = "Failed to connect to the peer"; break;
}
return detail.empty() ? text : text + ": " + detail;
}
};
}
@@ -0,0 +1,45 @@
#pragma once
#include "message_protocol/MessageType.hpp"
#include <cstdint>
#include <cstring>
#include <optional>
#include <span>
namespace tw::msg {
/**
* Fixed-size prefix carried by every message.
*
* `seq` correlates a reply with the request that produced it. SEQ_NONE marks a
* message that expects no reply, which is the common case.
*/
struct MessageHeader {
static constexpr uint32_t SEQ_NONE = 0;
static constexpr size_t SIZE = sizeof(MessageType) + sizeof(uint32_t);
MessageType type = 0;
uint32_t seq = SEQ_NONE;
/** Writes the header at the start of `target`, which must hold SIZE bytes. */
void encode(std::span<std::byte> target) const {
std::memcpy(target.data(), &type, sizeof(type));
std::memcpy(target.data() + sizeof(type), &seq, sizeof(seq));
}
/** Reads a header from the start of `source`, or nothing if it is too short. */
static std::optional<MessageHeader> decode(std::span<const std::byte> source) {
if(source.size() < SIZE) {
return {};
}
MessageHeader header;
std::memcpy(&header.type, source.data(), sizeof(header.type));
std::memcpy(&header.seq, source.data() + sizeof(header.type), sizeof(header.seq));
return header;
}
};
}
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
namespace tw::msg {
/**
* Address a message is delivered to. Concrete values are assigned by the
* application.
*/
using MessageType = uint32_t;
}
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
namespace tw::msg {
/**
* Identifies a remote peer. Assigned when the peer connects or is accepted and
* stable until it disconnects.
*/
using PeerId = uint64_t;
}