commit 72121036a6c1ffa5075bce530b020f7497c51969 Author: Andreas Tsouchlos Date: Fri Apr 18 21:52:54 2025 +0200 Add first tcp server implementation diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..ab6c625 --- /dev/null +++ b/.clang-tidy @@ -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_' + diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..88a5911 --- /dev/null +++ b/.clangd @@ -0,0 +1,4 @@ +CompileFlags: + Remove: ['-fno-shrink-wrap', '-fstrict-volatile-bitfields', '-fno-tree-switch-conversion'] + Add: ['-std=c++23', '-D__cpp_concepts=202002L'] + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2c186bf --- /dev/null +++ b/CMakeLists.txt @@ -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) + diff --git a/scripts/tcp_client.py b/scripts/tcp_client.py new file mode 100644 index 0000000..89e7fcd --- /dev/null +++ b/scripts/tcp_client.py @@ -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() + diff --git a/scripts/tcp_server.py b/scripts/tcp_server.py new file mode 100644 index 0000000..6b65c6f --- /dev/null +++ b/scripts/tcp_server.py @@ -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() diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..335cf38 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,101 @@ +#include "tcp.hpp" +#include + + +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(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(msg, strlen(msg))); + if (!sendRes) { + spdlog::error("Failed to send message"); + std::terminate(); + } + spdlog::info("Sent {} bytes", sendRes.value()); + client.disconnect(); + + return 0; +} diff --git a/src/tcp.cpp b/src/tcp.cpp new file mode 100644 index 0000000..dc76768 --- /dev/null +++ b/src/tcp.cpp @@ -0,0 +1,259 @@ +#include "tcp.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace { + + +std::expected 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 +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 +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 +NonBlockingClient::send(std::span 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 +NonBlockingClient::recv(std::span 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 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 +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 diff --git a/src/tcp.hpp b/src/tcp.hpp new file mode 100644 index 0000000..4d22e5b --- /dev/null +++ b/src/tcp.hpp @@ -0,0 +1,94 @@ +#pragma once + + +#include +#include +#include +#include +#include +#include +#include +#include + +// C stdlib includes +#include +#include +#include +#include +#include +#include +#include +#include + + +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; + +// struct ServerSettings { +// uint16_t port = 0; +// }; +// +// class NonBlockingServer { +// public: +// NonBlockingServer(ServerSettings settings); +// }; + +class NonBlockingClient { +public: + // clang-format off + [[nodiscard]] std::expected + connect(HostString host, uint16_t port); + // clang-format off + + void disconnect(); + + [[nodiscard]] std::expected + get_last_connection_status(); + + bool data_available(); + + [[nodiscard]] std::expected recv(std::span buffer); + [[nodiscard]] std::expected send(std::span 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 create_socket(); + [[nodiscard]] std::expected new_error_codes_available(); +}; + +} // namespace tcp diff --git a/tests/tcp_client.cpp b/tests/tcp_client.cpp new file mode 100644 index 0000000..c641b77 --- /dev/null +++ b/tests/tcp_client.cpp @@ -0,0 +1,8 @@ +#include "tcp.hpp" + +#include + +TEST(TcpClient, Connect) { + tcp::NonBlockingClient client; +} +