MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
TCPBackend.cpp
Go to the documentation of this file.
1#include "TCPBackend.hpp"
2
3#if defined(_WIN32) || defined(_WIN64)
4#include <windows.h>
5#endif
6
7#include <asio/read.hpp>
8#include <asio/write.hpp>
9
11
12namespace MayaFlux::Core {
13
14TCPBackend::TCPBackend(const TCPBackendInfo& config, asio::io_context& context)
15 : m_config(config)
16 , m_context(context)
17{
18}
19
24
25// ─────────────────────────────────────────────────────────────────────────────
26// Lifecycle
27// ─────────────────────────────────────────────────────────────────────────────
28
30{
31 if (m_initialized.load()) {
32 return true;
33 }
34
35 m_initialized.store(true);
36
38 "TCP backend initialized");
39
40 return true;
41}
42
44{
45 if (m_running.load()) {
46 return;
47 }
48
49 m_running.store(true);
50
52 "TCP backend started");
53}
54
56{
57 if (!m_running.load()) {
58 return;
59 }
60
61 m_running.store(false);
62
63 {
64 std::shared_lock lock(m_connections_mutex);
65 for (auto& [id, conn] : m_connections) {
66 asio::error_code ec;
67
68 if (conn->socket.close(ec)) {
70 "Error closing TCP connection {}: {}", id, ec.message());
71 }
72
73 if (conn->reconnect_timer) {
74 conn->reconnect_timer->cancel();
75 }
76 }
77 }
78
79 {
80 std::shared_lock lock(m_listeners_mutex);
81 for (auto& [id, listener] : m_listeners) {
82 asio::error_code ec;
83 if (!listener->acceptor.close(ec)) {
85 "Error closing TCP listener {}: {}", id, ec.message());
86 }
87 }
88 }
89
91 "TCP backend stopped");
92}
93
95{
96 stop();
97
98 {
99 std::unique_lock lock(m_connections_mutex);
100 m_connections.clear();
101 }
102
103 {
104 std::unique_lock lock(m_listeners_mutex);
105 m_listeners.clear();
106 }
107
108 m_initialized.store(false);
109
111 "TCP backend shutdown");
112}
113
114// ─────────────────────────────────────────────────────────────────────────────
115// Endpoint management
116// ─────────────────────────────────────────────────────────────────────────────
117
119{
120 bool is_listener = !info.remote_address.empty() ? false
121 : (info.local_port > 0);
122
123 if (is_listener) {
124 auto listener = std::make_unique<ListenerState>(m_context);
125 listener->info = info;
126 listener->info.state = EndpointState::OPENING;
127
128 asio::error_code ec;
129 auto endpoint = asio::ip::tcp::endpoint(asio::ip::tcp::v4(), info.local_port);
130
131 if (listener->acceptor.open(endpoint.protocol(), ec)) {
133 "Failed to open TCP acceptor: {}", ec.message());
134 return 0;
135 }
136
137 if (listener->acceptor.set_option(asio::socket_base::reuse_address(true), ec)) {
139 "Failed to set SO_REUSEADDR on TCP acceptor: {}", ec.message());
140 return 0;
141 }
142
143 if (listener->acceptor.bind(endpoint, ec)) {
145 "Failed to bind TCP acceptor to port {}: {}", info.local_port, ec.message());
146 return 0;
147 }
148
149 if (listener->acceptor.listen(asio::socket_base::max_listen_connections, ec)) {
151 "Failed to listen on port {}: {}", info.local_port, ec.message());
152 return 0;
153 }
154
155 listener->info.state = EndpointState::OPEN;
156
157 auto* raw = listener.get();
158
159 {
160 std::unique_lock lock(m_listeners_mutex);
161 m_listeners[info.id] = std::move(listener);
162 }
163
164 if (m_state_callback) {
166 }
167
168 start_accept(*raw);
169
171 "TCP listener {} opened on port {}", info.id, info.local_port);
172
173 return info.id;
174 }
175
176 auto conn = std::make_unique<ConnectionState>(m_context);
177 conn->info = info;
178 conn->info.state = EndpointState::OPENING;
179 conn->is_outbound = true;
180
181 auto* raw = conn.get();
182
183 {
184 std::unique_lock lock(m_connections_mutex);
185 m_connections[info.id] = std::move(conn);
186 }
187
188 start_connect(*raw);
189
191 "TCP connection {} opening to {}:{}", info.id, info.remote_address, info.remote_port);
192
193 return info.id;
194}
195
196void TCPBackend::close_endpoint(uint64_t endpoint_id)
197{
198 {
199 std::unique_lock lock(m_connections_mutex);
200 auto it = m_connections.find(endpoint_id);
201 if (it != m_connections.end()) {
202 asio::error_code ec;
203
204 if (!it->second->socket.close(ec)) {
206 "Error closing TCP connection {}: {}", endpoint_id, ec.message());
207 }
208
209 if (it->second->reconnect_timer) {
210 it->second->reconnect_timer->cancel();
211 }
212 m_connections.erase(it);
213
215 "TCP connection {} closed", endpoint_id);
216 return;
217 }
218 }
219
220 {
221 std::unique_lock lock(m_listeners_mutex);
222 auto it = m_listeners.find(endpoint_id);
223 if (it != m_listeners.end()) {
224 asio::error_code ec;
225
226 if (it->second->acceptor.close(ec)) {
228 "Error closing TCP listener {}: {}", endpoint_id, ec.message());
229 }
230
231 m_listeners.erase(it);
232
234 "TCP listener {} closed", endpoint_id);
235 }
236 }
237}
238
240{
241 {
242 std::shared_lock lock(m_connections_mutex);
243 auto it = m_connections.find(endpoint_id);
244 if (it != m_connections.end()) {
245 return it->second->info.state;
246 }
247 }
248
249 {
250 std::shared_lock lock(m_listeners_mutex);
251 auto it = m_listeners.find(endpoint_id);
252 if (it != m_listeners.end()) {
253 return it->second->info.state;
254 }
255 }
256
258}
259
260std::vector<EndpointInfo> TCPBackend::get_endpoints() const
261{
262 std::vector<EndpointInfo> result;
263
264 {
265 std::shared_lock lock(m_connections_mutex);
266 result.reserve(m_connections.size());
267 for (const auto& [id, conn] : m_connections) {
268 result.push_back(conn->info);
269 }
270 }
271
272 {
273 std::shared_lock lock(m_listeners_mutex);
274 for (const auto& [id, listener] : m_listeners) {
275 result.push_back(listener->info);
276 }
277 }
278
279 return result;
280}
281
282// ─────────────────────────────────────────────────────────────────────────────
283// Data transfer
284// ─────────────────────────────────────────────────────────────────────────────
285
286bool TCPBackend::send(uint64_t endpoint_id, const uint8_t* data, size_t size)
287{
288 std::shared_lock lock(m_connections_mutex);
289 auto it = m_connections.find(endpoint_id);
290 if (it == m_connections.end()) {
291 return false;
292 }
293
294 auto& conn = *it->second;
295 if (conn.info.state != EndpointState::OPEN) {
296 return false;
297 }
298
299 auto frame = std::make_shared<std::vector<uint8_t>>(sizeof(uint32_t) + size);
300 uint32_t net_len = htonl(static_cast<uint32_t>(size));
301 std::memcpy(frame->data(), &net_len, sizeof(uint32_t));
302 std::memcpy(frame->data() + sizeof(uint32_t), data, size);
303
304 asio::async_write(
305 conn.socket,
306 asio::buffer(*frame),
307 [frame, endpoint_id, this](const asio::error_code& ec, size_t) {
308 if (ec) {
309 MF_WARN(Journal::Component::Core, Journal::Context::NetworkBackend,
310 "TCP write failed on endpoint {}: {}", endpoint_id, ec.message());
311
312 std::shared_lock lk(m_connections_mutex);
313 auto cit = m_connections.find(endpoint_id);
314 if (cit != m_connections.end()) {
315 on_connection_error(*cit->second, ec);
316 }
317 }
318 });
319
320 return true;
321}
322
323bool TCPBackend::send_to(uint64_t endpoint_id, const uint8_t* data, size_t size,
324 const std::string&, uint16_t)
325{
326 return send(endpoint_id, data, size);
327}
328
329// ─────────────────────────────────────────────────────────────────────────────
330// Callbacks
331// ─────────────────────────────────────────────────────────────────────────────
332
333void TCPBackend::set_receive_callback(NetworkReceiveCallback callback)
334{
335 m_receive_callback = std::move(callback);
336}
337
338void TCPBackend::set_state_callback(EndpointStateCallback callback)
339{
340 m_state_callback = std::move(callback);
341}
342
343// ─────────────────────────────────────────────────────────────────────────────
344// Private: async connect
345// ─────────────────────────────────────────────────────────────────────────────
346
347void TCPBackend::start_connect(ConnectionState& conn)
348{
349 asio::error_code ec;
350 auto addr = asio::ip::make_address(conn.info.remote_address, ec);
351 if (ec) {
353 "Invalid remote address '{}': {}", conn.info.remote_address, ec.message());
354 transition_state(conn.info, EndpointState::ERROR);
355 return;
356 }
357
358 asio::ip::tcp::endpoint target(addr, conn.info.remote_port);
359
360 conn.socket.async_connect(
361 target,
362 [this, ep_id = conn.info.id](const asio::error_code& connect_ec) {
363 std::shared_lock lock(m_connections_mutex);
364 auto it = m_connections.find(ep_id);
365 if (it == m_connections.end()) {
366 return;
367 }
368
369 auto& c = *it->second;
370
371 if (connect_ec) {
373 "TCP connect failed for endpoint {}: {}",
374 ep_id, connect_ec.message());
375
376 if (m_config.auto_reconnect) {
377 transition_state(c.info, EndpointState::RECONNECTING);
378 schedule_reconnect(c);
379 } else {
380 transition_state(c.info, EndpointState::ERROR);
381 }
382 return;
383 }
384
385 transition_state(c.info, EndpointState::OPEN);
386 start_receive_chain(c);
387
389 "TCP endpoint {} connected to {}:{}",
390 ep_id, c.info.remote_address, c.info.remote_port);
391 });
392}
393
394// ─────────────────────────────────────────────────────────────────────────────
395// Private: async accept
396// ─────────────────────────────────────────────────────────────────────────────
397
398void TCPBackend::start_accept(ListenerState& listener)
399{
400 listener.pending_socket = asio::ip::tcp::socket(m_context);
401
402 listener.acceptor.async_accept(
403 listener.pending_socket,
404 [this, listener_id = listener.info.id](const asio::error_code& ec) {
405 std::shared_lock lock(m_listeners_mutex);
406 auto it = m_listeners.find(listener_id);
407 if (it == m_listeners.end()) {
408 return;
409 }
410
411 auto& lst = *it->second;
412
413 if (ec) {
414 if (ec == asio::error::operation_aborted) {
415 return;
416 }
418 "TCP accept error on listener {}: {}", listener_id, ec.message());
419 start_accept(lst);
420 return;
421 }
422
423 uint64_t new_id = 0;
424 if (m_allocate_endpoint_id) {
425 new_id = m_allocate_endpoint_id();
426 }
427
428 auto peer_ep = lst.pending_socket.remote_endpoint();
429 std::string peer_addr = peer_ep.address().to_string();
430 uint16_t peer_port = peer_ep.port();
431
432 auto conn = std::make_unique<ConnectionState>(m_context);
433 conn->socket = std::move(lst.pending_socket);
434 conn->is_outbound = false;
435 conn->info.id = new_id;
436 conn->info.transport = NetworkTransport::TCP;
437 conn->info.role = EndpointRole::BIDIRECTIONAL;
438 conn->info.state = EndpointState::OPEN;
439 conn->info.remote_address = peer_addr;
440 conn->info.remote_port = peer_port;
441 conn->info.local_port = lst.info.local_port;
442
443 auto* raw = conn.get();
444
445 {
446 std::unique_lock conn_lock(m_connections_mutex);
447 m_connections[new_id] = std::move(conn);
448 }
449
450 if (m_state_callback) {
451 m_state_callback(raw->info, EndpointState::CLOSED, EndpointState::OPEN);
452 }
453
454 start_receive_chain(*raw);
455
457 "TCP accepted connection {} from {}:{} on listener {}",
458 new_id, peer_addr, peer_port, listener_id);
459
460 start_accept(lst);
461 });
462}
463
464// ─────────────────────────────────────────────────────────────────────────────
465// Private: framed receive chain
466// ─────────────────────────────────────────────────────────────────────────────
467
468void TCPBackend::start_receive_chain(ConnectionState& conn)
469{
470 asio::async_read(
471 conn.socket,
472 asio::buffer(conn.header_buf),
473 [this, ep_id = conn.info.id](const asio::error_code& ec, size_t bytes) {
474 std::shared_lock lock(m_connections_mutex);
475 auto it = m_connections.find(ep_id);
476 if (it == m_connections.end()) {
477 return;
478 }
479 on_header_received(*it->second, ec, bytes);
480 });
481}
482
483void TCPBackend::on_header_received(ConnectionState& conn,
484 const asio::error_code& ec, size_t)
485{
486 if (ec) {
487 on_connection_error(conn, ec);
488 return;
489 }
490
491 uint32_t net_len {};
492 std::memcpy(&net_len, conn.header_buf.data(), sizeof(uint32_t));
493 uint32_t payload_size = ntohl(net_len);
494
495 if (payload_size == 0 || payload_size > 64 * 1024 * 1024) {
497 "TCP endpoint {} received invalid frame length: {}",
498 conn.info.id, payload_size);
499 on_connection_error(conn, asio::error::message_size);
500 return;
501 }
502
503 conn.payload_buf.resize(payload_size);
504
505 asio::async_read(
506 conn.socket,
507 asio::buffer(conn.payload_buf),
508 [this, ep_id = conn.info.id](const asio::error_code& read_ec, size_t bytes) {
509 std::shared_lock lock(m_connections_mutex);
510 auto it = m_connections.find(ep_id);
511 if (it == m_connections.end()) {
512 return;
513 }
514 on_payload_received(*it->second, read_ec, bytes);
515 });
516}
517
518void TCPBackend::on_payload_received(ConnectionState& conn,
519 const asio::error_code& ec, size_t /*bytes*/)
520{
521 if (ec) {
522 on_connection_error(conn, ec);
523 return;
524 }
525
526 if (m_receive_callback) {
527 std::string peer_str = conn.info.remote_address + ":" + std::to_string(conn.info.remote_port);
528
529 m_receive_callback(conn.info.id,
530 conn.payload_buf.data(), conn.payload_buf.size(),
531 peer_str);
532 }
533
534 start_receive_chain(conn);
535}
536
537// ─────────────────────────────────────────────────────────────────────────────
538// Private: error handling and reconnect
539// ─────────────────────────────────────────────────────────────────────────────
540
541void TCPBackend::on_connection_error(ConnectionState& conn, const asio::error_code& ec)
542{
543 if (ec == asio::error::operation_aborted) {
544 return;
545 }
546
547 if (ec == asio::error::eof) {
549 "TCP endpoint {} peer disconnected", conn.info.id);
550 } else {
552 "TCP endpoint {} error: {}", conn.info.id, ec.message());
553 }
554
555 asio::error_code close_ec;
556 if (!conn.socket.close(close_ec)) {
558 "Error closing TCP socket for endpoint {}: {}", conn.info.id, close_ec.message());
559 }
560
561 if (conn.is_outbound && m_config.auto_reconnect) {
562 transition_state(conn.info, EndpointState::RECONNECTING);
563 schedule_reconnect(conn);
564 } else {
565 transition_state(conn.info, EndpointState::ERROR);
566 }
567}
568
569void TCPBackend::schedule_reconnect(ConnectionState& conn)
570{
571 if (!conn.reconnect_timer) {
572 conn.reconnect_timer = std::make_unique<asio::steady_timer>(m_context);
573 }
574
575 conn.reconnect_timer->expires_after(
576 std::chrono::milliseconds(m_config.reconnect_interval_ms));
577
578 conn.reconnect_timer->async_wait(
579 [this, ep_id = conn.info.id](const asio::error_code& ec) {
580 if (ec == asio::error::operation_aborted) {
581 return;
582 }
583
584 std::shared_lock lock(m_connections_mutex);
585 auto it = m_connections.find(ep_id);
586 if (it == m_connections.end()) {
587 return;
588 }
589
590 auto& c = *it->second;
591
592 c.socket = asio::ip::tcp::socket(m_context);
593
595 "TCP endpoint {} attempting reconnect to {}:{}",
596 ep_id, c.info.remote_address, c.info.remote_port);
597
598 start_connect(c);
599 });
600}
601
602void TCPBackend::transition_state(EndpointInfo& info, EndpointState new_state)
603{
604 EndpointState prev = info.state;
605 info.state = new_state;
606
607 if (m_state_callback && prev != new_state) {
608 m_state_callback(info, prev, new_state);
609 }
610}
611
612} // namespace MayaFlux::Core
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
bool size
PickCallback callback
Definition Picker.cpp:22
void close_endpoint(uint64_t endpoint_id) override
Close an endpoint and release its resources.
void start_connect(ConnectionState &conn)
Initiate async_connect for an outbound endpoint.
std::atomic< bool > m_running
std::unordered_map< uint64_t, std::unique_ptr< ConnectionState > > m_connections
std::shared_mutex m_listeners_mutex
void shutdown() override
Release all resources, close all endpoints.
std::unordered_map< uint64_t, std::unique_ptr< ListenerState > > m_listeners
bool initialize() override
Initialise backend resources (sockets, SHM segments, etc.)
bool send(uint64_t endpoint_id, const uint8_t *data, size_t size) override
Send data through an endpoint.
void start_accept(ListenerState &listener)
Initiate async_accept loop for a listener.
std::shared_mutex m_connections_mutex
EndpointState get_endpoint_state(uint64_t endpoint_id) const override
Query the current state of an endpoint.
std::vector< EndpointInfo > get_endpoints() const override
List all endpoints currently managed by this backend.
void stop() override
Stop receive threads without releasing resources.
uint64_t open_endpoint(const EndpointInfo &info) override
Open a new endpoint.
asio::io_context & m_context
TCPBackend(const TCPBackendInfo &config, asio::io_context &context)
Construct with config and a reference to the shared io_context.
EndpointStateCallback m_state_callback
std::atomic< bool > m_initialized
void start() override
Start receive threads and accept connections.
EndpointState
Observable connection state for an endpoint.
std::function< void(uint64_t endpoint_id, const uint8_t *data, size_t size, std::string_view sender_addr)> NetworkReceiveCallback
Callback signature for inbound data on an endpoint.
std::function< void(const EndpointInfo &info, EndpointState previous, EndpointState current)> EndpointStateCallback
Callback signature for endpoint state changes.
@ Shutdown
Engine/subsystem shutdown and cleanup.
@ NetworkBackend
Network transport backend (UDP, TCP, SHM)
@ Init
Engine/subsystem initialization.
@ Core
Core engine, backend, subsystems.
Describes one logical send/receive endpoint managed by a backend.
Configuration for the TCP transport backend.
std::unique_ptr< asio::steady_timer > reconnect_timer
State for a connected TCP peer (inbound or outbound)
State for a listening acceptor.