qb
2.0.0.0
C++17 Actor Framework
|
Master how QB actors become powerful network clients and servers using the qb::io::use<> template for seamless asynchronous I/O.
One of the most powerful aspects of the QB Actor Framework is its seamless integration of asynchronous network I/O directly into actors. This allows you to build highly concurrent and responsive network applications—clients, servers, or peer-to-peer systems—where each network endpoint can be an independent, stateful actor.
The primary mechanism for this integration is the qb::io::use<DerivedActor> helper template.
Defined in qb/io/async.h, the qb::io::use<DerivedActor> template is a sophisticated CRTP (Curiously Recurring Template Pattern) utility. When your actor class inherits from one of its nested specializations (e.g., qb::io::use<MyClient>::tcp::client), it automatically gains the necessary base classes and methods to function as a specific type of network endpoint. This integration is deep: the actor's network operations become part of its VirtualCore's event loop, ensuring non-blocking behavior.
Key Specializations for Networked Actors:
Core Functionality Provided by qb::io::use<>:
When your actor inherits from one of these use<> specializations, it gains:
Let's outline the structure for an actor that connects to a server.
Conceptual Client-Side Network Interaction:
Inherit and Define Protocol: ```cpp #include <qb/actor.h> #include <qb/io/async.h> // For qb::io::use<> #include <qb/io/uri.h> // For qb::io::uri parsing #include <qb/io/protocol/text.h> // Example: for text::command protocol #include <qb/io.h> // For qb::io::cout // For SSL: // #include <qb/io/tcp/ssl/socket.h> // For SSL_CTX, qb::io::ssl::create_client_context
// Forward declaration for any events this actor sends/receives from other actors struct SendToServerCommand : qb::Event { qb::string<128> command_data; };
class MyNetworkClient : public qb::Actor, public qb::io::use<MyNetworkClient>::tcp::client<> { // For SSL: public qb::io::use<MyNetworkClient>::tcp::ssl::client<> { public: // Define the protocol for framing messages over the connection using Protocol = qb::protocol::text::command<MyNetworkClient>; // Example: newline-terminated
private: qb::io::uri _server_uri; bool _connected = false; // For SSL clients: // SSL_CTX* _ssl_ctx = nullptr; // Remember to manage its lifecycle (create/free)
public: explicit MyNetworkClient(const std::string& server_uri_string) : _server_uri(server_uri_string) { // For SSL: // _ssl_ctx = qb::io::ssl::create_client_context(TLS_client_method()); // if (!_ssl_ctx) { /* Handle error: throw or log & fail onInit */ } }
// For SSL: // ~MyNetworkClient() override { // if (_ssl_ctx) { SSL_CTX_free(_ssl_ctx); } // }
// ... (onInit, event handlers, etc., follow) }; ```
Use qb::io::async::tcp::connect for non-blocking connection establishment. Provide a callback lambda to handle the connection result. ```cpp // Inside MyNetworkClient bool onInit() override { registerEvent<SendToServerCommand>(*this); registerEvent<qb::KillEvent>(*this);
// For SSL: // if (!_ssl_ctx) return false; // Ensure SSL_CTX was created // this->transport().init(_ssl_ctx);
qb::io::cout() << "Client [" << id() << "]: Attempting connection to " << _server_uri.source().data() << ".\n";
// Deduce the socket type (tcp::socket or tcp::ssl::socket) using UnderlyingSocketType = decltype(this->transport());
qb::io::async::tcp::connect<UnderlyingSocketType>( _server_uri, // Target URI _server_uri.host().data(), // SNI hostname (esp. for SSL) [this](UnderlyingSocketType resulting_socket) { // Connection callback if (!this->is_alive()) return; // Actor might have been killed
if (resulting_socket.is_open()) { qb::io::cout() << "Client [" << id() << "]: TCP connection established.\n"; // Move the connected socket into our transport this->transport() = std::move(resulting_socket); // Initialize our chosen protocol on the now-active transport this->template switch_protocol<Protocol>(*this); // Start monitoring I/O events (read/write readiness) this->start(); _connected = true;
// For SSL, after start(), complete the handshake if constexpr (std::is_same_v<UnderlyingSocketType, qb::io::tcp::ssl::socket>) { if (this->transport().connected() != 0) { qb::io::cout() << "Client [" << id() << "]: SSL handshake failed.\n"; _connected = false; this->close(); // Close the underlying socket // Consider retry logic here via async::callback return; } qb::io::cout() << "Client [" << id() << "]: SSL handshake successful.\n"; }
// Optional: Send an initial message (e.g., authentication) // *this << "HELLO_SERVER" << Protocol::end; } else { qb::io::cout() << "Client [" << id() << "]: Connection failed.\n"; // Schedule a retry or terminate // qb::io::async::callback([this](){ if(this->is_alive()) this->onInit(); }, 5.0); } } //, 5.0 // Optional timeout for the connect attempt in seconds ); return true; } ```
(Reference: chat_tcp/client/ClientActor.h/.cpp, message_broker/client/ClientActor.h/.cpp for complete client implementations. test-async-io.cpp in qb/source/io/tests/system/ also shows SSL client setup within tests.**)
Servers generally consist of two main roles: an acceptor that listens for new connections, and session handlers that manage communication with individual connected clients. QB supports different ways to structure this:
Basic Server Architecture (Separate Acceptor & Session Managers):
Suitable for simpler servers where a single actor class can manage both listening for new connections and handling all active client sessions.
Define Session Class: This class will handle I/O for one connected client. It usually inherits from qb::io::use<MySessionClass>::tcp::client<MyServerActorType> (or its SSL variant), making it a client from qb-io's perspective but managed by your MyServerActorType. ```cpp // MySession.h class MyServerActor; // Forward declaration
class MyClientSession : public qb::io::use<MyClientSession>::tcp::client<MyServerActor> { public: using Protocol = qb::protocol::text::command<MyClientSession>; // Or your custom protocol
explicit MyClientSession(MyServerActor& server_logic) : client(server_logic) {}
void on(Protocol::message&& msg) { // Process data received from this client // Example: server().handleClientCommand(this->id(), msg.text); } void on(qb::io::async::event::disconnected const& event) { // Notify the main server actor of this client's disconnection // server().handleClientDisconnect(this->id()); } // ... other session logic ... };
**Define Server Actor:** This actor inherits from `qb::Actor` and `qb::io::use<MyServerActorType>::tcp::server<MySessionClass>` (or `::tcp::ssl::server`). The `tcp::server` base provides `io_handler` capabilities. cpp // MyServerActor.h class MyServerActor : public qb::Actor, public qb::io::use<MyServerActor>::tcp::server<MyClientSession> { private: // For SSL: // SSL_CTX* _server_ssl_ctx = nullptr; // Manage its lifecycle public: explicit MyServerActor(const qb::io::uri& listen_uri) { // For SSL: // _server_ssl_ctx = qb::io::ssl::create_server_context(TLS_server_method(), cert_path, key_path); // if (!_server_ssl_ctx) { /* error */ } // this->transport().init(_server_ssl_ctx);
if (this->transport().listen(listen_uri) != 0) { /* Handle listen error */ } this->start(); // Start accepting connections qb::io::cout() << "Server [" << id() << "] listening on " << listen_uri.source().data() << ".\n"; } // For SSL: ~MyServerActor() { if (_server_ssl_ctx) SSL_CTX_free(_server_ssl_ctx); }
// This method is called by the tcp::server base after a new MyClientSession // instance is created and its transport (the accepted socket) is set up. void on(IOSession& new_session) { // IOSession is MyClientSession here qb::io::cout() << "Server [" << id() << "]: New client session [" << new_session.id() << "] connected from " << new_session.transport().peer_endpoint().to_string() << ".\n"; // new_session.start() is typically called by the base when registering the session. // You can send a welcome message, etc. // new_session << "Welcome!" << MyClientSession::Protocol::end; }
// Example methods to be called by MyClientSession instances: // void handleClientCommand(qb::uuid session_uuid, const std::string& command) { /* ... */ } // void handleClientDisconnect(qb::uuid session_uuid) { // if (this->sessions().count(session_uuid)) { // this->sessions().erase(session_uuid); // Remove from managed sessions // qb::io::cout() << "Server: Session " << session_uuid << " removed.\n"; // } // }
void on(const qb::KillEvent& /*event*/) { qb::io::cout() << "Server [" << id() << "] shutting down.\n"; for (auto& [uuid, session_ptr] : this->sessions()) { if (session_ptr) session_ptr->disconnect(); // Request graceful session shutdown } this->sessions().clear(); this->close(); // Close the listener socket this->kill(); } }; ```
For greater scalability, especially to distribute session handling across multiple cores, you can separate the connection accepting logic from the session management logic.
(Reference: The chat_tcp and message_broker examples robustly implement Pattern 2. They have an AcceptActor, one or more ServerActors (which act as session managers), and specific Session classes (ChatSession, BrokerSession).**)
By leveraging qb::io::use<> and understanding these patterns, your QB actors can become powerful, self-contained network participants, capable of handling complex asynchronous communication with clarity and efficiency.
(Next: Review specific example analyses like chat_tcp Example Analysis to see these patterns in larger contexts.**) (See also: QB-IO: Transports, QB-IO: Protocols**)