Add first tcp server implementation

This commit is contained in:
Andreas Tsouchlos 2025-04-18 21:52:54 +02:00
commit 72121036a6
9 changed files with 653 additions and 0 deletions

14
.clang-tidy Normal file
View File

@ -0,0 +1,14 @@
Checks: "-*,readability-identifier-naming"
CheckOptions:
readability-identifier-naming.NamespaceCase: lower_case
readability-identifier-naming.EnumCase: CamelCase
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.StructCase: CamelCase
readability-identifier-naming.PrivateMemberCase: camelBack
readability-identifier-naming.PrivateMemberPrefix: 'm_'
readability-identifier-naming.FunctionCase: lower_case
readability-identifier-naming.VariableCase: camelBack
readability-identifier-naming.GlobalVariablePrefix: 'g_'

4
.clangd Normal file
View File

@ -0,0 +1,4 @@
CompileFlags:
Remove: ['-fno-shrink-wrap', '-fstrict-volatile-bitfields', '-fno-tree-switch-conversion']
Add: ['-std=c++23', '-D__cpp_concepts=202002L']

78
CMakeLists.txt Normal file
View File

@ -0,0 +1,78 @@
cmake_minimum_required(VERSION 3.16)
#
#
# Global settings
#
#
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
project(playground)
#
#
# Dependencies
#
#
# spdlog
find_package(spdlog REQUIRED)
# Google Test
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
#
#
# Executabless
#
#
# add_executable(playground src/main.cpp src/tcp.cpp)
# target_link_libraries(playground PRIVATE spdlog::spdlog)
#
#
# Tests
#
#
enable_testing()
add_executable(
tcp_client_test
tests/tcp_client.cpp
src/tcp.cpp
)
target_link_libraries(
tcp_client_test
spdlog::spdlog
GTest::gtest_main
)
target_include_directories(
tcp_client_test
PRIVATE
src
)
include(GoogleTest)
gtest_discover_tests(tcp_client_test)

15
scripts/tcp_client.py Normal file
View File

@ -0,0 +1,15 @@
import socket
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("localhost", 1234))
sock.sendall(b"asdf")
data = sock.recv(1024)
print(data.decode())
if __name__ == "__main__":
main()

80
scripts/tcp_server.py Normal file
View File

@ -0,0 +1,80 @@
import socket
import sys
import logging
from time import sleep
class TcpServer:
def __init__(self, port, client_handler):
self._port = port
self._client_handler = client_handler
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._server_socket.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_address = ('0.0.0.0', port)
self._server_socket.bind(server_address)
self._server_socket.listen(1)
def handle_connection(self):
try:
connection, client_address = self._server_socket.accept()
logging.info(f"Client connected: {client_address}")
self._client_handler(connection)
except KeyboardInterrupt:
sys.exit()
def handle_client(connection):
connection.settimeout(3.0)
while True:
connection.sendall(
b"""{
"jsonrpc": "2.0",
"id": "1",
"method": "SetLedPattern",
"params": {
"led": 19,
"pattern": 3.1
}
}
""")
try:
res = connection.recv(1024)
if not res:
logging.info("Client disconnected")
connection.close()
break
logging.info(f"SetLedPattern response: {res.decode()}")
except socket.timeout:
logging.error("Connection timed out")
break
sleep(1)
def main():
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
datefmt='%H:%M:%S',
level=logging.DEBUG,
stream=sys.stdout)
port = 65432
server = TcpServer(port=port, client_handler=handle_client)
logging.info(f"Started TCP server on port {port}")
while True:
try:
server.handle_connection()
except Exception as e:
logging.error(
f"An error occurred while handling the client connection: {e}")
if __name__ == "__main__":
main()

101
src/main.cpp Normal file
View File

@ -0,0 +1,101 @@
#include "tcp.hpp"
#include <spdlog/spdlog.h>
int main() {
spdlog::set_level(spdlog::level::debug);
tcp::NonBlockingClient client;
tcp::HostString host = {"192.168.0.12"};
uint16_t port = 123;
spdlog::info("Connecting to {}:{}", host.data(), port);
if (!client.connect(host, port)) std::terminate();
tcp::ConnectionStatus status = tcp::ConnectionStatus::AttemptingConnection;
while (status == tcp::ConnectionStatus::AttemptingConnection) {
auto statusRes = client.get_last_connection_status();
if (!statusRes) std::terminate();
status = statusRes.value();
}
switch (status) {
case tcp::ConnectionStatus::Successful:
spdlog::info("Connected successfully");
break;
case tcp::ConnectionStatus::Refused:
spdlog::error("Connection refused");
std::terminate();
case tcp::ConnectionStatus::NoRouteToHost:
spdlog::error("No route to host");
std::terminate();
case tcp::ConnectionStatus::TimedOut:
spdlog::error("Connection timed out");
std::terminate();
case tcp::ConnectionStatus::HostUnreachable:
spdlog::error("Host unreachable");
std::terminate();
case tcp::ConnectionStatus::OtherError:
spdlog::error("Other error");
std::terminate();
case tcp::ConnectionStatus::AttemptingConnection:
spdlog::error("Attempting connection");
std::terminate();
}
char buffer[1024];
while (!client.data_available())
;
auto recvRes = client.recv(std::span<char>(buffer, sizeof(buffer)));
if (!recvRes) {
switch (recvRes.error()) {
case tcp::Error::FailedToCreateSocket:
spdlog::error("Failed to create socket");
case tcp::Error::FailedToStartConnecting:
spdlog::error("Failed to start connecting");
case tcp::Error::FailedToReadSocketError:
spdlog::error("Failed to read socket error");
case tcp::Error::FailedToResolveHost:
spdlog::error("Failed to resolve host");
case tcp::Error::OtherOperationInProgress:
spdlog::error("Other operation in progress");
case tcp::Error::SocketNotConnected:
spdlog::error("Socket not connected");
case tcp::Error::BufferFull:
spdlog::error("Buffer full");
case tcp::Error::NoDataAvailable:
spdlog::error("No data available");
case tcp::Error::SendFailed:
spdlog::error("Send failed");
case tcp::Error::RecvFailed:
spdlog::error("Recv failed");
break;
}
std::terminate();
}
spdlog::info("Received {} bytes", recvRes.value());
if (recvRes.value() > 0) {
spdlog::info("Received message: {}",
std::string(buffer, recvRes.value()));
}
EAGAIN;
const char* msg = "Hello, world!";
auto sendRes = client.send(std::span<const char>(msg, strlen(msg)));
if (!sendRes) {
spdlog::error("Failed to send message");
std::terminate();
}
spdlog::info("Sent {} bytes", sendRes.value());
client.disconnect();
return 0;
}

259
src/tcp.cpp Normal file
View File

@ -0,0 +1,259 @@
#include "tcp.hpp"
#include <arpa/inet.h>
#include <array>
#include <cerrno>
#include <cstdint>
#include <expected>
#include <fcntl.h>
#include <netdb.h>
#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 {
std::expected<sockaddr_in, tcp::Error> get_server_address(const char* host,
uint16_t port) {
hostent* server = gethostbyname(host);
if (server == nullptr) {
spdlog::error("tcp: get_host_by_name() failed with h_errno={}",
h_errno);
return std::unexpected{tcp::Error::FailedToResolveHost};
}
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_port = htons(port);
bcopy((char*)server->h_addr, (char*)&address.sin_addr.s_addr,
server->h_length);
return address;
}
} // namespace
namespace tcp {
[[nodiscard]] std::expected<void, Error>
NonBlockingClient::connect(HostString host, uint16_t port) {
/// Resolve host
auto addrRes = get_server_address(host.data(), port);
if (!addrRes) return std::unexpected{addrRes.error()};
sockaddr_in serverAddress = addrRes.value();
/// Create socket
close_socket();
if (auto res = create_socket(); !res) {
return std::unexpected{res.error()};
}
/// Connect
int connectRes = ::connect(m_socket, (struct sockaddr*)&serverAddress,
sizeof(serverAddress));
if (connectRes < 0) {
if (errno == EALREADY) {
spdlog::error(
"tcp::NonBlockingClient: connect() failed with errno={}",
errno);
return std::unexpected{Error::OtherOperationInProgress};
} else if (errno != EINPROGRESS) {
spdlog::error(
"tcp::NonBlockingClient: connect() failed with errno={}",
errno);
return std::unexpected{Error::FailedToStartConnecting};
}
}
m_startedNewConAttemt = true;
return {};
}
[[nodiscard]] std::expected<ConnectionStatus, Error>
NonBlockingClient::get_last_connection_status() {
if (!m_startedNewConAttemt) return m_lastKnownConStatus;
/// Check if connect operation has been completed
auto pendingOpRes = new_error_codes_available();
if (!pendingOpRes) return std::unexpected{pendingOpRes.error()};
if (pendingOpRes.value()) return ConnectionStatus::AttemptingConnection;
m_startedNewConAttemt = false;
/// Check for connection errors
int err = 0;
socklen_t len = sizeof(err);
if (getsockopt(m_socket, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {
spdlog::error(
"tcp::NonBlockingClient: getsockopt() failed with errno={}", errno);
return std::unexpected{Error::FailedToReadSocketError};
}
if (err == 0) {
m_lastKnownConStatus = ConnectionStatus::Successful;
} else {
switch (err) {
case ECONNREFUSED:
m_lastKnownConStatus = ConnectionStatus::Refused;
break;
case ENETUNREACH:
m_lastKnownConStatus = ConnectionStatus::NoRouteToHost;
break;
case ETIMEDOUT:
m_lastKnownConStatus = ConnectionStatus::TimedOut;
break;
case EHOSTUNREACH:
m_lastKnownConStatus = ConnectionStatus::HostUnreachable;
break;
default:
m_lastKnownConStatus = ConnectionStatus::OtherError;
break;
}
}
return m_lastKnownConStatus;
}
void NonBlockingClient::disconnect() {
close_socket();
}
[[nodiscard]] std::expected<int, Error>
NonBlockingClient::send(std::span<const char> data) {
auto conRes = get_last_connection_status();
if (!conRes) return std::unexpected{conRes.error()};
if (conRes != ConnectionStatus::Successful) {
spdlog::error("tcp::NonBlockingClient: Socket not connected");
return std::unexpected{Error::SocketNotConnected};
}
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);
switch (errno) {
case EWOULDBLOCK:
return std::unexpected{Error::BufferFull};
case EALREADY:
return std::unexpected{Error::OtherOperationInProgress};
default:
return std::unexpected{Error::SendFailed};
}
}
return bytesWritten;
}
bool NonBlockingClient::data_available() {
auto conRes = get_last_connection_status();
if (!conRes) return false;
if (conRes != ConnectionStatus::Successful) return false;
int ret = poll(&m_pfdIn, 1, 0);
if (ret < 0) {
spdlog::error("tcp::NonBlockingClient: poll() failed with errno={}",
errno);
return false;
}
return (m_pfdIn.revents & POLLIN);
}
[[nodiscard]] std::expected<int, Error>
NonBlockingClient::recv(std::span<char> buffer) {
const int bytesReceived =
::recv(m_socket, (char*)buffer.data(), buffer.size(), 0);
if (bytesReceived < 0) {
spdlog::error("tcp::NonBlockingClient: recv() failed with errno={}",
errno);
switch (errno) {
case EWOULDBLOCK:
return std::unexpected{Error::NoDataAvailable};
default:
return std::unexpected{Error::RecvFailed};
}
}
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, Error> NonBlockingClient::create_socket() {
m_socket = socket(AF_INET, SOCK_STREAM, 0);
if (m_socket < 1) {
spdlog::error("tcp::NonBlockingClient: socket() failed with errno={}",
errno);
return std::unexpected{Error::FailedToCreateSocket};
}
m_pfdOut.fd = m_socket;
m_pfdIn.fd = m_socket;
auto currentFlags = fcntl(m_socket, F_GETFL, 0);
if (currentFlags == -1) {
spdlog::error("tcp::NonBlockingClient: fcntl() failed with errno={}",
errno);
close_socket();
return std::unexpected{Error::FailedToCreateSocket};
}
if (fcntl(m_socket, F_SETFL, (currentFlags) | O_NONBLOCK) == -1) {
spdlog::error("tcp::NonBlockingClient: fcntl() failed with errno={}",
errno);
close_socket();
return std::unexpected{Error::FailedToCreateSocket};
}
return {};
}
[[nodiscard]] std::expected<bool, Error>
NonBlockingClient::new_error_codes_available() {
int ret = poll(&m_pfdOut, 1, 0);
if (ret < 0) {
spdlog::error("tcp::NonBlockingClient: poll() failed with errno={}",
errno);
return std::unexpected{Error::FailedToReadSocketError};
}
return (ret == 0);
}
} // namespace tcp

94
src/tcp.hpp Normal file
View File

@ -0,0 +1,94 @@
#pragma once
#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 <netinet/tcp.h>
#include <sys/socket.h>
#include <sys/types.h>
namespace tcp {
enum class Error {
FailedToCreateSocket,
FailedToStartConnecting,
FailedToReadSocketError,
FailedToResolveHost,
OtherOperationInProgress,
SocketNotConnected,
BufferFull,
NoDataAvailable,
SendFailed,
RecvFailed,
};
enum class ConnectionStatus {
AttemptingConnection,
Successful,
Refused,
NoRouteToHost,
TimedOut,
HostUnreachable,
OtherError,
};
using HostString = std::array<char, 64>;
// struct ServerSettings {
// uint16_t port = 0;
// };
//
// class NonBlockingServer {
// public:
// NonBlockingServer(ServerSettings settings);
// };
class NonBlockingClient {
public:
// clang-format off
[[nodiscard]] std::expected<void, Error>
connect(HostString host, uint16_t port);
// clang-format off
void disconnect();
[[nodiscard]] std::expected<ConnectionStatus, Error>
get_last_connection_status();
bool data_available();
[[nodiscard]] std::expected<int, Error> recv(std::span<char> buffer);
[[nodiscard]] std::expected<int, Error> send(std::span<const char> data);
private:
int m_socket = -1;
pollfd m_pfdOut = {.fd = -1, .events = POLLOUT};
pollfd m_pfdIn = {.fd = -1, .events = POLLIN};
bool m_startedNewConAttemt = false;
ConnectionStatus m_lastKnownConStatus = ConnectionStatus::OtherError;
void close_socket();
[[nodiscard]] std::expected<void, Error> create_socket();
[[nodiscard]] std::expected<bool, Error> new_error_codes_available();
};
} // namespace tcp

8
tests/tcp_client.cpp Normal file
View File

@ -0,0 +1,8 @@
#include "tcp.hpp"
#include <gtest/gtest.h>
TEST(TcpClient, Connect) {
tcp::NonBlockingClient client;
}