File plugin.cpp
File List > plugins > tcp_network_backend > plugin.cpp
Go to the documentation of this file
#include "plugin.hpp"
#include
static constexpr uint32_t MAX_PACKET_BYTES = 256u * 1024u * 1024u;
using namespace ILLIXR;
tcp_network_backend::tcp_network_backend(const std::string& name_, phonebook* pb_)
: plugin(name_, pb_)
, switchboard_{pb_->lookup_impl<switchboard>()} {
// read environment variables
if (switchboard_->get_env_char("ILLIXR_SERVER_IP")) {
server_ip_ = switchboard_->get_env_char("ILLIXR_SERVER_IP");
spdlog::get("illixr")->info("[tcp_network_backend] Using server IP {}", server_ip_);
} else if (switchboard_->get_env_char("ILLIXR_TCP_SERVER_IP")) {
server_ip_ = switchboard_->get_env_char("ILLIXR_TCP_SERVER_IP");
spdlog::get("illixr")->info("[tcp_network_backend] Using TCP server IP {}", server_ip_);
}
if (switchboard_->get_env_char("ILLIXR_TCP_SERVER_PORT")) {
server_port_ = std::stoi(switchboard_->get_env_char("ILLIXR_TCP_SERVER_PORT"));
spdlog::get("illixr")->info("[tcp_network_backend] Using TCP server port {}", server_port_);
}
if (switchboard_->get_env_char("ILLIXR_CLIENT_IP")) {
client_ip_ = switchboard_->get_env_char("ILLIXR_CLIENT_IP");
spdlog::get("illixr")->info("[tcp_network_backend] Using client IP {}", client_ip_);
} else if (switchboard_->get_env_char("ILLIXR_TCP_CLIENT_IP")) {
client_ip_ = switchboard_->get_env_char("ILLIXR_TCP_CLIENT_IP");
spdlog::get("illixr")->info("[tcp_network_backend] Using TCP client IP {}", client_ip_);
}
if (switchboard_->get_env_char("ILLIXR_TCP_CLIENT_PORT")) {
client_port_ = std::stoi(switchboard_->get_env_char("ILLIXR_TCP_CLIENT_PORT"));
spdlog::get("illixr")->info("[tcp_network_backend] Using TCP client port {}", client_port_);
}
if (switchboard_->get_env_char("ILLIXR_IS_CLIENT")) {
is_client_ = std::stoi(switchboard_->get_env_char("ILLIXR_IS_CLIENT"));
spdlog::get("illixr")->info("[tcp_network_backend] Is client {}", is_client_);
} else {
is_client_ = 0;
}
if (is_client_) {
client = true;
#ifdef __ANDROID__
// The Quest may launch before desktop ILLIXR. Keep the app resident and
// retry until the host starts listening or shutdown is requested.
while (running_) {
auto* socket = new network::TCPSocket();
try {
socket->socket_set_reuseaddr();
if (switchboard_->get_env_char("ILLIXR_TCP_CLIENT_IP") &&
switchboard_->get_env_char("ILLIXR_TCP_CLIENT_PORT")) {
socket->socket_bind(client_ip_, client_port_);
}
spdlog::get("illixr")->info("[tcp_network_backend] Connecting to {}:{}", server_ip_, server_port_);
socket->socket_connect(server_ip_, server_port_);
socket->enable_no_delay();
peer_socket_ = socket;
spdlog::get("illixr")->info("[tcp_network_backend] Connected; TCP_NODELAY={}", socket->is_no_delay());
break;
} catch (const std::exception& error) {
delete socket;
spdlog::get("illixr")->warn("[tcp_network_backend] Desktop is not ready ({}); retrying", error.what());
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
#else
io_thread_ = std::thread([this]() {
start_client();
});
// wait till we are connected
while (!ready_) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
#endif
} else {
client = false;
#ifdef __ANDROID__
server_socket_.socket_set_reuseaddr();
server_socket_.socket_bind(server_ip_, server_port_);
server_socket_.enable_no_delay();
server_socket_.socket_listen();
auto* client_socket = new network::TCPSocket(server_socket_.socket_accept());
// Set linger-zero on the accepted socket so it sends RST on close
// rather than entering TIME_WAIT after a force-quit.
client_socket->socket_set_linger_zero();
spdlog::get("illixr")->debug("Accepted connection from client: " + client_socket->peer_address());
peer_socket_ = client_socket;
#else
io_thread_ = std::thread([this]() {
start_server();
});
while (!ready_) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
#endif
}
}
#ifdef __ANDROID__
void tcp_network_backend::start() {
plugin::start();
io_thread_ = std::thread([this]() {
read_loop(peer_socket_);
});
}
#else
void tcp_network_backend::start_client() {
auto* socket = new network::TCPSocket();
if (switchboard_->get_env_char("ILLIXR_TCP_CLIENT_IP") && switchboard_->get_env_char("ILLIXR_TCP_CLIENT_PORT")) {
// socket_bind() sets SO_REUSEADDR, SO_REUSEPORT, and SO_LINGER=0 internally.
socket->socket_bind(client_ip_, client_port_);
} else {
socket->socket_set_linger_zero();
}
socket->socket_set_reuseaddr();
socket->enable_no_delay();
peer_socket_ = socket;
std::cout << "Connecting to " + server_ip_ + " at port " + std::to_string(server_port_) << std::endl;
socket->socket_connect(server_ip_, server_port_);
std::cout << "Connected to server" << std::endl;
ready_ = true;
read_loop(socket);
}
void tcp_network_backend::start_server() {
network::TCPSocket server_socket;
server_socket.socket_set_reuseaddr();
server_socket.socket_bind(server_ip_, server_port_);
server_socket.enable_no_delay();
server_socket.socket_listen();
auto* client_socket = new network::TCPSocket(server_socket.socket_accept());
client_socket->socket_set_linger_zero();
client_socket->enable_no_delay();
spdlog::get("illixr")->info("[tcp_network_backend] TCP_NODELAY verified = {}", client_socket->is_no_delay());
std::cout << "Accepted connection from client: " << client_socket->peer_address() << std::endl;
peer_socket_ = client_socket;
ready_ = true;
read_loop(client_socket);
}
#endif
void tcp_network_backend::read_loop(network::TCPSocket* socket) {
std::string buffer;
while (running_) {
// read from socket
// packet are in the format
// total_length:4bytes|topic_name_length:4bytes|topic_name|message
#ifdef __ANDROID__
std::string packet = socket->read_data(10000);
#else
std::string packet = socket->read_data();
#endif
if (packet.empty()) {
if (running_.exchange(false)) {
spdlog::get("illixr")->error("[tcp_network_backend] TCP connection closed or read failed; restart the session");
}
return;
}
buffer += packet;
// check if we have a complete packet
while (buffer.size() >= 8) {
uint32_t total_length;
uint32_t topic_name_length;
std::memcpy(&total_length, buffer.data(), sizeof(total_length));
std::memcpy(&topic_name_length, buffer.data() + 4, sizeof(topic_name_length));
if (total_length < 8 || total_length > MAX_PACKET_BYTES || topic_name_length > total_length - 8) {
spdlog::get("illixr")->error("[tcp_network_backend] malformed packet header (total_length={}, "
"topic_name_length={}, buffered={} B) -- stream is desynced, "
"closing the read loop",
total_length, topic_name_length, buffer.size());
running_ = false;
return;
}
if (buffer.size() >= total_length) {
std::string topic_name(buffer.data() + 8, topic_name_length);
std::vector<char> message(buffer.begin() + 8 + topic_name_length, buffer.begin() + total_length);
try {
topic_receive(topic_name, message);
} catch (const std::exception& error) {
spdlog::get("illixr")->error(
"[tcp_network_backend] Failed to deserialize topic={} bytes={}: {}; restart the session", topic_name,
message.size(), error.what());
running_ = false;
return;
}
buffer.erase(buffer.begin(), buffer.begin() + total_length);
} else {
break;
}
}
}
}
void tcp_network_backend::topic_create(std::string topic_name, network::topic_config& config) {
networked_topics_.push_back(topic_name);
networked_topics_configs_[topic_name] = config;
std::string serialization;
if (config.serialization_method == network::topic_config::SerializationMethod::BOOST) {
serialization = "BOOST";
} else {
serialization = "PROTOBUF";
}
std::string message = "create_topic" + topic_name + delimiter_ + serialization;
send_to_peer("illixr_control", std::move(message));
}
bool tcp_network_backend::is_topic_networked(std::string topic_name) {
return std::find(networked_topics_.begin(), networked_topics_.end(), topic_name) != networked_topics_.end();
}
void tcp_network_backend::topic_send(std::string topic_name, std::string&& message) {
if (!is_topic_networked(topic_name)) {
std::cout << "Topic not networked" << std::endl;
return;
}
send_to_peer(topic_name, std::move(message));
}
// Helper function to queue a received message into the corresponding topic
void tcp_network_backend::topic_receive(const std::string& topic_name, std::vector<char>& message) {
if (topic_name == "illixr_control") {
std::string message_str(message.begin(), message.end());
// check if message starts with "create_topic"
if (message_str.find("create_topic") == 0) {
size_t d_pos = message_str.find(delimiter_);
assert(d_pos != std::string::npos);
std::string l_topic_name = message_str.substr(12, d_pos - 12);
std::string serialization = message_str.substr(d_pos + 1);
networked_topics_.push_back(l_topic_name);
network::topic_config config;
if (serialization == "BOOST") {
config.serialization_method = network::topic_config::SerializationMethod::BOOST;
} else {
config.serialization_method = network::topic_config::SerializationMethod::PROTOBUF;
}
networked_topics_configs_[l_topic_name] = config;
std::cout << "Received create_topic for " << l_topic_name << std::endl;
}
return;
}
if (!switchboard_->topic_exists(topic_name)) {
return;
}
switchboard_->get_topic(topic_name).deserialize_and_put(message, networked_topics_configs_[topic_name]);
}
void tcp_network_backend::stop() {
std::lock_guard<std::mutex> lock{stop_mutex_};
// shutdown() wakes read_loop without invalidating the descriptor; deletion
// is deferred until the owning thread has returned. A read/send failure may
// already have cleared running_, but its worker still needs to be joined.
running_.store(false);
if (peer_socket_ != nullptr) {
peer_socket_->socket_shutdown();
}
if (io_thread_.joinable() && io_thread_.get_id() != std::this_thread::get_id()) {
io_thread_.join();
}
delete peer_socket_;
peer_socket_ = nullptr;
plugin::stop();
}
tcp_network_backend::~tcp_network_backend() {
stop();
}
void tcp_network_backend::send_to_peer(const std::string& topic_name, std::string&& message) {
// packet are in the format
// total_length:4bytes|topic_name_length:4bytes|topic_name|message
uint32_t total_length = 8 + topic_name.size() + message.size();
std::string packet;
packet.append(reinterpret_cast<char*>(&total_length), 4);
uint32_t topic_name_length = topic_name.size();
packet.append(reinterpret_cast<char*>(&topic_name_length), 4);
packet.append(topic_name);
packet.append(message.begin(), message.end());
if (!running_)
return;
try {
std::lock_guard<std::mutex> lock{send_mutex_};
peer_socket_->write_data(packet);
} catch (const std::exception& error) {
running_ = false;
spdlog::get("illixr")->error("[tcp_network_backend] TCP send failed for topic={}: {}; restart the session", topic_name,
error.what());
}
}
extern "C" MY_EXPORT_API plugin* this_plugin_factory(phonebook* pb) {
auto* obj = new tcp_network_backend("tcp_network_backend", pb);
// The runtime owns the plugin returned by this factory. Register a non-owning
// service alias so the phonebook does not try to delete the same object again.
pb->register_impl<network::tcp_backend>(
std::shared_ptr<network::tcp_backend>(static_cast<network::tcp_backend*>(obj), [](network::tcp_backend*) { }));
return obj;
}