From 6198da115c16f5b9342a4e9e2baecfa88f9016d9 Mon Sep 17 00:00:00 2001 From: Julien Date: Mon, 21 Sep 2026 19:23:33 +0200 Subject: [PATCH] First commit. Server + test client + tokens + history --- CMakeLists.txt | 18 ++ src/CMakeLists.txt | 12 + src/Logger.cpp | 8 + src/Logger.hpp | 21 ++ src/Message.hpp | 43 ++++ src/Server.cpp | 506 ++++++++++++++++++++++++++++++++++++++++ src/Server.hpp | 66 ++++++ src/Socket.cpp | 299 ++++++++++++++++++++++++ src/Socket.hpp | 63 +++++ src/main.cpp | 10 + tests/CMakeLists.txt | 39 ++++ tests/Client.cpp | 285 ++++++++++++++++++++++ tests/Client.hpp | 35 +++ tests/history.txt | 5 + tests/history_clean.txt | 3 + tests/testClient.cpp | 8 + tests/testServer.cpp | 125 ++++++++++ tests/testSocket.cpp | 112 +++++++++ tests/tokens.txt | 2 + 19 files changed, 1660 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 src/CMakeLists.txt create mode 100644 src/Logger.cpp create mode 100644 src/Logger.hpp create mode 100644 src/Message.hpp create mode 100644 src/Server.cpp create mode 100644 src/Server.hpp create mode 100644 src/Socket.cpp create mode 100644 src/Socket.hpp create mode 100644 src/main.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 tests/Client.cpp create mode 100644 tests/Client.hpp create mode 100644 tests/history.txt create mode 100644 tests/history_clean.txt create mode 100644 tests/testClient.cpp create mode 100644 tests/testServer.cpp create mode 100644 tests/testSocket.cpp create mode 100644 tests/tokens.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..92d7736 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.31) + +project(NewsServer CXX) + +include(CTest) + +add_library(${PROJECT_NAME}Objects OBJECT) +target_compile_features(${PROJECT_NAME}Objects PUBLIC cxx_std_17) + +add_executable(${PROJECT_NAME}) +target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}Objects) +set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "newsserver") + +add_subdirectory(src) + +if (BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..3b1f0e6 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,12 @@ +target_include_directories(${PROJECT_NAME}Objects PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_sources(${PROJECT_NAME}Objects PRIVATE + Logger.cpp + Logger.hpp + Message.hpp + Server.cpp + Server.hpp + Socket.cpp + Socket.hpp) + +target_sources(${PROJECT_NAME} PRIVATE + main.cpp) diff --git a/src/Logger.cpp b/src/Logger.cpp new file mode 100644 index 0000000..bd6bba5 --- /dev/null +++ b/src/Logger.cpp @@ -0,0 +1,8 @@ +#include "Logger.hpp" + +#include + +void Logger::log(const std::string& entity, const std::string& message) { + std::lock_guard lock(externalMutex_); + std::cout << entity << ": " << message << std::endl; +} diff --git a/src/Logger.hpp b/src/Logger.hpp new file mode 100644 index 0000000..281b46a --- /dev/null +++ b/src/Logger.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +class Logger { +public: + Logger() = default; + ~Logger() = default; + + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + + Logger(Logger&&) = delete; + Logger& operator=(Logger&&) = delete; + + void log(const std::string& entity, const std::string& message); + +private: + std::mutex externalMutex_; +}; diff --git a/src/Message.hpp b/src/Message.hpp new file mode 100644 index 0000000..5d2bd55 --- /dev/null +++ b/src/Message.hpp @@ -0,0 +1,43 @@ +#pragma once + +#pragma pack(push, 1) + +namespace message { +enum class Type : std::uint8_t { + News = 0, + Reply = 1, + Login = 2, + Request = 3, +}; + +enum class ReplyCode : std::uint8_t { + OK = 0, + UnknownType = 1, + WrongLogin = 2, + NotLoggedIn = 3 +}; + +struct Header { + Type type{}; + std::uint16_t size{0}; +}; + +struct News { + std::uint32_t timestamp{0}; +}; + +struct Reply { + ReplyCode code{}; +}; + +struct Login { + std::uint64_t token{0}; +}; + +struct Request { + std::uint32_t firstMissing{0}; + std::uint32_t lastMissing{0}; +}; +} + +#pragma pack(pop) diff --git a/src/Server.cpp b/src/Server.cpp new file mode 100644 index 0000000..6b91c63 --- /dev/null +++ b/src/Server.cpp @@ -0,0 +1,506 @@ +#include "Server.hpp" + +#include // memcpy +#include +#include +#include + +Server::Server(Logger& logger) : logger_(logger) {} + +Server::~Server() { + stop(); +} + +bool Server::init(const std::filesystem::path& tokenFilePath, const std::filesystem::path& historyFilePath) { + std::lock_guard lock(externalMutex_); + + tokens_.clear(); + news_.clear(); + history_.close(); + + if (std::filesystem::exists(tokenFilePath)) { + if (std::filesystem::is_directory(tokenFilePath)) { + logger_.log("Server", "Token file path " + tokenFilePath.string() + " is a directory."); + return false; + } + + std::ifstream tokenFile(tokenFilePath); + + if (!tokenFile) { + logger_.log("Server", "Could not open token file " + tokenFilePath.string() + " to read."); + return false; + } + + std::size_t lineN{0}; + + for (std::string line; std::getline(tokenFile, line);) { + lineN++; + + std::uint64_t token{0}; + + try { + token = std::stoi(line); + } + catch (const std::exception& e) { + logger_.log("Server", + "Skipping invalid token " + line + " line " + std::to_string(lineN) + '.'); + continue; + } + + tokens_.insert(token); + + logger_.log("Server", "Added token " + line + '.'); + } + } + + if (tokens_.empty()) { + logger_.log("Server", "No tokkens were added."); + return false; + } + + if (std::filesystem::exists(historyFilePath)) { + if (std::filesystem::is_directory(historyFilePath)) { + logger_.log("Server", "History file path " + historyFilePath.string() + " is a directory."); + return false; + } + + std::ifstream historyFile(historyFilePath); + + if (!historyFile) { + logger_.log("Server", "Could not open history file " + historyFilePath.string() + " to read."); + return false; + } + + std::size_t lineN{0}; + + for (std::string line; std::getline(historyFile, line);) { + lineN++; + std::size_t linePos = line.find('|'); + + if (linePos == std::string::npos or linePos == 0) { + logger_.log("Server", "Skipping invalid line " + std::to_string(lineN) + " in history file."); + continue; + } + + std::string timestampStr = line.substr(0, linePos); + std::string news = line.substr(linePos + 1); + std::uint32_t timestamp{0}; + + try { + timestamp = std::stoi(timestampStr); + } + catch (const std::exception& e) { + logger_.log("Server", + "Skipping invalid timestamp " + timestampStr + " line " + std::to_string(lineN) + '.'); + continue; + } + + if (news.empty()) { + logger_.log("Server", "Skipping empty news line " + std::to_string(lineN) + '.'); + } + + auto const result = news_.try_emplace(timestamp, std::move(news)); + + if (!result.second) { + logger_.log("Server", "Skipping duplicated timestamp " + std::to_string(timestamp) + '.'); + continue; + } + + logger_.log("Server", + "Added timestamp " + std::to_string(result.first->first) + " news " + result.first->second); + } + } + + history_.open(historyFilePath, std::ios::binary | std::ios_base::app); + + if (!history_) { + logger_.log("Server", "Could not open history file " + historyFilePath.string() + " to write."); + return false; + } + + return true; +} + +bool Server::addNews(const std::uint32_t timestamp, std::string news) { + std::lock_guard lock(externalMutex_); + + if (news.empty()) { + logger_.log("Server", "Could not add empty news."); + return false; + } + + { + std::lock_guard lock(newsMutex_); + auto const result = news_.try_emplace(timestamp, news); + + if (!result.second) { + logger_.log("Server", "Could not add news for existing timestamp " + std::to_string(timestamp) + '.'); + return false; + } + } + + history_ << timestamp << '|' << news << '\n'; + + { + std::lock_guard lock(queueMutex_); + sendQueue_.push_back({timestamp, news}); + } + + logger_.log("Server", "Added timestamp " + std::to_string(timestamp) + " news " + news); + return true; +} + +bool Server::start(const std::uint16_t port) { + std::lock_guard lock(externalMutex_); + + if (running_) { + logger_.log("Server", "Already running."); + return false; + } + + running_ = true; + socketThread_ = std::thread([this, port](){ runThread(port); }); + + logger_.log("Server", "Started with port " + std::to_string(port) + '.'); + return true; +} + +void Server::stop() { + std::lock_guard lock(externalMutex_); + + if (!running_) { + return; + } + + logger_.log("Server", "Stop"); + + stopSignal_ = true; + + if (socketThread_.joinable()) { + socketThread_.join(); + } + + stopSignal_ = false; + running_ = false; +} + +bool Server::listening() const { + return listening_; +} + +void Server::runThread(const std::uint16_t port) +{ + Socket serverSocket(logger_); + + if (!serverSocket.init()) { + return; + } + + if (!serverSocket.listen(port)) { + return; + } + + SocketPoller poller(logger_); + + if (!poller.init()) { + return; + } + + if (!poller.add(serverSocket)) { + return; + } + + listening_ = true; + + std::unordered_map clients; + + while (!stopSignal_) { + std::unordered_set pollResult; + + // Poll incoming events. + if (!poller.poll(pollResult)) { + break; + } + + for (const auto fd : pollResult) { + // Event from listener socket. + if (fd == serverSocket.fileDescriptor()) { + Client newClient; + newClient.socket = std::make_unique(logger_); + + if (!serverSocket.accept(*newClient.socket)) { + continue; + } + + if (!newClient.socket->isOpen()) { + continue; + } + + if (!poller.add(*newClient.socket)) { + continue; + } + + logger_.log("Server " + std::to_string(serverSocket.fileDescriptor()), + "Added client " + std::to_string(newClient.socket->fileDescriptor()) + '.'); + clients.try_emplace(newClient.socket->fileDescriptor(), std::move(newClient)); + } + // Event from a client socket. + else { + auto it = clients.find(fd); + + if (it == clients.end()) { + logger_.log("Server " + std::to_string(serverSocket.fileDescriptor()), + "Received data from unknown source " + std::to_string(fd) + '.'); + continue; + } + + if (!handleMessage(it->second)) { + poller.remove(*it->second.socket); + clients.erase(it); + } + } + } // /for each polled event. + + + // Send news from queue. + decltype(sendQueue_) sendQueue; + + { + std::lock_guard lock(queueMutex_); + sendQueue.swap(sendQueue_); + } + + for (auto const& [timestamp, news] : sendQueue) { + message::News n; + n.timestamp = htobe32(timestamp); + + message::Header h; + h.type = message::Type::News; + h.size = htobe16(sizeof h + sizeof n + news.size()); + + std::vector buffer; + buffer.reserve(sizeof h + sizeof n + news.size()); + buffer.insert(buffer.end(), + reinterpret_cast(&h), + reinterpret_cast(&h) + sizeof h); + buffer.insert(buffer.end(), + reinterpret_cast(&n), + reinterpret_cast(&n) + sizeof n); + buffer.insert(buffer.end(), + reinterpret_cast(news.data()), + reinterpret_cast(news.data()) + news.size()); + + // Send to all logged in clients. + for (auto& [fd, client] : clients) { + if (client.loggedIn && !client.socket->write(buffer)) { + poller.remove(*client.socket); + clients.erase(fd); + continue; + } + + logger_.log("Server " + std::to_string(serverSocket.fileDescriptor()), + "Sent news timestamp " + std::to_string(timestamp) + + " for client " + std::to_string(fd) + '.'); + } + } + } // /while stop signal is false. + + listening_ = false; +} + +bool Server::handleMessage(Client& client) { + if (client.socket == nullptr) { + return false; + } + + std::vector buffer; + buffer.resize(sizeof(message::Header)); + + if (!client.socket->read(buffer)) { + return false; + } + + // TODO + if (buffer.size() < sizeof(message::Header)) { + logger_.log("Server", + "ZERO READ " + std::to_string(client.socket->fileDescriptor())); + return true; + } + + message::Header h; + std::memcpy(&h, buffer.data(), sizeof h); + h.size = be16toh(h.size); + + logger_.log("Server", + "Received message type " + std::to_string(static_cast(h.type)) + + " size " + std::to_string(h.size) + + " from client " + std::to_string(client.socket->fileDescriptor()) + '.'); + + switch (h.type) { + case message::Type::Login: { + buffer.resize(h.size - sizeof h); + + if (!client.socket->read(buffer)) { + return false; + } + + message::Login l; + std::memcpy(&l, buffer.data(), sizeof l); + l.token = be64toh(l.token); + + if (!handleLogin(client, l)) { + return false; + } + } break; + case message::Type::Request: { + buffer.resize(h.size - sizeof h); + + if (!client.socket->read(buffer)) { + return false; + } + + message::Request r; + std::memcpy(&r, buffer.data(), sizeof r); + r.firstMissing = be32toh(r.firstMissing); + r.lastMissing = be32toh(r.lastMissing); + + if (!handleRequest(client, r)) { + return false; + } + } break; + default: { + buffer.resize(h.size - sizeof h); + + if (!client.socket->read(buffer)) { + return false; + } + + if (!handleUnknownMessage(client, h)) { + return false; + } + + } break; + } // /switch message type. + + return true; +} + +bool Server::handleUnknownMessage(Client& client, const message::Header& message) { + logger_.log("Server", + "Received unknown message type " + std::to_string(static_cast(message.type)) + + " size " + std::to_string(message.size) + + " from client " + std::to_string(client.socket->fileDescriptor()) + '.'); + + message::Reply r; + r.code = message::ReplyCode::UnknownType; + + message::Header h; + h.type = message::Type::Reply; + h.size = htobe16(sizeof h + sizeof r); + + std::vector buffer; + buffer.reserve(sizeof h + sizeof r); + buffer.insert(buffer.end(), + reinterpret_cast(&h), + reinterpret_cast(&h) + sizeof h); + buffer.insert(buffer.end(), + reinterpret_cast(&r), + reinterpret_cast(&r) + sizeof r); + + if (!client.socket->write(buffer)) { + return false; + } + + return true; +} + +bool Server::handleLogin(Client& client, const message::Login& message) { + logger_.log("Server", + "Received login with token " + std::to_string(message.token) + + " from client " + std::to_string(client.socket->fileDescriptor()) + '.'); + + bool found = tokens_.find(message.token) != tokens_.cend(); + + message::Reply r; + + if (!found) { + r.code = message::ReplyCode::WrongLogin; + } else { + r.code = message::ReplyCode::OK; + } + + message::Header h; + h.type = message::Type::Reply; + h.size = htobe16(sizeof h + sizeof r); + + std::vector buffer; + buffer.reserve(sizeof h + sizeof r); + buffer.insert(buffer.end(), + reinterpret_cast(&h), + reinterpret_cast(&h) + sizeof h); + buffer.insert(buffer.end(), + reinterpret_cast(&r), + reinterpret_cast(&r) + sizeof r); + + if (!client.socket->write(buffer)) { + return false; + } + + if (!found) { + logger_.log("Server", + "Rejected login with token " + std::to_string(message.token) + + " from client " + std::to_string(client.socket->fileDescriptor()) + '.'); + return true; + } + + client.loggedIn = true; + logger_.log("Server", + "Client " + std::to_string(client.socket->fileDescriptor()) + + " logged in with token " + std::to_string(message.token) + '.'); + + return true; +} + +bool Server::handleRequest(Client& client, const message::Request& message) { + logger_.log("Server", + "Received request " + std::to_string(static_cast(message.firstMissing)) + + '-' + std::to_string(static_cast(message.lastMissing)) + + " from client " + std::to_string(client.socket->fileDescriptor()) + '.'); + + message::Reply r; + + if (!client.loggedIn) { + r.code = message::ReplyCode::NotLoggedIn; + } else { + r.code = message::ReplyCode::OK; + } + + message::Header h; + h.type = message::Type::Reply; + h.size = htobe16(sizeof h + sizeof r); + + std::vector buffer; + buffer.reserve(sizeof h + sizeof r); + buffer.insert(buffer.end(), + reinterpret_cast(&h), + reinterpret_cast(&h) + sizeof h); + buffer.insert(buffer.end(), + reinterpret_cast(&r), + reinterpret_cast(&r) + sizeof r); + + if (!client.socket->write(buffer)) { + return false; + } + + if (!client.loggedIn) { + logger_.log("Server", + "Rejected request " + std::to_string(static_cast(message.firstMissing)) + + '-' + std::to_string(static_cast(message.lastMissing)) + + " to unauthentified client " + std::to_string(client.socket->fileDescriptor()) + '.'); + return true; + } + + //TODO: SEND HISTORY. + + return true; +} diff --git a/src/Server.hpp b/src/Server.hpp new file mode 100644 index 0000000..8524512 --- /dev/null +++ b/src/Server.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Logger.hpp" +#include "Message.hpp" +#include "Socket.hpp" + +class Server { +public: + Server(Logger& logger); + ~Server(); + + Server(const Server&) = delete; + Server& operator=(const Server&) = delete; + + Server(Server&&) = delete; + Server& operator=(Server&&) = delete; + + bool init(const std::filesystem::path& tokenFilePath, const std::filesystem::path& historyFilePath); + bool addNews(const std::uint32_t timestamp, std::string news); + bool start(const std::uint16_t port); + void stop(); + + bool listening() const; + +private: + struct Client { + std::unique_ptr socket; + bool loggedIn{false}; + }; + + Logger& logger_; + std::mutex externalMutex_; + + std::unordered_set tokens_; + + std::ofstream history_; + std::map news_; + std::mutex newsMutex_; + + std::deque> sendQueue_; + std::mutex queueMutex_; + + std::thread socketThread_; + std::atomic running_{false}; + std::atomic listening_{false}; + std::atomic stopSignal_{false}; + + void runThread(const std::uint16_t port); + bool handleMessage(Client& client); + bool handleUnknownMessage(Client& client, const message::Header& message); + bool handleLogin(Client& client, const message::Login& message); + bool handleRequest(Client& client, const message::Request& message); +}; diff --git a/src/Socket.cpp b/src/Socket.cpp new file mode 100644 index 0000000..94f702c --- /dev/null +++ b/src/Socket.cpp @@ -0,0 +1,299 @@ +#include "Socket.hpp" + +#include +#include +#include +#include +#include +#include // close +#include // inet_pton inet_ntop +#include // sockaddr_in +#include +#include // listen + +Socket::Socket(Logger& logger) : logger_(logger) {} + +Socket::~Socket() { + if (open_) { + close(fileDescriptor_); + logger_.log("Socket " + std::to_string(fileDescriptor_), "Closed."); + } +} + +bool Socket::init() { + if (open_) { + close(fileDescriptor_); + open_ = false; + logger_.log("Socket " + std::to_string(fileDescriptor_), "Closed."); + fileDescriptor_ = 0; + } + + std::int32_t fd = socket(AF_INET /* ipv4 */, SOCK_STREAM /* tcp */, 0 /* single protocol */); + + if (fd < 0) { + logger_.log("Socket", "Could not create socket. Error code " + std::to_string(errno) + ": " + strerror(errno)); + return false; + } + + fileDescriptor_ = fd; + open_ = true; + logger_.log("Socket " + std::to_string(fileDescriptor_), "Initialized."); + return true; +} + +void Socket::init(const std::uint32_t fileDescriptor) { + fileDescriptor_ = fileDescriptor; + open_ = true; +} + +bool Socket::listen(const std::uint16_t port) { + if (!open_) { + logger_.log("Socket", "Not open."); + return false; + } + + if (port == 0) { + logger_.log("Socket " + std::to_string(fileDescriptor_), "Invalid port value " + std::to_string(port) + '.'); + return false; + } + + sockaddr_in socketAddress{}; + socketAddress.sin_addr.s_addr = INADDR_ANY; // Any incoming address from any interface. + socketAddress.sin_family = AF_INET; // ipv4. + socketAddress.sin_port = htobe16(port); + + // Enable fast reusable address for this socket file descriptor used to listen to incoming connections. + std::uint32_t optValue = 1; + setsockopt(fileDescriptor_, SOL_SOCKET, SO_REUSEADDR, &optValue, sizeof optValue); + // Set non waiting. + fcntl(fileDescriptor_, F_SETFL, O_NONBLOCK); + + std::int32_t bindResult = bind(fileDescriptor_, reinterpret_cast(&socketAddress), sizeof socketAddress); + + if (bindResult < 0) { + logger_.log("Socket " + std::to_string(fileDescriptor_), + "Could not bind socket " + std::to_string(fileDescriptor_) + " to port " + std::to_string(port) + + ". Error code " + std::to_string(errno) + ": " + strerror(errno)); + return false; + } + + std::int32_t listenResult = ::listen(fileDescriptor_, SOMAXCONN); + + if (listenResult < 0) { + logger_.log("Socket " + std::to_string(fileDescriptor_), + "Could not listen with socket " + std::to_string(fileDescriptor_) + + " on port " + std::to_string(port) + + ". Error code " + std::to_string(errno) + ": " + strerror(errno)); + return false; + } + + logger_.log("Socket " + std::to_string(fileDescriptor_), "Listening to port " + std::to_string(port) + '.'); + return true; +} + +bool Socket::accept(Socket& client) { + sockaddr_in clientSocketAddress{}; + socklen_t clientSocketAddressSize = sizeof clientSocketAddress; + + std::int32_t fd = ::accept(fileDescriptor_, + reinterpret_cast(&clientSocketAddress), + &clientSocketAddressSize); + + if (fd < 0) { + // No new client to accept. + if (errno == EAGAIN or errno == EWOULDBLOCK) { + return true; + } + + logger_.log("Socket " + std::to_string(fileDescriptor_), + "Could not create client socket. Error code " + std::to_string(errno) + ": " + strerror(errno)); + return false; + } + + client.init(fd); + + char s[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &clientSocketAddress.sin_addr, s, INET_ADDRSTRLEN); + + logger_.log("Socket " + std::to_string(fileDescriptor_), "Accepted client socket " + std::to_string(fd) + + " from " + std::string(s) + ':' + + std::to_string(be16toh(clientSocketAddress.sin_port)) + '.'); + return true; +} + +bool Socket::connect(const std::string& address, const std::uint16_t port) { + if (!open_) { + logger_.log("Socket", "Not open."); + return false; + } + + sockaddr_in socketAddress{}; + + if (std::int32_t r = inet_pton(AF_INET, address.c_str(), &socketAddress.sin_addr) <= 0) { + logger_.log("Socket " + std::to_string(fileDescriptor_), + "Could not resolve address " + address + ". Error code " + std::to_string(r) + '.'); + return false; + } + + if (port == 0) { + logger_.log("Socket " + std::to_string(fileDescriptor_), "Invalid port value " + std::to_string(port) + '.'); + return false; + } + + socketAddress.sin_family = AF_INET; + socketAddress.sin_port = htobe16(port); + + std::int32_t connectResult = ::connect(fileDescriptor_, + reinterpret_cast(&socketAddress), + sizeof socketAddress); + + if (connectResult != 0) { + logger_.log("Socket " + std::to_string(fileDescriptor_), + "Could not connect socket to " + address + ':' + std::to_string(port) + + ". Error code " + std::to_string(errno) + ": " + strerror(errno) + '.'); + return false; + } + + logger_.log("Socket " + std::to_string(fileDescriptor_), + "Connected to " + address + ':' + std::to_string(port) + '.'); + return true; +} + +bool Socket::read(std::vector& buffer) { + std::int32_t r = ::read(fileDescriptor_, buffer.data(), buffer.size()); + + if (r < 0) { + logger_.log("Socket " + std::to_string(fileDescriptor_), + "Could not read. Error code " + std::to_string(errno) + ": " + strerror(errno) + '.'); + return false; + } + + buffer.resize(r); + logger_.log("Socket " + std::to_string(fileDescriptor_), "Read " + std::to_string(r) + " bytes."); + return true; +} + +bool Socket::write(const std::vector& buffer) { + std::int32_t r = ::write(fileDescriptor_, buffer.data(), buffer.size()); + + if (r < 0) { + logger_.log("Socket " + std::to_string(fileDescriptor_), + "Could not write. Error code " + std::to_string(errno) + '.'); + return false; + } + + if (r < buffer.size()) { + logger_.log("Socket " + std::to_string(fileDescriptor_), + "Could not write all data. " + std::to_string(r) + '/' + std::to_string(buffer.size()) + '.'); + return false; + } + + logger_.log("Socket " + std::to_string(fileDescriptor_), "Wrote " + std::to_string(r) + " bytes."); + return true; +} + +bool Socket::isOpen() const { + return open_; +} + +std::uint32_t Socket::fileDescriptor() const { + return fileDescriptor_; +} + +SocketPoller::SocketPoller(Logger& logger) : logger_(logger) {} + +SocketPoller::~SocketPoller() { + if (open_) { + close(fileDescriptor_); + logger_.log("SocketPoller " + std::to_string(fileDescriptor_), "Closed."); + } +} + +bool SocketPoller::init() { + if (open_) { + close(fileDescriptor_); + open_ = false; + logger_.log("SocketPoller " + std::to_string(fileDescriptor_), "Closed."); + fileDescriptor_ = 0; + } + + std::int32_t fd = epoll_create1(0); + + if (fd < 0) { + logger_.log("SocketPoller", + "Could not create poll. Error code " + std::to_string(errno) + ": " + strerror(errno)); + return false; + } + + fileDescriptor_ = fd; + open_ = true; + logger_.log("SocketPoller " + std::to_string(fileDescriptor_), "Initialized."); + return true; +} + +bool SocketPoller::add(const Socket& socket) { + if (!open_) { + logger_.log("SocketPoller", "Not open."); + return false; + } + + epoll_event ev{}; + ev.events = EPOLLIN; + ev.data.fd = socket.fileDescriptor(); + + std::int32_t r = epoll_ctl(fileDescriptor_, EPOLL_CTL_ADD, socket.fileDescriptor(), &ev); + + if (r < 0) { + logger_.log("SocketPoller " + std::to_string(fileDescriptor_), + "Could not add socket " + std::to_string(socket.fileDescriptor()) + + ". Error code " + std::to_string(errno) + ": " + strerror(errno) + '.'); + return false; + } + + logger_.log("SocketPoller " + std::to_string(fileDescriptor_), + "Added socket " + std::to_string(socket.fileDescriptor()) + '.'); + return true; +} + +void SocketPoller::remove(const Socket& socket) { + if (!open_) { + return; + } + + std::int32_t r = epoll_ctl(fileDescriptor_, EPOLL_CTL_DEL, socket.fileDescriptor(), nullptr); + + if (r < 0) { + logger_.log("SocketPoller " + std::to_string(fileDescriptor_), + "Could not remove socket " + std::to_string(socket.fileDescriptor()) + + ". Error code " + std::to_string(errno) + ": " + strerror(errno) + '.'); + return; + } + + logger_.log("SocketPoller " + std::to_string(fileDescriptor_), + "Removed socket " + std::to_string(socket.fileDescriptor()) + '.'); +} + +bool SocketPoller::poll(std::unordered_set& fileDescriptors) { + if (!open_) { + logger_.log("SocketPoller", "Not open."); + return false; + } + + std::array events{}; + + std::int32_t r = epoll_wait(fileDescriptor_, events.data(), events.size(), 100 /* timeout in ms */); + + if (r < 0) { + logger_.log("SocketPoller " + std::to_string(fileDescriptor_), + "Could not poll. Error code " + std::to_string(errno) + ": " + strerror(errno) + '.'); + return false; + } + + for (std::int32_t i(0); i < r ; ++i) { + logger_.log("SocketPoller " + std::to_string(fileDescriptor_), + "New event from socket " + std::to_string(events[i].data.fd) + '.'); + fileDescriptors.insert(events[i].data.fd); + } + + return true; +} diff --git a/src/Socket.hpp b/src/Socket.hpp new file mode 100644 index 0000000..e946724 --- /dev/null +++ b/src/Socket.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include + +#include "Logger.hpp" + +class Socket { +public: + Socket(Logger& logger); + ~Socket(); + + Socket(const Socket&) = delete; + Socket& operator=(const Socket&) = delete; + + Socket(Socket&&) = delete; + Socket& operator=(Socket&&) = delete; + + bool init(); + void init(const std::uint32_t fileDescriptor); + + bool listen(const std::uint16_t port); + bool accept(Socket& client); + bool connect(const std::string& address, const std::uint16_t port); + + bool read(std::vector& buffer); + bool write(const std::vector& buffer); + + bool isOpen() const; + std::uint32_t fileDescriptor() const; + +private: + Logger& logger_; + + bool open_{false}; + std::uint32_t fileDescriptor_{0}; +}; + +class SocketPoller { +public: + SocketPoller(Logger& logger); + ~SocketPoller(); + + SocketPoller(const SocketPoller&) = delete; + SocketPoller& operator=(const SocketPoller&) = delete; + + SocketPoller(SocketPoller&&) = delete; + SocketPoller& operator=(SocketPoller&&) = delete; + + bool init(); + + bool add(const Socket& socket); + void remove(const Socket& socket); + bool poll(std::unordered_set& fileDescriptors); + +private: + Logger& logger_; + + bool open_{false}; + std::uint32_t fileDescriptor_{0}; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ae7c94d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,10 @@ +#include "Logger.hpp" +#include "Server.hpp" + +int main() +{ + Logger logger; + Server server(logger); + + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..0db8ace --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,39 @@ +find_package(GTest REQUIRED) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/history.txt ${CMAKE_CURRENT_BINARY_DIR}/history_ref.txt COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/history_clean.txt ${CMAKE_CURRENT_BINARY_DIR}/history_clean_ref.txt COPYONLY) + +add_library(${PROJECT_NAME}Client OBJECT) +target_sources(${PROJECT_NAME}Client PRIVATE + Client.cpp + Client.hpp) +target_link_libraries(${PROJECT_NAME}Client PUBLIC + ${PROJECT_NAME}Objects) + +add_executable(${PROJECT_NAME}TestClient) +target_sources(${PROJECT_NAME}TestClient PRIVATE testClient.cpp) +target_link_libraries(${PROJECT_NAME}TestClient PRIVATE + GTest::gtest_main + ${PROJECT_NAME}Client + ${PROJECT_NAME}Objects) +set_target_properties(${PROJECT_NAME}TestClient PROPERTIES OUTPUT_NAME "testclient") +add_test(NAME ${PROJECT_NAME}TestClient COMMAND testclient) + +add_executable(${PROJECT_NAME}TestServer) +target_compile_definitions(${PROJECT_NAME}TestServer PRIVATE SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") +target_compile_definitions(${PROJECT_NAME}TestServer PRIVATE BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}") +target_sources(${PROJECT_NAME}TestServer PRIVATE testServer.cpp) +target_link_libraries(${PROJECT_NAME}TestServer PRIVATE + GTest::gtest_main + ${PROJECT_NAME}Client + ${PROJECT_NAME}Objects) +set_target_properties(${PROJECT_NAME}TestServer PROPERTIES OUTPUT_NAME "testserver") +add_test(NAME ${PROJECT_NAME}TestServer COMMAND testserver) + +add_executable(${PROJECT_NAME}TestSocket) +target_sources(${PROJECT_NAME}TestSocket PRIVATE testSocket.cpp) +target_link_libraries(${PROJECT_NAME}TestSocket PRIVATE + GTest::gtest_main + ${PROJECT_NAME}Objects) +set_target_properties(${PROJECT_NAME}TestSocket PROPERTIES OUTPUT_NAME "testsocket") +add_test(NAME ${PROJECT_NAME}TestSocket COMMAND testsocket) diff --git a/tests/Client.cpp b/tests/Client.cpp new file mode 100644 index 0000000..db6e9e0 --- /dev/null +++ b/tests/Client.cpp @@ -0,0 +1,285 @@ +#include "Client.hpp" + +#include // memcpy +#include + +#include "Message.hpp" + +Client::Client(Logger& logger) : logger_(logger), socket_(logger_) {} + +bool Client::init() { + if (!socket_.init()) { + return false; + } + + logger_.log("Client", "Created with socket " + std::to_string(socket_.fileDescriptor()) + '.'); + return true; +} + +bool Client::connect(const std::string& ip, const std::uint16_t port) { + if (!socket_.connect(ip, port)) { + return false; + } + + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Connected to " + ip + ":" + std::to_string(port) + '.'); + return true; +} + +bool Client::read() { + std::vector buffer; + buffer.resize(sizeof(message::Header)); + + if (!socket_.read(buffer)) { + return false; + } + + message::Header h; + std::memcpy(&h, buffer.data(), sizeof h); + h.size = be16toh(h.size); + + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received message type " + std::to_string(static_cast(h.type)) + + " size " + std::to_string(h.size) + '.'); + + switch (h.type) { + case message::Type::News: { + buffer.resize(h.size - sizeof h); + + if (!socket_.read(buffer)) { + return false; + } + + message::News n; + std::memcpy(&n, buffer.data(), sizeof n); + n.timestamp = be32toh(n.timestamp); + + std::string news(buffer.begin() + sizeof n, buffer.end()); + + history_.try_emplace(n.timestamp, news); + + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received timestamp " + std::to_string(n.timestamp) + " news " + news); + } break; + case message::Type::Reply: { + buffer.resize(h.size - sizeof h); + + if (!socket_.read(buffer)) { + return false; + } + + message::Reply r; + std::memcpy(&r, buffer.data(), sizeof r); + + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received reply code " + std::to_string(static_cast(r.code)) + '.'); + } break; + default: { + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received unknown message type " + std::to_string(static_cast(h.type)) + + " size " + std::to_string(h.size) + '.'); + return false; + } break; + } + + return true; +} + +bool Client::unknown() { + message::Reply su; + + message::Header sh; + sh.type = message::Type::Reply; + sh.size = htobe16(sizeof sh + sizeof su); + + std::vector buffer; + buffer.reserve(sizeof sh + sizeof su); + buffer.insert(buffer.end(), + reinterpret_cast(&sh), + reinterpret_cast(&sh) + sizeof sh); + buffer.insert(buffer.end(), + reinterpret_cast(&su), + reinterpret_cast(&su) + sizeof su); + + if (!socket_.write(buffer)) { + return false; + } + + buffer.resize(sizeof(message::Header)); + + if (!socket_.read(buffer)) { + return false; + } + + if (buffer.size() < sizeof(message::Header)) { + return false; + } + + message::Header rh; + std::memcpy(&rh, buffer.data(), sizeof rh); + rh.size = be16toh(rh.size); + + if (rh.type != message::Type::Reply) { + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received message type " + std::to_string(static_cast(rh.type)) + '.'); + return false; + } + + buffer.resize(rh.size - sizeof rh); + + if (!socket_.read(buffer)) { + return false; + } + + if (buffer.size() < sizeof(message::Reply)) { + return false; + } + + message::Reply rr; + std::memcpy(&rr, buffer.data(), sizeof rr); + + if (rr.code != message::ReplyCode::UnknownType) { + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received reply error code " + std::to_string(static_cast(rr.code)) + '.'); + return false; + } + + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), "Received unknown reply."); + return true; +} + +bool Client::login(const std::uint64_t token) { + message::Login sl; + sl.token = htobe64(token); + + message::Header sh; + sh.type = message::Type::Login; + sh.size = htobe16(sizeof sh + sizeof sl); + + std::vector buffer; + buffer.reserve(sizeof sh + sizeof sl); + buffer.insert(buffer.end(), + reinterpret_cast(&sh), + reinterpret_cast(&sh) + sizeof sh); + buffer.insert(buffer.end(), + reinterpret_cast(&sl), + reinterpret_cast(&sl) + sizeof sl); + + if (!socket_.write(buffer)) { + return false; + } + + buffer.resize(sizeof(message::Header)); + + if (!socket_.read(buffer)) { + return false; + } + + if (buffer.size() < sizeof(message::Header)) { + return false; + } + + message::Header rh; + std::memcpy(&rh, buffer.data(), sizeof rh); + rh.size = be16toh(rh.size); + + if (rh.type != message::Type::Reply) { + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received message type " + std::to_string(static_cast(rh.type)) + '.'); + return false; + } + + buffer.resize(rh.size - sizeof rh); + + if (!socket_.read(buffer)) { + return false; + } + + if (buffer.size() < sizeof(message::Reply)) { + return false; + } + + message::Reply rr; + std::memcpy(&rr, buffer.data(), sizeof rr); + + if (rr.code != message::ReplyCode::OK) { + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received reply error code " + std::to_string(static_cast(rr.code)) + '.'); + return false; + } + + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Logged in with token " + std::to_string(token) + '.'); + return true; +} + +bool Client::request(const std::uint32_t firstMissing, const std::uint32_t lastMissing) { + message::Request sr; + sr.firstMissing = htobe32(firstMissing); + sr.lastMissing = htobe32(lastMissing); + + message::Header sh; + sh.type = message::Type::Request; + sh.size = htobe16(sizeof sh + sizeof sr); + + std::vector buffer; + buffer.reserve(sizeof sh + sizeof sr); + buffer.insert(buffer.end(), + reinterpret_cast(&sh), + reinterpret_cast(&sh) + sizeof sh); + buffer.insert(buffer.end(), + reinterpret_cast(&sr), + reinterpret_cast(&sr) + sizeof sr); + + if (!socket_.write(buffer)) { + return false; + } + + buffer.resize(sizeof(message::Header)); + + if (!socket_.read(buffer)) { + return false; + } + + if (buffer.size() < sizeof(message::Header)) { + return false; + } + + message::Header rh; + std::memcpy(&rh, buffer.data(), sizeof rh); + rh.size = be16toh(rh.size); + + if (rh.type != message::Type::Reply) { + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received message type " + std::to_string(static_cast(rh.type)) + '.'); + return false; + } + + buffer.resize(rh.size - sizeof rh); + + if (!socket_.read(buffer)) { + return false; + } + + if (buffer.size() < sizeof(message::Reply)) { + return false; + } + + message::Reply rr; + std::memcpy(&rr, buffer.data(), sizeof rr); + + if (rr.code != message::ReplyCode::OK) { + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Received reply error code " + std::to_string(static_cast(rr.code)) + '.'); + return false; + } + + logger_.log("Client " + std::to_string(socket_.fileDescriptor()), + "Requested history from " + std::to_string(firstMissing) + + " to " + std::to_string(lastMissing) + '.'); + return true; +} + +const std::map& Client::history() const { + return history_; +} diff --git a/tests/Client.hpp b/tests/Client.hpp new file mode 100644 index 0000000..5f4f514 --- /dev/null +++ b/tests/Client.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include "Logger.hpp" +#include "Socket.hpp" + +class Client { +public: + Client(Logger& logger); + ~Client() = default; + + Client(const Client&) = delete; + Client& operator=(const Client&) = delete; + + Client(Client&&) = delete; + Client& operator=(Client&&) = delete; + + bool init(); + bool connect(const std::string& ip, const std::uint16_t port); + bool read(); + bool unknown(); + bool login(const std::uint64_t token); + bool request(const std::uint32_t firstMissing, const std::uint32_t lastMissing); + + const std::map& history() const; + +private: + Logger& logger_; + + Socket socket_; + std::map history_; +}; diff --git a/tests/history.txt b/tests/history.txt new file mode 100644 index 0000000..f55f641 --- /dev/null +++ b/tests/history.txt @@ -0,0 +1,5 @@ +|invalid +invalid|invalid +123|This is some news. +456| +789|Last news. diff --git a/tests/history_clean.txt b/tests/history_clean.txt new file mode 100644 index 0000000..dc3ef59 --- /dev/null +++ b/tests/history_clean.txt @@ -0,0 +1,3 @@ +123|This is some news. +456|More news. +789|Last news. diff --git a/tests/testClient.cpp b/tests/testClient.cpp new file mode 100644 index 0000000..59cacb1 --- /dev/null +++ b/tests/testClient.cpp @@ -0,0 +1,8 @@ +#include + +#include "Client.hpp" + +TEST(Client, Client) { + Logger logger; + Client client(logger); +} diff --git a/tests/testServer.cpp b/tests/testServer.cpp new file mode 100644 index 0000000..e66dfe5 --- /dev/null +++ b/tests/testServer.cpp @@ -0,0 +1,125 @@ +#include + +#include +#include + +#include "Client.hpp" +#include "Server.hpp" + +const std::filesystem::path sourceDir = SOURCE_DIR; +const std::filesystem::path binaryDir = BINARY_DIR; + +namespace { +bool waitListening(Server& server) { + auto start = std::chrono::steady_clock::now(); + + while (std::chrono::steady_clock::now() - start < std::chrono::seconds(1)) { + if (server.listening()) { + return true; + } + } + + return false; +} +} + +TEST(Server, NewFile) { + std::filesystem::remove(binaryDir / "test.txt"); + Logger logger; + Server server(logger); + ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "test.txt")); + EXPECT_TRUE(server.addNews(1234, "EEEE")); + EXPECT_FALSE(server.addNews(1234, "EEEEEE")); +} + +TEST(Server, ExistingFile) { + std::filesystem::remove(binaryDir / "history.txt"); + ASSERT_TRUE(std::filesystem::copy_file(binaryDir / "history_ref.txt", binaryDir / "history.txt")); + Logger logger; + Server server(logger); + ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "history.txt")); + EXPECT_FALSE(server.addNews(123, "EEEE")); + EXPECT_TRUE(server.addNews(1234, "EEEEEE")); +} + +TEST(Server, Connect) { + std::filesystem::remove(binaryDir / "history_clean.txt"); + ASSERT_TRUE(std::filesystem::copy_file(binaryDir / "history_clean_ref.txt", binaryDir / "history_clean.txt")); + Logger logger; + Server server(logger); + server.stop(); + ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "history_clean.txt")); + ASSERT_TRUE(server.start(2000)); + ASSERT_TRUE(waitListening(server)); + + Client c1(logger); + ASSERT_TRUE(c1.init()); + ASSERT_TRUE(c1.connect("127.0.0.1", 2000)); + + ASSERT_TRUE(c1.unknown()); + ASSERT_FALSE(c1.login(11114444)); + ASSERT_TRUE(c1.login(11115555)); +} + +TEST(Server, News) { + std::filesystem::remove(binaryDir / "history_clean.txt"); + ASSERT_TRUE(std::filesystem::copy_file(binaryDir / "history_clean_ref.txt", binaryDir / "history_clean.txt")); + Logger logger; + Server server(logger); + server.stop(); + ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "history_clean.txt")); + ASSERT_TRUE(server.start(2000)); + ASSERT_TRUE(waitListening(server)); + + Client c1(logger); + Client c2(logger); + ASSERT_TRUE(c1.init()); + ASSERT_TRUE(c2.init()); + ASSERT_TRUE(c1.connect("127.0.0.1", 2000)); + ASSERT_TRUE(c2.connect("127.0.0.1", 2000)); + + ASSERT_TRUE(c1.login(11115555)); + + // This news is only going to be received by c1. c2 not logged in. + ASSERT_TRUE(server.addNews(1111, "Hello")); + + ASSERT_TRUE(c1.read()); + EXPECT_EQ(c1.history().size(), 1); + ASSERT_TRUE(c1.history().find(1111) != c1.history().cend()); + EXPECT_EQ(c1.history().at(1111), "Hello"); + + ASSERT_TRUE(c2.login(11116666)); + + // This news is only going to be received by c1 and c2. + ASSERT_TRUE(server.addNews(2222, "Helloo")); + + ASSERT_TRUE(c1.read()); + ASSERT_TRUE(c2.read()); + + EXPECT_EQ(c1.history().size(), 2); + ASSERT_TRUE(c1.history().find(2222) != c1.history().cend()); + EXPECT_EQ(c1.history().at(2222), "Helloo"); + EXPECT_EQ(c2.history().size(), 1); + ASSERT_TRUE(c2.history().find(2222) != c2.history().cend()); + EXPECT_EQ(c2.history().at(2222), "Helloo"); +} + +TEST(Server, Request) { + std::filesystem::remove(binaryDir / "history_clean.txt"); + ASSERT_TRUE(std::filesystem::copy_file(binaryDir / "history_clean_ref.txt", binaryDir / "history_clean.txt")); + Logger logger; + Server server(logger); + server.stop(); + ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "history_clean.txt")); + ASSERT_TRUE(server.start(2000)); + ASSERT_TRUE(waitListening(server)); + + Client c1(logger); + ASSERT_TRUE(c1.init()); + ASSERT_TRUE(c1.connect("127.0.0.1", 2000)); + + ASSERT_FALSE(c1.request(124, 789)); // Not logged in. + EXPECT_TRUE(c1.history().empty()); + ASSERT_TRUE(c1.login(11115555)); + ASSERT_TRUE(c1.request(124, 789)); +} diff --git a/tests/testSocket.cpp b/tests/testSocket.cpp new file mode 100644 index 0000000..36c8e37 --- /dev/null +++ b/tests/testSocket.cpp @@ -0,0 +1,112 @@ +#include + +#include +#include +#include +#include + +#include "Socket.hpp" + +TEST(Socket, Socket) { + Logger logger; + Socket serverSocket(logger); + + ASSERT_FALSE(serverSocket.listen(2000)); + ASSERT_TRUE(serverSocket.init()); + ASSERT_FALSE(serverSocket.listen(0)); + ASSERT_TRUE(serverSocket.listen(2000)); + + std::vector ref{std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}}; + + std::thread t([&logger, &serverSocket, &ref]() { + Socket client(logger); + + std::chrono::time_point start = std::chrono::steady_clock::now(); + + while (!client.isOpen()) { + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(1)) { + return; + } + + serverSocket.accept(client); + } + + EXPECT_TRUE(client.write(ref)); + }); + + Socket clientSocket(logger); + ASSERT_FALSE(clientSocket.connect("127.0.0.1", 2000)); + ASSERT_TRUE(clientSocket.init()); + ASSERT_FALSE(clientSocket.connect("INVALID", 2000)); + ASSERT_FALSE(clientSocket.connect("127.0.0.1", 0)); + ASSERT_TRUE(clientSocket.connect("127.0.0.1", 2000)); + std::vector result(4); + ASSERT_TRUE(clientSocket.read(result)); + + t.join(); + + EXPECT_EQ(result, ref); +} + +TEST(Socket, SocketPoller) { + Logger logger; + Socket serverSocket(logger); + ASSERT_TRUE(serverSocket.init()); + ASSERT_TRUE(serverSocket.listen(2000)); + + SocketPoller poller(logger); + ASSERT_TRUE(poller.init()); + ASSERT_TRUE(poller.add(serverSocket)); + + std::vector messageRef{std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}}; + + std::thread t([&logger, &serverSocket, &poller, &messageRef]() { + std::uint8_t count = 2; + std::unordered_map> clientSockets; + + while (count > 0) { + std::unordered_set pollResult; + ASSERT_TRUE(poller.poll(pollResult)); + + for (const auto& fd : pollResult) { + // Event from listener socket. + if (fd == serverSocket.fileDescriptor()) { + std::unique_ptr newClientSocket = std::make_unique(logger); + ASSERT_TRUE(serverSocket.accept(*newClientSocket)); + ASSERT_TRUE(newClientSocket->isOpen()); + ASSERT_TRUE(poller.add(*newClientSocket)); + ASSERT_TRUE(clientSockets.try_emplace(newClientSocket->fileDescriptor(), + std::move(newClientSocket)).second); + + continue; + } + + // Event from a client socket. + auto it = clientSockets.find(fd); + + ASSERT_TRUE(it != clientSockets.end()); // Event should be from the client socket list. + + // Read data sent from client. + std::vector messageResult(4); + ASSERT_NE(it->second, nullptr); + ASSERT_TRUE(it->second->read(messageResult)); + EXPECT_EQ(messageResult, messageRef); + count--; + } + } + }); + + Socket client1(logger); + Socket client2(logger); + + ASSERT_TRUE(client1.init()); + ASSERT_TRUE(client2.init()); + + ASSERT_TRUE(client1.connect("127.0.0.1", 2000)); + ASSERT_TRUE(client2.connect("127.0.0.1", 2000)); + + EXPECT_TRUE(client1.write(messageRef)); + EXPECT_TRUE(client2.write(messageRef)); + + t.join(); +} diff --git a/tests/tokens.txt b/tests/tokens.txt new file mode 100644 index 0000000..f6b2fcc --- /dev/null +++ b/tests/tokens.txt @@ -0,0 +1,2 @@ +11115555 +11116666 -- 2.47.3