diff --git a/CMakeLists.txt b/CMakeLists.txt index 53805bb..26ea253 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,16 +54,28 @@ target_link_libraries(playground PRIVATE spdlog::spdlog) # +include(GoogleTest) + + enable_testing() -add_executable(tcp_test - tests/tcp.cpp +add_executable(tcp_blocking_test + tests/tcp_blocking.cpp src/tcp_server.cpp src/tcp_client.cpp ) -target_link_libraries(tcp_test spdlog::spdlog GTest::gtest_main) -target_include_directories(tcp_test PRIVATE src) +target_link_libraries(tcp_blocking_test spdlog::spdlog GTest::gtest_main) +target_include_directories(tcp_blocking_test PRIVATE src) -include(GoogleTest) -gtest_discover_tests(tcp_test) +gtest_discover_tests(tcp_blocking_test) + +add_executable(tcp_non_blocking_test + tests/tcp_non_blocking.cpp + src/tcp_server.cpp + src/tcp_client.cpp +) +target_link_libraries(tcp_non_blocking_test spdlog::spdlog GTest::gtest_main) +target_include_directories(tcp_non_blocking_test PRIVATE src) + +gtest_discover_tests(tcp_non_blocking_test) diff --git a/src/tcp_client.cpp b/src/tcp_client.cpp index 7d4e287..ee710f3 100644 --- a/src/tcp_client.cpp +++ b/src/tcp_client.cpp @@ -12,6 +12,97 @@ namespace tcp { +// +// +// Blocking client +// +// + + +BlockingClient::~BlockingClient() { + close_socket(); +} + +[[nodiscard]] std::expected +BlockingClient::connect(const HostString& host, uint16_t port) { + /// Resolve host + + auto addrRes = resolve_remote_address(host.data(), port); + if (!addrRes) return std::unexpected{addrRes.error()}; + sockaddr_in serverAddress = addrRes.value(); + + /// Create socket + + close_socket(); + + m_socket = socket(AF_INET, SOCK_STREAM, 0); + if (m_socket < 1) { + spdlog::error("tcp::BlockingClient: socket() failed with errno={}", + errno); + close_socket(); + return std::unexpected{errno}; + } + + /// Connect + + int connectRes = ::connect(m_socket, (struct sockaddr*)&serverAddress, + sizeof(serverAddress)); + + if (connectRes < 0) { + spdlog::error("tcp::BlockingClient: connect() failed with errno={}", + errno); + return std::unexpected{errno}; + } + + return {}; +} + +void BlockingClient::disconnect() { + close_socket(); +} + +[[nodiscard]] std::expected +BlockingClient::send(std::span data) { + const int bytesWritten = + ::send(m_socket, (const char*)data.data(), data.size(), 0); + + if (bytesWritten <= 0) { + spdlog::error("tcp::BlockingClient: send() failed with errno={}", + errno); + return std::unexpected{errno}; + } + + return bytesWritten; +} + +[[nodiscard]] std::expected +BlockingClient::recv(std::span buffer) { + const int bytesReceived = + ::recv(m_socket, (char*)buffer.data(), buffer.size(), 0); + + if (bytesReceived < 0) { + spdlog::error("tcp::BlockingClient: recv() failed with errno={}", + errno); + return std::unexpected{errno}; + } + return {bytesReceived}; +} + +void BlockingClient::close_socket() { + if (shutdown(m_socket, 0) < 0) { + spdlog::debug("tcp::BlockingClient: shutdown() failed with errno={}", + errno); + } + + if (close(m_socket) < 0) { + spdlog::debug("tcp::BlockingClient: close() failed with errno={}", + errno); + } + + m_socket = -1; +} + + // // // Non blocking client diff --git a/src/tcp_client.hpp b/src/tcp_client.hpp index 37734ca..8aa0970 100644 --- a/src/tcp_client.hpp +++ b/src/tcp_client.hpp @@ -18,6 +18,37 @@ namespace tcp { +// +// +// Blocking client +// +// + + +class BlockingClient { +public: + BlockingClient() = default; + BlockingClient(const BlockingClient&) = delete; + BlockingClient(BlockingClient&&) = delete; + BlockingClient& operator=(const BlockingClient&) = delete; + BlockingClient& operator=(BlockingClient&&) = delete; + ~BlockingClient(); + + [[nodiscard]] std::expected connect(const HostString& host, + uint16_t port); + + void disconnect(); + + [[nodiscard]] std::expected send(std::span data); + [[nodiscard]] std::expected recv(std::span buffer); + +private: + int m_socket = -1; + + void close_socket(); +}; + + // // // Non blocking client diff --git a/src/tcp_server.cpp b/src/tcp_server.cpp index 663651a..4c60765 100644 --- a/src/tcp_server.cpp +++ b/src/tcp_server.cpp @@ -8,10 +8,177 @@ // Library includes #include +// Project includes +#include "common.hpp" + namespace tcp { +// +// +// Blocking server +// +// + + +BlockingServer::~BlockingServer() { + close_client_socket(); + close_server_socket(); +} + +/// +/// @details Calling this repeatedly closes the previous socket and creates a +/// new one. +/// +std::expected BlockingServer::start_listening(uint16_t port) { + /// Create socket + + close_server_socket(); + if (auto res = create_server_socket(); !res) { + return std::unexpected{res.error()}; + } + + /// Bind socket and start listening + + const struct sockaddr_in serverAddress = get_local_address(port); + + if (::bind(m_serverSocket, (struct sockaddr*)&serverAddress, + sizeof(serverAddress)) != 0) { + spdlog::error("tcp::BlockingServer: bind() failed with errno={}", + errno); + close_server_socket(); + return std::unexpected{errno}; + }; + + if (::listen(m_serverSocket, 1) != 0) { + spdlog::error("tcp::BlockingServer: listen() failed with errno={}", + errno); + close_server_socket(); + return std::unexpected{errno}; + } + + return {}; +} + +void BlockingServer::stop_listening() { + close_server_socket(); +} + +[[nodiscard]] std::expected BlockingServer::accept() { + disconnect_from_client(); + + socklen_t clientAddrLen = sizeof(m_clientAddress); + + int result = ::accept(m_serverSocket, (struct sockaddr*)&m_clientAddress, + &clientAddrLen); + if (result < 0) return std::unexpected{errno}; + + m_clientSocket = result; + + return {}; +} + +void BlockingServer::disconnect_from_client() { + close_client_socket(); +} + +[[nodiscard]] std::expected, int> +BlockingServer::get_client_ip_as_string() { + std::array addrStr; + + if (inet_ntop(AF_INET, &((struct sockaddr_in*)&m_clientAddress)->sin_addr, + addrStr.data(), addrStr.size()) == nullptr) { + spdlog::error("tcp::BlockingServer: inet_ntop() failed with errno={}", + errno); + return std::unexpected{errno}; + } + + // if (inet_ntoa_r(((struct sockaddr_in*)&m_clientAddress)->sin_addr, + // addrStr.data(), addrStr.size()) == nullptr) { + // return std::unexpected{errno}; + // } + + return addrStr; +} + +[[nodiscard]] std::expected +BlockingServer::send(std::span data) { + const int bytesWritten = + ::send(m_clientSocket, (const char*)data.data(), data.size(), 0); + + if (bytesWritten <= 0) { + spdlog::error("tcp::BlockingServer: send() failed with errno={}", + errno); + return std::unexpected{errno}; + } + + return bytesWritten; +} + +[[nodiscard]] std::expected +BlockingServer::recv(std::span buffer) { + const int bytesReceived = + ::recv(m_clientSocket, (char*)buffer.data(), buffer.size(), 0); + + if (bytesReceived < 0) { + spdlog::error("tcp::BlockingServer: recv() failed with errno={}", + errno); + return std::unexpected{errno}; + } + return {bytesReceived}; +} + +[[nodiscard]] std::expected BlockingServer::create_server_socket() { + m_serverSocket = socket(AF_INET, SOCK_STREAM, 0); + if (m_serverSocket < 1) { + spdlog::error("tcp::BlockingServer: socket() failed with errno={}", + errno); + close_server_socket(); + return std::unexpected{errno}; + } + + int flag = 1; + if (::setsockopt(m_serverSocket, SOL_SOCKET, SO_REUSEADDR, &flag, + sizeof(flag)) < 0) { + spdlog::error("tcp::BlockingServer: setsockopt() failed with errno={}", + errno); + close_server_socket(); + return std::unexpected{errno}; + } + + return {}; +} + +void BlockingServer::close_server_socket() { + if (shutdown(m_serverSocket, 0) < 0) { + spdlog::debug("tcp::BlockingServer: shutdown() failed with errno={}", + errno); + } + + if (close(m_serverSocket) < 0) { + spdlog::debug("tcp::BlockingServer: close() failed with errno={}", + errno); + } + + m_serverSocket = -1; +} + +void BlockingServer::close_client_socket() { + if (shutdown(m_clientSocket, 0) < 0) { + spdlog::debug("tcp::BlockingServer: shutdown() failed with errno={}", + errno); + } + + if (close(m_clientSocket) < 0) { + spdlog::debug("tcp::BlockingServer: close() failed with errno={}", + errno); + } + + m_clientSocket = -1; +} + + // // // Non blocking server @@ -36,7 +203,7 @@ std::expected NonBlockingServer::start_listening(uint16_t port) { return std::unexpected{res.error()}; } - /// Bin socket and start listening + /// Bind socket and start listening const struct sockaddr_in serverAddress = get_local_address(port); diff --git a/src/tcp_server.hpp b/src/tcp_server.hpp index 175e24b..4d93887 100644 --- a/src/tcp_server.hpp +++ b/src/tcp_server.hpp @@ -11,13 +11,50 @@ #include #include -// Project includes -#include "common.hpp" - namespace tcp { +// +// +// Blocking server +// +// + + +class BlockingServer { +public: + BlockingServer() = default; + BlockingServer(const BlockingServer&) = delete; + BlockingServer(BlockingServer&&) = delete; + BlockingServer& operator=(const BlockingServer&) = delete; + BlockingServer& operator=(BlockingServer&&) = delete; + ~BlockingServer(); + + [[nodiscard]] std::expected start_listening(uint16_t port); + void stop_listening(); + + [[nodiscard]] std::expected accept(); + void disconnect_from_client(); + + [[nodiscard]] std::expected, int> + get_client_ip_as_string(); + + [[nodiscard]] std::expected send(std::span data); + [[nodiscard]] std::expected recv(std::span buffer); + +private: + int m_serverSocket = -1; + + int m_clientSocket = -1; + struct sockaddr m_clientAddress = {0}; + + [[nodiscard]] std::expected create_server_socket(); + void close_server_socket(); + void close_client_socket(); +}; + + // // // Non blocking server diff --git a/tests/tcp_blocking.cpp b/tests/tcp_blocking.cpp new file mode 100644 index 0000000..df7ecdb --- /dev/null +++ b/tests/tcp_blocking.cpp @@ -0,0 +1,202 @@ +#include + +// STL includes +#include +#include + +// Project includes +#include "tcp_client.hpp" +#include "tcp_server.hpp" + + +TEST(BlockingTcpServer, Accept) { + tcp::BlockingServer server; + tcp::BlockingClient client; + + auto lisRes = server.start_listening(1234); + + std::thread clientThread([&client]() { + auto conRes = client.connect({"localhost"}, 1234); + EXPECT_TRUE(conRes.has_value()); + }); + + std::thread serverThread([&server]() { + auto accRes = server.accept(); + EXPECT_TRUE(accRes.has_value()); + }); + + serverThread.join(); + clientThread.join(); +} + +TEST(BlockingTcpServer, RestartListening) { + tcp::BlockingServer server; + tcp::BlockingClient client; + + auto lisRes = server.start_listening(1234); + + std::thread clientThread([&client]() { + auto conRes = client.connect({"localhost"}, 1234); + EXPECT_TRUE(conRes.has_value()); + }); + + clientThread.join(); + + auto lisRes2 = server.start_listening(2345); + + std::thread clientThread2([&client]() { + auto conRes = client.connect({"localhost"}, 1234); + EXPECT_FALSE(conRes.has_value()); + }); + + clientThread2.join(); + + std::thread clientThread3([&client]() { + auto conRes2 = client.connect({"localhost"}, 2345); + EXPECT_TRUE(conRes2.has_value()); + }); + + clientThread3.join(); +} + +TEST(BlockingTcpServer, StopListening) { + tcp::BlockingServer server; + tcp::BlockingClient client; + + auto lisRes = server.start_listening(1234); + + std::thread clientThread([&client]() { + auto conRes = client.connect({"localhost"}, 1234); + EXPECT_TRUE(conRes.has_value()); + }); + + clientThread.join(); + + server.stop_listening(); + + std::thread clientThread2([&client]() { + auto conRes2 = client.connect({"localhost"}, 1234); + EXPECT_FALSE(conRes2.has_value()); + }); + + clientThread2.join(); +} + +TEST(BlockingTcpServer, MultipleAccept) { + tcp::BlockingServer server; + tcp::BlockingClient client1; + tcp::BlockingClient client2; + + auto lisRes1 = server.start_listening(1234); + + std::thread clientThread1([&client1]() { + auto conRes1 = client1.connect({"localhost"}, 1234); + EXPECT_TRUE(conRes1.has_value()); + }); + + std::thread clientThread2([&client2]() { + auto conRes2 = client2.connect({"localhost"}, 1234); + EXPECT_TRUE(conRes2.has_value()); + }); + + std::thread serverThread([&server]() { + auto accRes1 = server.accept(); + EXPECT_TRUE(accRes1.has_value()); + + auto accRes2 = server.accept(); + EXPECT_TRUE(accRes2.has_value()); + }); + + clientThread1.join(); + clientThread2.join(); + serverThread.join(); +} + +TEST(BlockingTcpServer, GetClientIPAsString) { + tcp::BlockingServer server; + tcp::BlockingClient client; + + auto lisRes = server.start_listening(1234); + auto conRes = client.connect({"localhost"}, 1234); + auto accRes = server.accept(); + + auto ipRes = server.get_client_ip_as_string(); + if (!ipRes) return; + + EXPECT_STREQ(ipRes.value().data(), "127.0.0.1"); +} + +TEST(BlockingTcpClient, Connect) { + tcp::BlockingServer server; + tcp::BlockingClient client; + + auto lisRes = server.start_listening(1234); + + std::thread clientThread([&client]() { + auto conRes = client.connect({"localhost"}, 1234); + EXPECT_TRUE(conRes.has_value()); + }); + + clientThread.join(); +} + +TEST(BlockingClient, Reconnect) { + tcp::BlockingServer server1; + tcp::BlockingServer server2; + tcp::BlockingClient client; + + auto lisRes1 = server1.start_listening(1234); + auto lisRes2 = server2.start_listening(2345); + + std::thread clientThread([&client]() { + auto conRes = client.connect({"localhost"}, 1234); + EXPECT_TRUE(conRes.has_value()); + }); + + clientThread.join(); + + std::thread clientThread2([&client]() { + auto conRes2 = client.connect({"localhost"}, 2345); + EXPECT_TRUE(conRes2.has_value()); + }); + + clientThread2.join(); +} + +TEST(BlockingTcpClientServer, ClientSendServerReceive) { + tcp::BlockingServer server; + tcp::BlockingClient client; + + auto lisRes = server.start_listening(1234); + auto conRes = client.connect({"localhost"}, 1234); + auto accRes = server.accept(); + + std::array txMsg = {"Hello, World!"}; + auto sendRes = client.send(std::as_bytes(std::span(txMsg))); + + EXPECT_EQ(sendRes.value(), txMsg.size()); + + std::array rxBuffer; + auto recvRes = server.recv(std::as_writable_bytes(std::span(rxBuffer))); + EXPECT_EQ(recvRes.value(), txMsg.size()); + EXPECT_EQ(std::strcmp(txMsg.data(), rxBuffer.data()), 0); +} + +TEST(BlockingTcpClientServer, ClientReceiveServerSend) { + tcp::BlockingServer server; + tcp::BlockingClient client; + + auto lisRes = server.start_listening(1234); + auto conRes = client.connect({"localhost"}, 1234); + auto accRes = server.accept(); + + std::array txMsg = {"Hello, World!"}; + auto sendRes = server.send(std::as_bytes(std::span(txMsg))); + + EXPECT_EQ(sendRes.value(), txMsg.size()); + + std::array rxBuffer; + auto recvRes = client.recv(std::as_writable_bytes(std::span(rxBuffer))); + EXPECT_EQ(recvRes.value(), txMsg.size()); + EXPECT_EQ(std::strcmp(txMsg.data(), rxBuffer.data()), 0); +} diff --git a/tests/tcp.cpp b/tests/tcp_non_blocking.cpp similarity index 93% rename from tests/tcp.cpp rename to tests/tcp_non_blocking.cpp index 7e39bd5..d2dcfc0 100644 --- a/tests/tcp.cpp +++ b/tests/tcp_non_blocking.cpp @@ -4,11 +4,11 @@ #include // Project includes -#include "tcp_server.hpp" #include "tcp_client.hpp" +#include "tcp_server.hpp" -TEST(TcpServer, Accept) { +TEST(NonBlockingTcpServer, Accept) { tcp::NonBlockingServer server; tcp::NonBlockingClient client; @@ -25,7 +25,7 @@ TEST(TcpServer, Accept) { EXPECT_FALSE(server.next_client_available()); } -TEST(TcpServer, RestartListening) { +TEST(NonBlockingTcpServer, RestartListening) { tcp::NonBlockingServer server; tcp::NonBlockingClient client; @@ -48,7 +48,7 @@ TEST(TcpServer, RestartListening) { EXPECT_EQ(statRes2.value(), EISCONN); } -TEST(TcpServer, StopListening) { +TEST(NonBlockingTcpServer, StopListening) { tcp::NonBlockingServer server; tcp::NonBlockingClient client; @@ -67,7 +67,7 @@ TEST(TcpServer, StopListening) { EXPECT_EQ(client.get_last_connection_status().value(), ECONNREFUSED); } -TEST(TcpServer, DataAvailable) { +TEST(NonBlockingTcpServer, DataAvailable) { tcp::NonBlockingServer server; tcp::NonBlockingClient client; @@ -90,7 +90,7 @@ TEST(TcpServer, DataAvailable) { EXPECT_FALSE(server.data_available()); } -TEST(TcpServer, GetClientIPAsString) { +TEST(NonBlockingTcpServer, GetClientIPAsString) { tcp::NonBlockingServer server; tcp::NonBlockingClient client; @@ -103,7 +103,7 @@ TEST(TcpServer, GetClientIPAsString) { EXPECT_STREQ(ipRes.value().data(), "127.0.0.1"); } -TEST(TcpClient, Connect) { +TEST(NonBlockingTcpClient, Connect) { tcp::NonBlockingServer server; tcp::NonBlockingClient client; @@ -140,7 +140,7 @@ TEST(TcpClient, Connect) { EXPECT_EQ(statRes4.value(), ECONNREFUSED); } -TEST(TcpClient, Reconnect) { +TEST(NonBlockingTcpClient, Reconnect) { tcp::NonBlockingServer server1; tcp::NonBlockingServer server2; tcp::NonBlockingClient client; @@ -161,7 +161,7 @@ TEST(TcpClient, Reconnect) { EXPECT_EQ(statRes2.value(), EISCONN); } -TEST(TcpClient, DataAvailable) { +TEST(NonBlockingTcpClient, DataAvailable) { tcp::NonBlockingServer server; tcp::NonBlockingClient client; @@ -187,7 +187,7 @@ TEST(TcpClient, DataAvailable) { EXPECT_FALSE(client.data_available()); } -TEST(TcpClientServer, ClientSendServerReceive) { +TEST(NonBlockingTcpClientServer, ClientSendServerReceive) { tcp::NonBlockingServer server; tcp::NonBlockingClient client; @@ -207,7 +207,7 @@ TEST(TcpClientServer, ClientSendServerReceive) { EXPECT_EQ(std::strcmp(txMsg.data(), rxBuffer.data()), 0); } -TEST(TcpClientServer, ClientReceiveServerSend) { +TEST(NonBlockingTcpClientServer, ClientReceiveServerSend) { tcp::NonBlockingServer server; tcp::NonBlockingClient client;