Program Listing for File client.cpp¶
↰ Return to documentation for file (software/shared/simple-networking/src/client.cpp)
#include "simple_networking/client.hpp"
#include <arpa/inet.h>
#include <net/if.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <cassert>
#include <cstring>
#include <format>
#include <stdexcept>
using std::string;
using namespace networking;
Client::Client(const address_t& address, const socket_protocol protocol, const address_t& bind_address, const socket_config_handlers_t& config_handlers)
: _address(address),
_bind_address(bind_address),
_protocol(protocol)
{
_socket = FdWrapper(std::bind(&Client::_create_socket, this, config_handlers));
}
ssize_t Client::transmit(const string& message)
{
return transmit(std::vector<uint8_t>(message.begin(), message.end()));
}
ssize_t Client::transmit(const std::vector<uint8_t>& buffer)
{
const ssize_t bytes_written = send(static_cast<int>(_socket), buffer.data(), buffer.size(), 0);
if (bytes_written < 0)
{
string err = std::strerror(errno);
throw std::runtime_error("Failed to transmit data: " + err);
}
if (bytes_written == 0 && buffer.size() > 0)
{
throw std::runtime_error("Connection closed: 0 bytes transmitted");
}
return bytes_written;
}
std::optional<std::vector<uint8_t>> Client::receive(size_t len, bool blocking)
{
std::vector<uint8_t> buffer(len);
const ssize_t bytes_read = recv(static_cast<int>(_socket), buffer.data(), len, blocking ? 0 : MSG_DONTWAIT);
if (bytes_read < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
return std::nullopt;
throw std::runtime_error(std::format("Failed to receive packet from {}: {}",
_get_full_address_string(), std::strerror(errno)));
}
if (bytes_read == 0)
{
if (len > 0)
throw std::runtime_error(std::format("Connection closed: 0 bytes received from {}",
_get_full_address_string()));
return std::nullopt;
}
buffer.resize(bytes_read);
return buffer;
}
int Client::_create_bound_socket(struct addrinfo*& current_addr, struct addrinfo* current_bind_addr, const socket_config_handlers_t& config_handlers)
{
bool should_skip_bind = current_bind_addr == nullptr;
while (current_addr && (should_skip_bind || current_bind_addr))
{
int socket_fd = socket(current_addr->ai_family, current_addr->ai_socktype, current_addr->ai_protocol);
if (socket_fd == -1)
{
current_addr = current_addr->ai_next;
continue;
}
if (current_bind_addr)
{
const int yes = 1;
// enable address reuse (prevents "address already in use" errors)
if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0)
{
close(socket_fd);
current_bind_addr = current_bind_addr->ai_next;
continue;
}
if (config_handlers.pre_bind)
{
if (!config_handlers.pre_bind(socket_fd))
{
close(socket_fd);
current_bind_addr = current_bind_addr->ai_next;
continue;
}
}
if (bind(socket_fd, current_bind_addr->ai_addr, current_bind_addr->ai_addrlen) < 0)
{
close(socket_fd);
current_bind_addr = current_bind_addr->ai_next;
continue;
}
}
return socket_fd;
}
// if we hit this, socket creation for all addresses failed
if (!current_addr)
{
throw std::runtime_error(std::format("Failed to create socket for {}: {}",
_get_full_address_string(), std::strerror(errno)));
}
// and if we hit this, binding failed
if (!should_skip_bind && !current_bind_addr)
{
throw std::runtime_error(std::format("Failed to bind address {}: {}",
static_cast<string>(_bind_address), std::strerror(errno)));
}
// we should never hit this
assert(false);
// make the compiler happy
return -1;
}
#include <iostream>
int Client::_create_socket(const socket_config_handlers_t& config_handlers)
{
// perform address resolution
struct addrinfo hints{};
std::fill_n(reinterpret_cast<uint8_t*>(&hints), sizeof(hints), 0);
hints.ai_family = AF_UNSPEC; // don't care whether it's IPv4 or IPv6
hints.ai_socktype = (_protocol == socket_protocol::TCP) ? SOCK_STREAM : SOCK_DGRAM;
if (const int status =
getaddrinfo(_address.hostname.c_str(), _address.service.c_str(), &hints, &_destination_addrinfo);
status != 0)
{
throw std::runtime_error(std::format("Failed to resolve address {}: {}",
_get_full_address_string(),
gai_strerror(status)));
}
if (!_bind_address.service.empty())
{
hints.ai_flags = AI_PASSIVE; // wildcard IP address - will be ignored if hostname is provided
const char* const bind_hostname = _bind_address.hostname.empty() ? nullptr : _bind_address.hostname.c_str();
if (const int status =
getaddrinfo(bind_hostname, _bind_address.service.c_str(), &hints, &_bind_addrinfo);
status != 0)
{
throw std::runtime_error(std::format("Failed to resolve bind address {}: {}",
static_cast<string>(_bind_address),
gai_strerror(status)));
}
}
for (auto current_addr = _destination_addrinfo.get(); current_addr; current_addr = current_addr->ai_next)
{
int socket_fd = _create_bound_socket(current_addr, _bind_addrinfo.get(), config_handlers);
if (config_handlers.pre_connect)
{
if (!config_handlers.pre_connect(socket_fd))
{
close(socket_fd);
// same thing here
if (!current_addr->ai_next)
{
throw std::runtime_error(std::format("Failed to configure socket pre-connection for {}: {}",
_get_full_address_string(), std::strerror(errno)));
}
continue;
}
}
if (connect(socket_fd, _destination_addrinfo->ai_addr, _destination_addrinfo->ai_addrlen) < 0)
{
close(socket_fd);
// if we've run out of addresses to try, it's failed
if (!current_addr->ai_next)
{
throw std::runtime_error(std::format("Failed to connect to {}: {}",
_get_full_address_string(), std::strerror(errno)));
}
continue;
}
if (config_handlers.post_connect)
{
if (!config_handlers.post_connect(socket_fd))
{
close(socket_fd);
// same thing here
if (!current_addr->ai_next)
{
throw std::runtime_error(std::format("Failed to configure socket post-connection for {}: {}",
_get_full_address_string(), std::strerror(errno)));
}
continue;
}
}
// free the lists, we don't need them any more
_destination_addrinfo = nullptr;
_bind_addrinfo = nullptr;
return socket_fd;
}
// we should never hit this - any failure by this point should have thrown an exception
assert(false);
// make the compiler happy
return -1;
}
string Client::_get_full_address_string() const
{
return std::format("{} ({})", static_cast<string>(_address), get_string_from_protocol(_protocol));
}