Compare commits

..

No commits in common. "da6159d7067622b8b615e2620f71f6d993dcd0a2" and "1847a0ea0ccf5f7c9997f8c5cc1206482c1695e7" have entirely different histories.

5 changed files with 175 additions and 234 deletions

View File

@ -54,6 +54,7 @@ target_link_libraries(playground PRIVATE spdlog::spdlog)
#
enable_testing()
add_executable(

View File

@ -1,15 +1,12 @@
// Library includes
#include <spdlog/spdlog.h>
// Project includes
#include "tcp.hpp"
#include <cstddef>
#include <spdlog/spdlog.h>
int main() {
spdlog::set_level(spdlog::level::debug);
tcp::NonBlockingServer server{};
if (auto res = server.start_listening(65432); !res) {
tcp::NonBlockingServer server{{.port = 65432}};
if (auto res = server.start_listening(); !res) {
spdlog::error("Failed to start server: {}", res.error());
return 1;
}
@ -50,6 +47,6 @@ int main() {
spdlog::info("Sent {} bytes", sendRes.value());
}
server.disconnect_from_client();
server.disconnect();
}
}

View File

@ -1,12 +1,18 @@
#include "tcp.hpp"
// C stdlib includes
#include <arpa/inet.h>
#include <cerrno>
#include <cstdint>
#include <expected>
#include <fcntl.h>
#include <netdb.h>
// Library includes
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <span>
#include <spdlog/spdlog.h>
#include <sys/socket.h>
#include <sys/types.h>
namespace {
@ -30,7 +36,7 @@ std::expected<sockaddr_in, int> resolve_remote_address(const char* host,
return address;
}
inline sockaddr_in get_local_address(uint16_t port) {
inline auto get_local_address(uint16_t port) -> sockaddr_in {
sockaddr_in address{};
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_family = AF_INET;
@ -39,6 +45,7 @@ inline sockaddr_in get_local_address(uint16_t port) {
}
} // namespace
@ -52,49 +59,43 @@ namespace tcp {
//
NonBlockingServer::NonBlockingServer(ServerSettings settings)
: m_settings{settings}, m_serverAddress(get_local_address(settings.port)) {
}
NonBlockingServer::~NonBlockingServer() {
close_client_socket();
close_server_socket();
}
///
/// @details Calling this repeatedly closes the previous socket and creates a
/// new one.
///
std::expected<void, int> NonBlockingServer::start_listening(uint16_t port) {
std::expected<void, int> NonBlockingServer::start_listening() {
/// Create socket
close_server_socket();
if (auto res = create_server_socket(); !res) {
if (auto res = create_socket(); !res) {
return std::unexpected{res.error()};
}
/// Bin socket and start listening
const struct sockaddr_in serverAddress = get_local_address(port);
if (::bind(m_serverSocket, (struct sockaddr*)&serverAddress,
sizeof(serverAddress)) != 0) {
if (::bind(m_serverSocket, (struct sockaddr*)&m_serverAddress,
sizeof(m_serverAddress)) != 0) {
spdlog::error("tcp::NonBlockingServer: bind() failed with errno={}",
errno);
close_server_socket();
close(m_serverSocket);
return std::unexpected{errno};
};
if (::listen(m_serverSocket, 1) != 0) {
spdlog::error("tcp::NonBlockingServer: listen() failed with errno={}",
errno);
close_server_socket();
close(m_serverSocket);
return std::unexpected{errno};
}
return {};
}
void NonBlockingServer::stop_listening() {
close_server_socket();
}
bool NonBlockingServer::next_client_available() {
int ret = poll(&m_serverPfdIn, 1, 0);
if (ret < 0) {
@ -107,12 +108,10 @@ bool NonBlockingServer::next_client_available() {
}
[[nodiscard]] std::expected<void, int> NonBlockingServer::accept_next_client() {
disconnect_from_client();
socklen_t clientAddrLen = sizeof(m_clientAddress);
disconnect();
int result = ::accept(m_serverSocket, (struct sockaddr*)&m_clientAddress,
&clientAddrLen);
&m_clientAddrLen);
if (result < 0) return std::unexpected{errno};
m_clientSocket = result;
@ -120,13 +119,16 @@ bool NonBlockingServer::next_client_available() {
return {};
}
void NonBlockingServer::disconnect_from_client() {
void NonBlockingServer::disconnect() {
close_client_socket();
}
// TODO: Do we need this?
// bool NonBlockingServer::socket_closed() const {
// }
[[nodiscard]] std::expected<std::array<char, 16>, int>
NonBlockingServer::get_client_ip_as_string() {
NonBlockingServer::get_client_ip_as_string() const {
std::array<char, 16> addrStr;
if (inet_ntop(AF_INET, &((struct sockaddr_in*)&m_clientAddress)->sin_addr,
@ -141,22 +143,11 @@ NonBlockingServer::get_client_ip_as_string() {
// return std::unexpected{errno};
// }
return addrStr;
}
bool NonBlockingServer::data_available() {
int ret = poll(&m_clientPfdIn, 1, 0);
if (ret < 0) {
spdlog::error("tcp::NonBlockingServer: poll() failed with errno={}",
errno);
return false;
}
return (m_clientPfdIn.revents & POLLIN);
return {addrStr};
}
[[nodiscard]] std::expected<int, int>
NonBlockingServer::send(std::span<const std::byte> data) {
NonBlockingServer::send(std::span<const std::byte> data) const {
const int bytesWritten =
::send(m_clientSocket, (const char*)data.data(), data.size(), 0);
@ -170,7 +161,7 @@ NonBlockingServer::send(std::span<const std::byte> data) {
}
[[nodiscard]] std::expected<int, int>
NonBlockingServer::recv(std::span<std::byte> buffer) {
NonBlockingServer::recv(std::span<std::byte> buffer) const {
const int bytesReceived =
::recv(m_clientSocket, (char*)buffer.data(), buffer.size(), 0);
@ -182,13 +173,22 @@ NonBlockingServer::recv(std::span<std::byte> buffer) {
return {bytesReceived};
}
[[nodiscard]] std::expected<void, int>
NonBlockingServer::create_server_socket() {
bool NonBlockingServer::data_available() {
int ret = poll(&m_clientPfdIn, 1, 0);
if (ret < 0) {
spdlog::error("tcp::NonBlockingServer: poll() failed with errno={}",
errno);
return false;
}
return (m_clientPfdIn.revents & POLLIN);
}
[[nodiscard]] std::expected<void, int> NonBlockingServer::create_socket() {
m_serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (m_serverSocket < 1) {
spdlog::error("tcp::NonBlockingServer: socket() failed with errno={}",
errno);
close_server_socket();
return std::unexpected{errno};
}
@ -264,7 +264,7 @@ NonBlockingClient::~NonBlockingClient() {
}
[[nodiscard]] std::expected<void, int>
NonBlockingClient::connect(const HostString& host, uint16_t port) {
NonBlockingClient::connect(HostString host, uint16_t port) {
/// Resolve host
auto addrRes = resolve_remote_address(host.data(), port);
@ -294,27 +294,13 @@ NonBlockingClient::connect(const HostString& host, uint16_t port) {
return {};
}
void NonBlockingClient::disconnect() {
m_lastKnownConStatus = ENOTCONN;
close_socket();
}
///
/// @brief Get last known connection status
/// @return - expected(EINPROGRESS) if connection is still being attempted
/// - expected(EISCONN) if already connected
/// - expected(error code) if an error occurred during connection (e.g.,
/// ECONNREFUSED)
/// - unexpected(errno) if an error occurred while checking connection
/// status
///
[[nodiscard]] std::expected<int, int>
NonBlockingClient::get_last_connection_status() {
if (!m_startedNewConAttempt) return m_lastKnownConStatus;
/// Check if connect operation has been completed
auto pendingOpRes = socket_has_new_output_event();
auto pendingOpRes = new_error_codes_available();
if (!pendingOpRes) return std::unexpected{pendingOpRes.error()};
if (!pendingOpRes.value()) return EINPROGRESS;
@ -339,6 +325,33 @@ NonBlockingClient::get_last_connection_status() {
return m_lastKnownConStatus;
}
void NonBlockingClient::disconnect() {
m_lastKnownConStatus = ENOTCONN;
close_socket();
}
[[nodiscard]] std::expected<int, int>
NonBlockingClient::send(std::span<const std::byte> data) {
// TODO: Do we need this?
auto conRes = get_last_connection_status();
if (!conRes) return std::unexpected{conRes.error()};
if (conRes.value() != EISCONN) {
spdlog::error("tcp::NonBlockingClient: Socket not connected");
return std::unexpected{ENOTCONN};
}
const int bytesWritten =
::send(m_socket, (const char*)data.data(), data.size(), 0);
if (bytesWritten <= 0) {
spdlog::error("tcp::NonBlockingClient: send() failed with errno={}",
errno);
return std::unexpected{errno};
}
return bytesWritten;
}
bool NonBlockingClient::data_available() {
auto conRes = get_last_connection_status();
if (!conRes) return false;
@ -354,20 +367,6 @@ bool NonBlockingClient::data_available() {
return (m_pfdIn.revents & POLLIN);
}
[[nodiscard]] std::expected<int, int>
NonBlockingClient::send(std::span<const std::byte> data) {
const int bytesWritten =
::send(m_socket, (const char*)data.data(), data.size(), 0);
if (bytesWritten <= 0) {
spdlog::error("tcp::NonBlockingClient: send() failed with errno={}",
errno);
return std::unexpected{errno};
}
return bytesWritten;
}
[[nodiscard]] std::expected<int, int>
NonBlockingClient::recv(std::span<std::byte> buffer) {
const int bytesReceived =
@ -381,12 +380,27 @@ NonBlockingClient::recv(std::span<std::byte> buffer) {
return {bytesReceived};
}
void NonBlockingClient::close_socket() {
if (shutdown(m_socket, 0) < 0) {
spdlog::debug("tcp::NonBlockingClient: shutdown() failed with errno={}",
errno);
}
if (close(m_socket) < 0) {
spdlog::debug("tcp::NonBlockingClient: close() failed with errno={}",
errno);
}
m_socket = -1;
m_pfdOut.fd = -1;
m_pfdIn.fd = -1;
}
[[nodiscard]] std::expected<void, int> NonBlockingClient::create_socket() {
m_socket = socket(AF_INET, SOCK_STREAM, 0);
if (m_socket < 1) {
spdlog::error("tcp::NonBlockingClient: socket() failed with errno={}",
errno);
close_socket();
return std::unexpected{errno};
}
@ -411,29 +425,8 @@ NonBlockingClient::recv(std::span<std::byte> buffer) {
return {};
}
void NonBlockingClient::close_socket() {
if (shutdown(m_socket, 0) < 0) {
spdlog::debug("tcp::NonBlockingClient: shutdown() failed with errno={}",
errno);
}
if (close(m_socket) < 0) {
spdlog::debug("tcp::NonBlockingClient: close() failed with errno={}",
errno);
}
m_socket = -1;
m_pfdOut.fd = -1;
m_pfdIn.fd = -1;
}
///
/// @return true if there is new output event, i.e., the connect() operation has
/// completed (successfully or not)
///
[[nodiscard]] std::expected<bool, int>
NonBlockingClient::socket_has_new_output_event() {
NonBlockingClient::new_error_codes_available() {
int ret = poll(&m_pfdOut, 1, 0);
if (ret < 0) {
spdlog::error("tcp::NonBlockingClient: poll() failed with errno={}",

View File

@ -1,16 +1,24 @@
#pragma once
// STL includes
#include <array>
#include <cerrno>
#include <cstdint>
#include <expected>
#include <poll.h>
#include <span>
#include <spdlog/spdlog.h>
#include <sys/socket.h>
// C stdlib includes
#include <arpa/inet.h>
#include <expected>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/poll.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <sys/types.h>
namespace tcp {
@ -18,48 +26,52 @@ namespace tcp {
using HostString = std::array<char, 64>;
//
//
// Non blocking server
//
//
struct ServerSettings {
uint16_t port = 0;
};
class NonBlockingServer {
public:
NonBlockingServer() = default;
NonBlockingServer(const NonBlockingServer&) = delete;
NonBlockingServer(NonBlockingServer&&) = delete;
NonBlockingServer& operator=(const NonBlockingServer&) = delete;
NonBlockingServer& operator=(NonBlockingServer&&) = delete;
NonBlockingServer(ServerSettings settings);
~NonBlockingServer();
[[nodiscard]] std::expected<void, int> start_listening(uint16_t port);
void stop_listening();
[[nodiscard]] std::expected<void, int> start_listening();
bool next_client_available();
[[nodiscard]] std::expected<void, int> accept_next_client();
void disconnect_from_client();
void disconnect();
[[nodiscard]] std::expected<std::array<char, 16>, int>
get_client_ip_as_string();
[[nodiscard]] std::expected<int, int>
send(std::span<const std::byte> data) const;
[[nodiscard]] std::expected<int, int>
recv(std::span<std::byte> buffer) const;
bool data_available();
[[nodiscard]] std::expected<int, int> send(std::span<const std::byte> data);
[[nodiscard]] std::expected<int, int> recv(std::span<std::byte> buffer);
[[nodiscard]] std::expected<std::array<char, 16>, int>
get_client_ip_as_string() const;
private:
int m_serverSocket = -1;
ServerSettings m_settings;
const struct sockaddr_in m_serverAddress;
int m_serverSocket = -1;
pollfd m_serverPfdIn = {.fd = -1, .events = POLLIN};
pollfd m_clientPfdIn = {.fd = -1, .events = POLLIN};
int m_clientSocket = -1;
struct sockaddr m_clientAddress = {0};
int m_clientSocket = -1;
struct sockaddr m_clientAddress;
socklen_t m_clientAddrLen = sizeof(m_clientAddress);
[[nodiscard]] std::expected<void, int> create_server_socket();
[[nodiscard]] std::expected<void, int> create_socket();
void close_server_socket();
void close_client_socket();
};
@ -74,15 +86,12 @@ private:
class NonBlockingClient {
public:
NonBlockingClient() = default;
NonBlockingClient(const NonBlockingClient&) = delete;
NonBlockingClient(NonBlockingClient&&) = delete;
NonBlockingClient& operator=(const NonBlockingClient&) = delete;
NonBlockingClient& operator=(NonBlockingClient&&) = delete;
~NonBlockingClient();
[[nodiscard]] std::expected<void, int> connect(const HostString& host,
uint16_t port);
// clang-format off
[[nodiscard]] std::expected<void, int>
connect(HostString host, uint16_t port);
// clang-format on
void disconnect();
@ -90,8 +99,8 @@ public:
bool data_available();
[[nodiscard]] std::expected<int, int> send(std::span<const std::byte> data);
[[nodiscard]] std::expected<int, int> recv(std::span<std::byte> buffer);
[[nodiscard]] std::expected<int, int> send(std::span<const std::byte> data);
private:
int m_socket = -1;
@ -101,10 +110,10 @@ private:
bool m_startedNewConAttempt = false;
int m_lastKnownConStatus = ENOTCONN;
[[nodiscard]] std::expected<void, int> create_socket();
void close_socket();
void close_socket();
[[nodiscard]] std::expected<bool, int> socket_has_new_output_event();
[[nodiscard]] std::expected<void, int> create_socket();
[[nodiscard]] std::expected<bool, int> new_error_codes_available();
};

View File

@ -1,17 +1,13 @@
#include <gtest/gtest.h>
// STL includes
#include <cstring>
// Project includes
#include "tcp.hpp"
#include <gtest/gtest.h>
TEST(TcpServer, Accept) {
tcp::NonBlockingServer server;
tcp::NonBlockingServer server{{.port = 1234}};
tcp::NonBlockingClient client;
auto lisRes = server.start_listening(1234);
auto lisRes = server.start_listening();
EXPECT_FALSE(server.next_client_available());
@ -24,86 +20,8 @@ TEST(TcpServer, Accept) {
EXPECT_FALSE(server.next_client_available());
}
TEST(TcpServer, RestartListening) {
tcp::NonBlockingServer server;
tcp::NonBlockingClient client;
auto lisRes = server.start_listening(1234);
auto conRes = client.connect({"localhost"}, 1234);
EXPECT_TRUE(server.next_client_available());
auto lisRes2 = server.start_listening(2345);
EXPECT_FALSE(server.next_client_available());
auto conRes2 = client.connect({"localhost"}, 2345);
EXPECT_TRUE(server.next_client_available());
auto statRes2 = client.get_last_connection_status();
if (!statRes2) return;
EXPECT_EQ(statRes2.value(), EISCONN);
}
TEST(TcpServer, StopListening) {
tcp::NonBlockingServer server;
tcp::NonBlockingClient client;
auto lisRes = server.start_listening(1234);
auto conRes = client.connect({"localhost"}, 1234);
EXPECT_TRUE(server.next_client_available());
server.stop_listening();
EXPECT_FALSE(server.next_client_available());
auto conRes2 = client.connect({"localhost"}, 1234);
EXPECT_EQ(client.get_last_connection_status().value(), ECONNREFUSED);
}
TEST(TcpServer, DataAvailable) {
tcp::NonBlockingServer server;
tcp::NonBlockingClient client;
EXPECT_FALSE(server.data_available());
auto lisRes = server.start_listening(1234);
auto conRes = client.connect({"localhost"}, 1234);
auto accRes = server.accept_next_client();
EXPECT_FALSE(server.data_available());
std::array<char, 14> txMsg = {"Hello, World!"};
auto sendRes = client.send(std::as_bytes(std::span(txMsg)));
EXPECT_EQ(sendRes.value(), txMsg.size());
EXPECT_TRUE(server.data_available());
std::array<char, 1024> rxBuffer;
auto recvRes = server.recv(std::as_writable_bytes(std::span(rxBuffer)));
EXPECT_EQ(recvRes.value(), txMsg.size());
EXPECT_FALSE(server.data_available());
}
TEST(TcpServer, GetClientIPAsString) {
tcp::NonBlockingServer server;
tcp::NonBlockingClient client;
auto lisRes = server.start_listening(1234);
auto conRes = client.connect({"localhost"}, 1234);
auto accRes = server.accept_next_client();
auto ipRes = server.get_client_ip_as_string();
if (!ipRes) return;
EXPECT_STREQ(ipRes.value().data(), "127.0.0.1");
}
TEST(TcpClient, Connect) {
tcp::NonBlockingServer server;
tcp::NonBlockingServer server{{.port = 1234}};
tcp::NonBlockingClient client;
/// Default connection status, i.e., before attempting connection
@ -122,7 +40,7 @@ TEST(TcpClient, Connect) {
/// Connection status when connection is acknowledged
auto lisRes = server.start_listening(1234);
auto lisRes = server.start_listening();
auto conRes2 = client.connect({"localhost"}, 1234);
@ -140,12 +58,12 @@ TEST(TcpClient, Connect) {
}
TEST(TcpClient, Reconnect) {
tcp::NonBlockingServer server1;
tcp::NonBlockingServer server2;
tcp::NonBlockingServer server1{{.port = 1234}};
tcp::NonBlockingServer server2{{.port = 2345}};
tcp::NonBlockingClient client;
auto lisRes1 = server1.start_listening(1234);
auto lisRes2 = server2.start_listening(2345);
auto lisRes1 = server1.start_listening();
auto lisRes2 = server2.start_listening();
auto conRes1 = client.connect({"localhost"}, 1234);
@ -161,12 +79,12 @@ TEST(TcpClient, Reconnect) {
}
TEST(TcpClient, DataAvailable) {
tcp::NonBlockingServer server;
tcp::NonBlockingServer server{{.port = 1234}};
tcp::NonBlockingClient client;
EXPECT_FALSE(client.data_available());
auto lisRes = server.start_listening(1234);
auto lisRes = server.start_listening();
auto conRes = client.connect({"localhost"}, 1234);
auto accRes = server.accept_next_client();
@ -186,11 +104,34 @@ TEST(TcpClient, DataAvailable) {
EXPECT_FALSE(client.data_available());
}
TEST(TcpClientServer, ClientSendServerReceive) {
tcp::NonBlockingServer server;
TEST(TcpServer, DataAvailable) {
tcp::NonBlockingServer server{{.port = 1234}};
tcp::NonBlockingClient client;
auto lisRes = server.start_listening(1234);
EXPECT_FALSE(server.data_available());
auto lisRes = server.start_listening();
auto conRes = client.connect({"localhost"}, 1234);
auto accRes = server.accept_next_client();
EXPECT_FALSE(server.data_available());
std::array<char, 14> txMsg = {"Hello, World!"};
auto sendRes = client.send(std::as_bytes(std::span(txMsg)));
EXPECT_EQ(sendRes.value(), txMsg.size());
EXPECT_TRUE(server.data_available());
std::array<char, 1024> rxBuffer;
auto recvRes = server.recv(std::as_writable_bytes(std::span(rxBuffer)));
EXPECT_EQ(recvRes.value(), txMsg.size());
EXPECT_FALSE(server.data_available());
}
TEST(TcpClientServer, ClientSendServerReceive) {
tcp::NonBlockingServer server{{.port = 1234}};
tcp::NonBlockingClient client;
auto lisRes = server.start_listening();
auto conRes = client.connect({"localhost"}, 1234);
auto accRes = server.accept_next_client();
@ -207,10 +148,10 @@ TEST(TcpClientServer, ClientSendServerReceive) {
}
TEST(TcpClientServer, ClientReceiveServerSend) {
tcp::NonBlockingServer server;
tcp::NonBlockingServer server{{.port = 1234}};
tcp::NonBlockingClient client;
auto lisRes = server.start_listening(1234);
auto lisRes = server.start_listening();
auto conRes = client.connect({"localhost"}, 1234);
auto accRes = server.accept_next_client();