From 9a150d90ad6285d0830a49d9f80a15b2be30deeb Mon Sep 17 00:00:00 2001 From: "Wayne H. Shephard" Date: Wed, 17 Jun 2026 21:11:25 +0300 Subject: [PATCH] initial --- .clang-format | 55 ++++++++ .idea/.gitignore | 10 ++ .idea/.name | 1 + .idea/editor.xml | 252 ++++++++++++++++++++++++++++++++++ .idea/iovi-network.iml | 2 + .idea/misc.xml | 7 + .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 + CMakeLists.txt | 43 ++++++ EchoServer.cpp | 138 +++++++++++++++++++ EchoServer.h | 39 ++++++ NetworkConfig.h | 52 +++++++ SimpleClient.cpp | 104 ++++++++++++++ SimpleClient.h | 25 ++++ main.cpp | 34 +++++ server/ServerSocketThread.cpp | 167 ++++++++++++++++++++++ server/ServerSocketThread.h | 49 +++++++ 17 files changed, 992 insertions(+) create mode 100644 .clang-format create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/editor.xml create mode 100644 .idea/iovi-network.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 CMakeLists.txt create mode 100644 EchoServer.cpp create mode 100644 EchoServer.h create mode 100644 NetworkConfig.h create mode 100644 SimpleClient.cpp create mode 100644 SimpleClient.h create mode 100644 main.cpp create mode 100644 server/ServerSocketThread.cpp create mode 100644 server/ServerSocketThread.h diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..60b7251 --- /dev/null +++ b/.clang-format @@ -0,0 +1,55 @@ +# Generated from CLion C/C++ Code Style settings +--- +Language: Cpp +BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignOperands: false +AlignTrailingComments: false +AlwaysBreakTemplateDeclarations: Yes +BraceWrapping: + AfterCaseLabel: true + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: true + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: true + BeforeWhile: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBraces: Custom +BreakConstructorInitializers: AfterColon +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 120 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +IncludeCategories: + - Regex: '^<.*' + Priority: 1 + - Regex: '^".*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseBlocks: true +IndentWidth: 4 +InsertNewlineAtEOF: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +PointerAlignment: Left +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +TabWidth: 4 +... diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..8c184f7 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +iovi_network \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..5862eb1 --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,252 @@ + + + + + \ No newline at end of file diff --git a/.idea/iovi-network.iml b/.idea/iovi-network.iml new file mode 100644 index 0000000..4c94235 --- /dev/null +++ b/.idea/iovi-network.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0b76fe5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2d2d5d2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6a41eab --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +### 1. CLion needs a recipe to know how to build your code. +## This file tells CLion: "Hey, make a program called iovi_network using main.cpp. +## And make sure we are allowed to use multiple threads (helpers), +## and also if we are on Windows, turn on the Windows networking plug." + +# EchoProject/ +# ├── CMakeLists.txt (The updated recipe) +# ├── NetworkConfig.h (The universal translator rules) +# ├── EchoServer.h (The Server Menu) +# ├── EchoServer.cpp (The Server Kitchen) +# ├── EchoClient.h (The Client Menu) +# ├── EchoClient.cpp (The Client Kitchen) +# └── main.cpp (The boss who tells everyone what to do) + +cmake_minimum_required(VERSION 3.20) +project(iovi_network) + +# Tell C++ to use modern features (C++17) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find the threading library (needed for our "async" cloning magic) +find_package(Threads REQUIRED) + +# Create our single program from the list of files +add_executable(iovi_network + main.cpp + NetworkConfig.h + EchoServer.cpp + EchoServer.h + SimpleClient.cpp + SimpleClient.h + server/ServerSocketThread.cpp + server/ServerSocketThread.h +) + +# Link the threading library +target_link_libraries(iovi_network PRIVATE Threads::Threads) + +# If we are on Windows, we need to link the Windows Socket library (ws2_32) +if(WIN32) + target_link_libraries(iovi_network PRIVATE ws2_32) +endif() diff --git a/EchoServer.cpp b/EchoServer.cpp new file mode 100644 index 0000000..f5c7705 --- /dev/null +++ b/EchoServer.cpp @@ -0,0 +1,138 @@ +#include "EchoServer.h" +#include +#include + +// THE CONSTRUCTOR (Birth) +// We just initialize our variables. We haven't built any network connections yet. +EchoServer::EchoServer(const int p) : serverSocket(INVALID_SOCKET), port(p) {} + +// THE DESTRUCTOR (Cleanup) +// When the Server object is destroyed, we must hang up the phone. +EchoServer::~EchoServer() { + if (serverSocket != INVALID_SOCKET) { + CLOSE_SOCKET(serverSocket); + } +} + +// THE START METHOD (The Main Algorithm) +[[noreturn]] void EchoServer::start() { + + // ========================================================================= + // STEP 1: CREATE THE SOCKET (Building the physical telephone) + // ========================================================================= + // AF_INET = We are using IPv4 addresses. + // SOCK_STREAM = We want a reliable, two-way stream (TCP), not just random UDP packets. + // 0 = Let the OS pick the default protocol (which is TCP for SOCK_STREAM). + // Success: We now have a "telephone", but it's not plugged in yet. + serverSocket = socket(AF_INET, SOCK_STREAM, 0); + if (serverSocket == INVALID_SOCKET) { + throw std::runtime_error("Failed to create the socket!"); + } + + // Allow the port to be reused immediately if we restart the program. + // Without this, if you crash the server and restart it, the OS will say + // "Port 8080 is still in use!" for about 30 seconds. This line prevents that. + constexpr int opt = 1; + setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)); + + // ========================================================================= + // STEP 2: BIND (Tuning the telephone to a specific channel/number) + // ========================================================================= + // Right now, our telephone has no phone number. We need to assign it an + // IP address and a Port number so clients know where to call us. + sockaddr_in serverAddr{}; + serverAddr.sin_family = AF_INET; // IPv4 + serverAddr.sin_addr.s_addr = INADDR_ANY; // Listen on ALL Wi-Fi/Ethernet cards on this PC + serverAddr.sin_port = htons(port); // Set the port (e.g., 8080). + // htons = "Host to Network Short" (converts computer math to network math) + + // Bind the physical socket to this specific address and port. + if (bind(serverSocket, reinterpret_cast(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR) { + throw std::runtime_error("Failed to bind to port " + std::to_string(port) + "!"); + } + + // ========================================================================= + // STEP 3: LISTEN (Turning on the ringer) + // ========================================================================= + // The phone is built and has a number. Now we tell the Operating System: + // "Please start listening for incoming calls on this line." + // SOMAXCONN tells the OS to make the "waiting line" for callers as big as possible. + if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) { + throw std::runtime_error("Failed to listen!"); + } + + std::cout << "🟢 Server is listening on port " << port << "...\n"; + + // ========================================================================= + // STEP 4: THE INFINITE LOOP (Waiting for calls forever) + // ========================================================================= + while (true) { + sockaddr_in clientAddr{}; + socklen_t clientLen = sizeof(clientAddr); + + // ===================================================================== + // STEP 5: ACCEPT (Picking up the ringing phone) + // ===================================================================== + // This function BLOCKS the code until someone actually calls. + // MAGIC TRICK: When a client connects, the OS creates a BRAND NEW SOCKET + // (clientSocket) just for talking to them. + // Our original 'serverSocket' stays free to go back to the top of the loop + // and wait for the NEXT caller. This is how we handle multiple people! + if (SOCKET_TYPE clientSocket = accept(serverSocket, reinterpret_cast(&clientAddr), &clientLen); + clientSocket != INVALID_SOCKET) { + + std::cout << "📞 New client connected!\n"; + + // Spawn a new thread (a clone of ourselves) to talk to this specific client. + // Because it's a new thread, the main loop instantly goes back to accept() + // the next caller. This is the "Async" magic! + std::thread([clientSocket]() { + handleClient(clientSocket); + }).detach(); + } + } +} + +// 4. THE HELPER METHOD (The actual conversation with one client) +void EchoServer::handleClient(const SOCKET_TYPE clientSocket) { + char buffer[1025]; // A box to hold the incoming words + + while (true) { + // ===================================================================== + // STEP 6: RECV (Listening to the microphone) + // ===================================================================== + // We wait for the client to speak. + // We read up to 1024 bytes (sizeof(buffer) - 1) into our 'buffer' box. + // We leave 1 byte empty at the end so we can add the '\0' stop-sign later. + const auto bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0); + + // If bytesReceived is 0, the client hung up politely. + // If it's < 0, the connection broke or errored out. + if (bytesReceived <= 0) { + std::cout << "📴 Client disconnected.\n"; + break; // Exit the while loop to close the socket. + } + + // Convert the byte count to a safe size for array indexing + const auto len = static_cast(bytesReceived); + + // Add the invisible C-string stop-sign so we can print it safely + buffer[len] = '\0'; + std::cout << "📩 Received: " << buffer; + + // ===================================================================== + // STEP 7: SEND (Speaking into the microphone) + // ===================================================================== + // We take the exact same words we just heard, and send them back! + // This is why it's called an "Echo" server. + send(clientSocket, buffer, static_cast(len), 0); + } + + // ===================================================================== + // STEP 8: CLOSE (Hanging up the phone) + // ===================================================================== + // The client disconnected, so we are done talking to them. + // We close their specific 'clientSocket'. + // The main 'serverSocket' is still open and listening for others! + CLOSE_SOCKET(clientSocket); +} \ No newline at end of file diff --git a/EchoServer.h b/EchoServer.h new file mode 100644 index 0000000..cbe45dc --- /dev/null +++ b/EchoServer.h @@ -0,0 +1,39 @@ +/** + * This is the Server's menu. + * It says: "I have a socket, a port, I can be created, I can be destroyed, I can start, and + * I have a helper to talk to clients." This short resume doesn't say how Server does any of that. + */ + +#ifndef IOVI_NETWORK_ECHO_SERVER_H +#define IOVI_NETWORK_ECHO_SERVER_H + +#include "NetworkConfig.h" // We need the rulebook +#include +#include + +class EchoServer { + SOCKET_TYPE serverSocket; // The main walkie-talkie + int port; // The channel number + + // this helper function belongs to the class itself, + // not to a specific server object. Threads love static + static void handleClient(SOCKET_TYPE clientSocket); + +public: + // Constructor: Runs when the server is born + explicit EchoServer(int p); + + // Destructor: Runs when the server is destroyed (cleans up) + ~EchoServer(); + + // The main action: Starts listening + // It has a while(true) loop. There is no break; and no return; inside that loop. + // Normally, a function is like a hallway: you walk in, do some work, and walk out the other end + // (which is called "returning"). But start() function has a brick wall at the end of the hallway. + // It will loop forever. CLion warns "Wait, this function never finishes! Is that a mistake?" + // We add [[noreturn]]. This is a "Warning Label" you stick on the function. It tells the compiler: + // "I know this never finishes. That is on purpose. Stop worrying." + [[noreturn]] void start(); +}; + +#endif // IOVI_NETWORK_ECHO_SERVER_H diff --git a/NetworkConfig.h b/NetworkConfig.h new file mode 100644 index 0000000..76f08cd --- /dev/null +++ b/NetworkConfig.h @@ -0,0 +1,52 @@ +/** + * Think of this as the "Rulebook". + * Instead of teaching Windows and Mac rules to the Server and the Client separately, we put the rules in one book. + * Anyone who needs to know the rules just reads this book (#include "NetworkConfig.h"). + */ + +// This is a "Do Not Duplicate" sticker. +// It ensures this file is only read once, even if multiple files include it. +// Can use a '#pragma once' but I prefer this old-style one. +#ifndef IOVI_NETWORK_NETWORK_CONFIG_H +#define IOVI_NETWORK_NETWORK_CONFIG_H + +/** + * THE TRANSLATOR (Cross-Platform Setup) + * Windows and Mac/Linux speak different "socket" languages. + * This block makes sure our code understands both + */ +#ifdef _WIN32 + #include + #include + #define CLOSE_SOCKET closesocket + #define SOCKET_TYPE SOCKET +#else + #include + #include + #include + #include + #define CLOSE_SOCKET close + #define SOCKET_TYPE int + #define INVALID_SOCKET (-1) + #define SOCKET_ERROR (-1) +#endif + +/** + * A helper to automatically turn on networking when the program starts. + * For non-Windows systems does nothing. + */ +struct NetworkStarter { + NetworkStarter() { +#ifdef _WIN32 + WSADATA wsaData; + WSAStartup(MAKEWORD(2, 2), &wsaData); +#endif + } + ~NetworkStarter() { +#ifdef _WIN32 + WSACleanup(); +#endif + } +}; + +#endif // IOVI_NETWORK_NETWORK_CONFIG_H diff --git a/SimpleClient.cpp b/SimpleClient.cpp new file mode 100644 index 0000000..d2d30cc --- /dev/null +++ b/SimpleClient.cpp @@ -0,0 +1,104 @@ +#include "SimpleClient.h" +#include +#include + +// 1. THE CONSTRUCTOR (Birth) +// We initialize the socket to INVALID. We haven't bought a telephone yet. +SimpleClient::SimpleClient() : clientSocket(INVALID_SOCKET) {} + +// 2. THE DESTRUCTOR (Cleanup) +// When the Client object is destroyed, we must hang up the phone. +SimpleClient::~SimpleClient() { + if (clientSocket != INVALID_SOCKET) { + CLOSE_SOCKET(clientSocket); + } +} + +// 3. THE CONNECT METHOD (The Dialing Algorithm) +bool SimpleClient::connectTo(const std::string& ip, int port) { + + // ========================================================================= + // STEP 1: CREATE THE SOCKET (Buying the physical telephone) + // ========================================================================= + // Just like the server, we need a physical socket to communicate. + // AF_INET = IPv4, SOCK_STREAM = Reliable TCP connection. + clientSocket = socket(AF_INET, SOCK_STREAM, 0); + if (clientSocket == INVALID_SOCKET) { + std::cerr << "❌ Failed to create client socket!\n"; + return false; + } + + // ========================================================================= + // STEP 2: SETUP THE ADDRESS (Looking up the phone number) + // ========================================================================= + // We need to tell the OS *who* we want to call. + sockaddr_in serverAddr{}; + serverAddr.sin_family = AF_INET; // IPv4 + serverAddr.sin_port = htons(port); // The port number (e.g., 8080) + + // inet_pton = "Internet Presentation to Network" + // It converts the human-readable text "127.0.0.1" into the 32-bit binary + // number that the computer's network card actually understands. + if (inet_pton(AF_INET, ip.c_str(), &serverAddr.sin_addr) <= 0) { + std::cerr << "❌ Invalid IP address format!\n"; + return false; + } + + // ========================================================================= + // STEP 3: CONNECT (Dialing the number and waiting for an answer) + // ========================================================================= + // This function BLOCKS (pauses) the program here until the server picks up. + // If the server isn't running, or the port is wrong, this will fail. + if (connect(clientSocket, reinterpret_cast(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR) { + std::cerr << "❌ Failed to connect to server at " << ip << ":" << port << "\n"; + return false; // We couldn't get through, so we return false. + } + + // If we reach here, the connection is successful! The line is open. + return true; +} + +// 4. THE TALK METHOD (The Conversation Algorithm) +void SimpleClient::talk(const std::string& message) const { + // Safety check: Don't try to talk if we aren't connected! + if (clientSocket == INVALID_SOCKET) { + std::cerr << "⚠️ Cannot talk: Not connected to a server.\n"; + return; + } + + std::cout << "📤 Sending: " << message; + + // ========================================================================= + // STEP 4: SEND (Speaking into the microphone) + // ========================================================================= + // We push our text message out through the open connection. + // We cast the length to 'int' to satisfy Windows, and it's perfectly safe + // because our messages are small. + send(clientSocket, message.c_str(), static_cast(message.length()), 0); + + // ========================================================================= + // STEP 5: RECV (Listening for the echo) + // ========================================================================= + char buffer[1025]; // Our listening box (1024 data + 1 for '\0') + + // We BLOCK (wait) here until the server sends data back to us. + // We read up to 1024 bytes, leaving 1 byte for the stop-sign. + auto bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0); + + if (bytesReceived > 0) { + // Convert to safe size_t for array indexing + const auto len = static_cast(bytesReceived); + + // Add the invisible C-string stop-sign + buffer[len] = '\0'; + + std::cout << "📥 Server echoed back: " << buffer << "\n"; + } else { + // If bytesReceived is 0, the server hung up on us politely. + // If < 0, the connection broke. + std::cerr << "⚠️ Server disconnected or error occurred while waiting for echo.\n"; + } +} +// Note: We do NOT call CLOSE_SOCKET() here. +// We want to keep the line open so the user can call talk() multiple times! +// The destructor (~EchoClient) will handle the final hang-up. \ No newline at end of file diff --git a/SimpleClient.h b/SimpleClient.h new file mode 100644 index 0000000..60fb5f1 --- /dev/null +++ b/SimpleClient.h @@ -0,0 +1,25 @@ +// +// Created by Иван Игоревич Овчинников on 08.06.2026. +// + +#ifndef IOVI_NETWORK_SIMPLE_CLIENT_H +#define IOVI_NETWORK_SIMPLE_CLIENT_H + +#include "NetworkConfig.h" +#include + +class SimpleClient { + SOCKET_TYPE clientSocket; // The client's walkie-talkie + +public: + SimpleClient(); + ~SimpleClient(); + + // Returns true if we successfully dialed and connected, false otherwise + bool connectTo(const std::string& ip, int port); + + // Sends a message and waits for the server to echo it back + void talk(const std::string& message) const; +}; + +#endif // IOVI_NETWORK_SIMPLE_CLIENT_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..a1e5550 --- /dev/null +++ b/main.cpp @@ -0,0 +1,34 @@ +/** + * main.cpp is the manager. It hires a NetworkStarter, checks if it was told to be a "server" or a "client", + * creates that object, and tells it to do its job. It doesn't need to know the messy details of sockets + */ + +#include "NetworkConfig.h" +#include "EchoServer.h" +#include "SimpleClient.h" +#include +#include + +int main(const int argc, char* argv[]) { + // 1. Turn on the networking rulebook + NetworkStarter network; + + // 2. Check what the user typed when they ran the program + if (argc > 1 && std::string(argv[1]) == "server") { + EchoServer server(8080); + server.start(); // Boss says: "Server, start working!" + } + else if (argc > 1 && std::string(argv[1]) == "client") { + if (SimpleClient client; client.connectTo("127.0.0.1", 8080)) { + client.talk("Hello, Server! Are you there?\n"); + client.talk("This is a second message!\n"); + } + } + else { + std::cout << "How to use:\n"; + std::cout << " Run as Server: echo_app server\n"; + std::cout << " Run as Client: echo_app client\n"; + } + + return 0; +} \ No newline at end of file diff --git a/server/ServerSocketThread.cpp b/server/ServerSocketThread.cpp new file mode 100644 index 0000000..b6e7125 --- /dev/null +++ b/server/ServerSocketThread.cpp @@ -0,0 +1,167 @@ +#include "ServerSocketThread.h" +#include +#include + +// THE CONSTRUCTOR +ServerSocketThread::ServerSocketThread(const int p) : serverSocket(INVALID_SOCKET), port(p) {} + +// THE DESTRUCTOR +// If the object is destroyed, we MUST ensure the background thread is stopped first! +ServerSocketThread::~ServerSocketThread() { + stop(); +} + +// THE START (Spawning the Worker) +void ServerSocketThread::start() { + if (running.load()) { + std::cerr << "⚠️ Server is already running!\n"; + return; + } + + // ========================================================================= + // STEP 1: CREATE, BIND, LISTEN (Same as before, but in the main thread) + // ========================================================================= + serverSocket = socket(AF_INET, SOCK_STREAM, 0); + if (serverSocket == INVALID_SOCKET) { + throw std::runtime_error("Failed to create socket!"); + } + + constexpr int opt = 1; + setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)); + + sockaddr_in serverAddr{}; + serverAddr.sin_family = AF_INET; + serverAddr.sin_addr.s_addr = INADDR_ANY; + serverAddr.sin_port = htons(port); + + if (bind(serverSocket, reinterpret_cast(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR) { + throw std::runtime_error("Failed to bind to port " + std::to_string(port) + "!"); + } + + if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) { + throw std::runtime_error("Failed to listen!"); + } + + // ========================================================================= + // STEP 2: RAISE THE "GO" FLAG AND SPAWN THE THREAD + // ========================================================================= + running.store(true); + + // We pass 'this' so the background thread can access the serverLoop method + serverThread = std::thread(&ServerSocketThread::serverLoop, this); + + std::cout << "🟢 Server thread started and listening on port " << port << "...\n"; +} + +// THE STOP METHOD (The Interrupt Command) +void ServerSocketThread::stop() { + if (!running.load()) { + std::cerr << "⚠️ Server is already stopped!\n"; + return; + } + + std::cout << "\n🛑 Interrupt received. Shutting down server...\n"; + + // Lower the "Go" flag. The background loop will see this on its next check. + running.store(false); + + // FORCE WAKEUP TRICK: Closing the socket from THIS thread will immediately + // cause the 'select' or 'accept' call in the OTHER thread to fail and return. + // This guarantees we don't have to wait for the 1-second timeout to finish! + if (serverSocket != INVALID_SOCKET) { + CLOSE_SOCKET(serverSocket); + serverSocket = INVALID_SOCKET; + } + + // Wait for the background thread to finish its cleanup and join us. + if (serverThread.joinable()) { + serverThread.join(); + } + + std::cout << "✅ Server thread stopped cleanly.\n"; +} + +// THE SERVER LOOP (The Background Algorithm) +void ServerSocketThread::serverLoop() const { + std::cout << "🟢 Server loop is now running in the background.\n"; + + // We loop AS LONG AS the stop sign is NOT raised. + while (running.load()) { + + // ===================================================================== + // STEP 3: THE ALARM CLOCK (select with timeout) + // ===================================================================== + // Instead of blocking forever on accept(), we ask the OS: + // "Is there a client trying to connect? I'll wait up to 1 second." + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(serverSocket, &readfds); + + timeval tv{}; + tv.tv_sec = 1; // 1 second timeout + tv.tv_usec = 0; + + // select() returns: + // > 0 : The socket is ready to be accepted + // 0 : Timeout (1 second passed, no clients) + // < 0 : Error (or the socket was closed by our stop() method) +#ifdef _WIN32 + const int activity = select(0, &readfds, nullptr, nullptr, &tv); // Windows ignores the first param +#else + // POSIX needs max_fd + 1 + const int activity = select(static_cast(serverSocket + 1), &readfds, nullptr, nullptr, &tv); +#endif + + // CRITICAL CHECK: Did someone call stop() while we were sleeping? + if (!running.load()) { + break; // Exit the loop immediately + } + + if (activity > 0 && FD_ISSET(serverSocket, &readfds)) { + // ================================================================= + // STEP 4: ACCEPT (A client is here) + // ================================================================= + sockaddr_in clientAddr{}; + socklen_t clientLen = sizeof(clientAddr); + + if (SOCKET_TYPE clientSocket = accept(serverSocket, reinterpret_cast(&clientAddr), &clientLen); + clientSocket != INVALID_SOCKET) { + std::cout << "📞 New client connected! Spawning handler thread...\n"; + + // Spawn a detached thread to handle this specific client + std::thread([clientSocket]() { + handleClient(clientSocket); + }).detach(); + } + } + else if (activity < 0) { + // This usually happens when stop() closes the socket, forcing select to return an error. + // We just break the loop to shut down gracefully. + std::cout << "Usually happens when stop() closes the socket\n"; + break; + } + // If activity == 0, it's just a timeout. The while loop repeats, + // checks isRunning again, and sets the alarm clock for another second. + } +} + +// 6. THE HELPER METHOD (Unchanged, perfectly safe echo logic) +void ServerSocketThread::handleClient(SOCKET_TYPE clientSocket) { + char buffer[1025]; + + while (true) { + auto bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0); + + if (bytesReceived <= 0) { + std::cout << "📴 Client disconnected.\n"; + break; + } + + const auto len = static_cast(bytesReceived); + buffer[len] = '\0'; + std::cout << "📩 Received: " << buffer; + + send(clientSocket, buffer, static_cast(len), 0); + } + CLOSE_SOCKET(clientSocket); +} \ No newline at end of file diff --git a/server/ServerSocketThread.h b/server/ServerSocketThread.h new file mode 100644 index 0000000..3e36a90 --- /dev/null +++ b/server/ServerSocketThread.h @@ -0,0 +1,49 @@ +/** + * To solve the problem of accept() blocking forever, we are going to use a classic networking trick: + * The Alarm Clock (select). Instead of letting accept() sleep forever, we set an alarm clock for 1 second. + * If a client calls before the alarm rings, we answer the phone (accept). If the alarm rings first, we wake up, + * check our "Stop Sign" (the interrupt flag), and if it's raised, we gracefully shut down. + */ + +#ifndef IOVI_NETWORK_SERVER_SOCKET_THREAD_H +#define IOVI_NETWORK_SERVER_SOCKET_THREAD_H + +#include "../NetworkConfig.h" +#include +#include +#include // For thread-safe "Stop Signs" + +class ServerSocketThread { + SOCKET_TYPE serverSocket; + int port; + + // THE STOP SIGN: std::atomic ensures that when one thread changes this + // to 'false', the other thread sees the change instantly, without caching issues. + std::atomic running{false}; + + // THE WORKER: The background thread that will run our server loop. + std::thread serverThread; + + // The actual infinite loop that runs inside the background thread, main server logic + void serverLoop() const; + + // The echo logic (same as before, but now static so the thread can call it) + static void handleClient(SOCKET_TYPE clientSocket); + +public: + explicit ServerSocketThread(int p); + ~ServerSocketThread(); + + // Starts the background thread + void start(); + + // Raises the stop sign and waits for the thread to finish cleanly + void stop(); + + // Checks if the server is currently running + bool isRunning() const { + return running.load(); + } +}; + +#endif // IOVI_NETWORK_SERVER_SOCKET_THREAD_H