commit 9a150d90ad6285d0830a49d9f80a15b2be30deeb Author: Wayne H. Shephard Date: Wed Jun 17 21:11:25 2026 +0300 initial 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