diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index a73168371..b0f7f331b 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -139,6 +139,7 @@ set(CUBOS_CORE_SOURCE "src/geom/intersections.cpp" "src/net/address.cpp" + "src/net/tcp_socket.cpp" ) # Create core library diff --git a/core/include/cubos/core/net/tcp_socket.hpp b/core/include/cubos/core/net/tcp_socket.hpp new file mode 100644 index 000000000..e7fa7160e --- /dev/null +++ b/core/include/cubos/core/net/tcp_socket.hpp @@ -0,0 +1,76 @@ +/// @file +/// @brief Class @ref cubos::core::net::TcpSocket. +/// @ingroup core-net + +#pragma once + +#include + +#include +#include + +#ifdef _WIN32 +#include +#include +#else +// Socket type. Differs from Windows (SOCKET) and POSIX (int). +#define SOCKET int +// Reprensets an invalid socket. +#define INVALID_SOCKET (-1) +#endif + +namespace cubos::core::net +{ + /// @brief Represents a TCP socket. + /// @ingroup core-net + class CUBOS_CORE_API TcpSocket final + { + public: + /// @brief Constructs a empty TCP socket. + TcpSocket(); + + /// @brief Deconstructs by desconnecting the inner socket. + ~TcpSocket(); + + /// @brief Forbid copy construction. + TcpSocket(const TcpSocket&) = delete; + + /// @brief Forbid copy assignment. + TcpSocket& operator=(const TcpSocket&) = delete; + + /// @brief Connects via TCP to a remote `address` and `port`. + /// @param address Destination address. + /// @param port Destination port. + /// @param timeoutMs Connection timeout in milliseconds. + /// @return Whether connecting was successful. + bool connect(const Address& address, uint16_t port, int timeoutMs = 0); + + /// @brief Disconnects inner socket. + void disconnect(); + + /// @brief Sends `data` to the connected socket. + /// @param data Message. + /// @param size Message size. + /// @return Whether sending was successful. + bool send(const void* data, std::size_t size); + + /// @brief Receives a single datagram message on the socket. + /// @param data Buffer to store the datagram message. + /// @param size Maximum buffer size. + /// @param[out] received Number of bytes read. + /// @return Whether receiving was successful. + bool receive(void* data, std::size_t size, std::size_t& received); + + /// @brief Control blocking mode of inner socket. + /// @note A blocking socket does not return control until it has sent (or received) some or all data + /// specified for the operation. + /// @note A non-blocking socket returns whatever is in the receive buffer and immediately continues. + /// @param blocking Whether to block or not. + /// @return Whether setting the block flag was sucessfull. + bool setBlocking(bool blocking); + + private: + SOCKET mSock{INVALID_SOCKET}; + bool mBlocking{false}; + }; +} // namespace cubos::core::net \ No newline at end of file diff --git a/core/src/net/tcp_socket.cpp b/core/src/net/tcp_socket.cpp new file mode 100644 index 000000000..7bbdfd4f2 --- /dev/null +++ b/core/src/net/tcp_socket.cpp @@ -0,0 +1,164 @@ +#ifdef _WIN32 +#include +#include +#pragma comment(lib, "Ws2_32.lib") +#else +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include + +using cubos::core::net::Address; +using cubos::core::net::TcpSocket; + +/// @brief Cross-platform utility to get last error message. +/// @return Message. +/// @todo Move this to a higher module for general purposes? +static std::string error() +{ +#ifdef _WIN32 + static char message[256] = {0}; + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 0, WSAGetLastError(), 0, message, 256, 0); + char* nl = strrchr(message, '\n'); + if (nl) + *nl = 0; + return {message}; +#else + return {strerror(errno)}; +#endif +} + +TcpSocket::TcpSocket() +{ +#ifdef _WIN32 + WSADATA wsa; + CUBOS_ASSERT(WSAStartup(MAKEWORD(2, 2), &wsa) == 0, " WSAStartup failed: {}", WSAGetLastError()); +#endif +} + +TcpSocket::~TcpSocket() +{ + disconnect(); +} + +bool TcpSocket::connect(const Address& address, uint16_t port, int timeoutMs) +{ + sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr(address.toString().c_str()); + addr.sin_port = htons(port); + + if (::connect(mSock, (struct sockaddr*)&addr, sizeof(addr)) < 0) + { + CUBOS_ERROR("Failed to connect to address {} at port '{}': {}", address.toString(), port, error()); + return false; + } + + (void)timeoutMs; + // TODO implement timeout + + CUBOS_INFO("New TCP connection at address {} and port '{}'", address.toString(), port); + return true; +} + +void TcpSocket::disconnect() +{ + if (mSock != -1) + { + CUBOS_WARN("Closing TCP socket: '{}'", mSock); +#ifdef _WIN32 + closesocket(mSock); + WSACleanup(); +#else + close(mSock); +#endif + } +} + +bool TcpSocket::send(const void* data, std::size_t size) +{ + if (mSock == -1) + { + CUBOS_ERROR("Invalid socket"); + return false; + } + +#ifdef _WIN32 + auto res = ::send(mSock, static_cast(data), size, 0); +#else + auto res = ::send(mSock, data, size, 0); +#endif + if (res < 0) + { + CUBOS_ERROR("Failed to send TCP data: {}", error()); + return false; + } + + CUBOS_INFO("Sent TCP message"); + return true; +} + +bool TcpSocket::receive(void* data, std::size_t size, std::size_t& received) +{ + if (mSock == -1) + { + CUBOS_ERROR("Invalid socket"); + return false; + } + +#ifdef _WIN32 + received = ::recv(mSock, static_cast(data), size, 0); +#else + received = ::recv(mSock, data, size, 0); +#endif + if (received < 0) + { + CUBOS_ERROR("Failed to receive TCP data: {}", error()); + return false; + } + + CUBOS_INFO("Received TCP message"); + return true; +} + +bool TcpSocket::setBlocking(bool blocking) +{ + bool success = false; + +#ifdef _WIN32 + u_long flags = blocking ? 0 : 1; + success = NO_ERROR == ioctlsocket(mSock, FIONBIO, &flags); +#else + mBlocking = blocking; + + int flags = fcntl(mSock, F_GETFL, 0); + if (blocking) + { + flags &= ~O_NONBLOCK; + } + else + { + flags |= O_NONBLOCK; + } + success = fcntl(mSock, F_SETFL, flags) == 0; +#endif + if (!success) + { + CUBOS_ERROR("Failed to change block flag of socket '{}'", mSock); + } + else + { + CUBOS_INFO("UDP socket {} is now '{}'", mSock, blocking ? "blocking" : "non-blocking"); + } + + return success; +} \ No newline at end of file