--- /dev/null
+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()
--- /dev/null
+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)
--- /dev/null
+#include "Logger.hpp"
+
+#include <iostream>
+
+void Logger::log(const std::string& entity, const std::string& message) {
+ std::lock_guard lock(externalMutex_);
+ std::cout << entity << ": " << message << std::endl;
+}
--- /dev/null
+#pragma once
+
+#include <mutex>
+#include <string>
+
+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_;
+};
--- /dev/null
+#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)
--- /dev/null
+#include "Server.hpp"
+
+#include <cstring> // memcpy
+#include <endian.h>
+#include <sstream>
+#include <unordered_map>
+
+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<std::uint32_t, Client> clients;
+
+ while (!stopSignal_) {
+ std::unordered_set<std::uint32_t> 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<Socket>(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<std::byte> buffer;
+ buffer.reserve(sizeof h + sizeof n + news.size());
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&h),
+ reinterpret_cast<const std::byte*>(&h) + sizeof h);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&n),
+ reinterpret_cast<const std::byte*>(&n) + sizeof n);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(news.data()),
+ reinterpret_cast<const std::byte*>(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<std::byte> 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<std::uint16_t>(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<std::uint16_t>(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<std::byte> buffer;
+ buffer.reserve(sizeof h + sizeof r);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&h),
+ reinterpret_cast<const std::byte*>(&h) + sizeof h);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&r),
+ reinterpret_cast<const std::byte*>(&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<std::byte> buffer;
+ buffer.reserve(sizeof h + sizeof r);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&h),
+ reinterpret_cast<const std::byte*>(&h) + sizeof h);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&r),
+ reinterpret_cast<const std::byte*>(&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<std::uint16_t>(message.firstMissing))
+ + '-' + std::to_string(static_cast<std::uint16_t>(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<std::byte> buffer;
+ buffer.reserve(sizeof h + sizeof r);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&h),
+ reinterpret_cast<const std::byte*>(&h) + sizeof h);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&r),
+ reinterpret_cast<const std::byte*>(&r) + sizeof r);
+
+ if (!client.socket->write(buffer)) {
+ return false;
+ }
+
+ if (!client.loggedIn) {
+ logger_.log("Server",
+ "Rejected request " + std::to_string(static_cast<std::uint16_t>(message.firstMissing))
+ + '-' + std::to_string(static_cast<std::uint16_t>(message.lastMissing))
+ + " to unauthentified client " + std::to_string(client.socket->fileDescriptor()) + '.');
+ return true;
+ }
+
+ //TODO: SEND HISTORY.
+
+ return true;
+}
--- /dev/null
+#pragma once
+
+#include <atomic>
+#include <cstdint>
+#include <deque>
+#include <filesystem>
+#include <fstream>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <unordered_set>
+#include <vector>
+
+#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> socket;
+ bool loggedIn{false};
+ };
+
+ Logger& logger_;
+ std::mutex externalMutex_;
+
+ std::unordered_set<std::uint64_t> tokens_;
+
+ std::ofstream history_;
+ std::map<std::uint32_t, std::string> news_;
+ std::mutex newsMutex_;
+
+ std::deque<std::pair<std::uint32_t, std::string>> sendQueue_;
+ std::mutex queueMutex_;
+
+ std::thread socketThread_;
+ std::atomic<bool> running_{false};
+ std::atomic<bool> listening_{false};
+ std::atomic<bool> 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);
+};
--- /dev/null
+#include "Socket.hpp"
+
+#include <array>
+#include <cstring>
+#include <endian.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h> // close
+#include <arpa/inet.h> // inet_pton inet_ntop
+#include <netinet/in.h> // sockaddr_in
+#include <sys/epoll.h>
+#include <sys/socket.h> // 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<sockaddr*>(&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<sockaddr*>(&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<sockaddr*>(&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<std::byte>& 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<std::byte>& 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<std::uint32_t>& fileDescriptors) {
+ if (!open_) {
+ logger_.log("SocketPoller", "Not open.");
+ return false;
+ }
+
+ std::array<epoll_event, 100 /* max events */> 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;
+}
--- /dev/null
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+#include <unordered_set>
+
+#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<std::byte>& buffer);
+ bool write(const std::vector<std::byte>& 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<std::uint32_t>& fileDescriptors);
+
+private:
+ Logger& logger_;
+
+ bool open_{false};
+ std::uint32_t fileDescriptor_{0};
+};
--- /dev/null
+#include "Logger.hpp"
+#include "Server.hpp"
+
+int main()
+{
+ Logger logger;
+ Server server(logger);
+
+ return 0;
+}
--- /dev/null
+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)
--- /dev/null
+#include "Client.hpp"
+
+#include <cstring> // memcpy
+#include <endian.h>
+
+#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<std::byte> 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<std::uint16_t>(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<std::uint16_t>(r.code)) + '.');
+ } break;
+ default: {
+ logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+ "Received unknown message type " + std::to_string(static_cast<std::uint16_t>(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<std::byte> buffer;
+ buffer.reserve(sizeof sh + sizeof su);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&sh),
+ reinterpret_cast<const std::byte*>(&sh) + sizeof sh);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&su),
+ reinterpret_cast<const std::byte*>(&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<std::uint16_t>(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<std::uint16_t>(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<std::byte> buffer;
+ buffer.reserve(sizeof sh + sizeof sl);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&sh),
+ reinterpret_cast<const std::byte*>(&sh) + sizeof sh);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&sl),
+ reinterpret_cast<const std::byte*>(&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<std::uint16_t>(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<std::uint16_t>(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<std::byte> buffer;
+ buffer.reserve(sizeof sh + sizeof sr);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&sh),
+ reinterpret_cast<const std::byte*>(&sh) + sizeof sh);
+ buffer.insert(buffer.end(),
+ reinterpret_cast<const std::byte*>(&sr),
+ reinterpret_cast<const std::byte*>(&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<std::uint16_t>(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<std::uint16_t>(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<std::uint32_t, std::string>& Client::history() const {
+ return history_;
+}
--- /dev/null
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <string>
+
+#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<std::uint32_t, std::string>& history() const;
+
+private:
+ Logger& logger_;
+
+ Socket socket_;
+ std::map<std::uint32_t, std::string> history_;
+};
--- /dev/null
+|invalid
+invalid|invalid
+123|This is some news.
+456|
+789|Last news.
--- /dev/null
+123|This is some news.
+456|More news.
+789|Last news.
--- /dev/null
+#include <gtest/gtest.h>
+
+#include "Client.hpp"
+
+TEST(Client, Client) {
+ Logger logger;
+ Client client(logger);
+}
--- /dev/null
+#include <gtest/gtest.h>
+
+#include <chrono>
+#include <filesystem>
+
+#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));
+}
--- /dev/null
+#include <gtest/gtest.h>
+
+#include <chrono>
+#include <memory>
+#include <thread>
+#include <unordered_map>
+
+#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<std::byte> 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<std::byte> 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<std::byte> 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<std::uint32_t, std::unique_ptr<Socket>> clientSockets;
+
+ while (count > 0) {
+ std::unordered_set<std::uint32_t> pollResult;
+ ASSERT_TRUE(poller.poll(pollResult));
+
+ for (const auto& fd : pollResult) {
+ // Event from listener socket.
+ if (fd == serverSocket.fileDescriptor()) {
+ std::unique_ptr<Socket> newClientSocket = std::make_unique<Socket>(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<std::byte> 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();
+}
--- /dev/null
+11115555
+11116666