From 4c10b2af00635d9009d2020773e491da210c1091 Mon Sep 17 00:00:00 2001 From: Aleksandr Kovalko Date: Sat, 2 May 2026 02:58:45 +0200 Subject: [PATCH] Prepare v1.0.0: fix build, harden inputs, add e2e + release workflow --- .github/workflows/release.yml | 84 ++++++++++++ .github/workflows/tests.yml | 18 ++- .gitignore | 30 ++++- Makefile | 41 ++++-- README.md | 171 ++++++++++++++++++------ lib/cxxopts | 2 +- src/Config.cpp | 6 +- src/Config.hpp | 2 + src/Main.cpp | 243 ++++++++++++++++++++++------------ src/Receiver.cpp | 126 ++++++++++++++---- src/Sender.cpp | 174 +++++++++++++++--------- src/Utils.hpp | 60 +++------ tests/e2e.sh | 100 ++++++++++++++ 13 files changed, 782 insertions(+), 275 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100755 tests/e2e.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b4040cc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,84 @@ +name: Release + +on: + push: + tags: ['v*'] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + artifact_name: FileBroadcaster + asset_name: FileBroadcaster-linux-x86_64 + - os: macos-latest + artifact_name: FileBroadcaster + asset_name: FileBroadcaster-macos-arm64 + - os: windows-latest + artifact_name: FileBroadcaster.exe + asset_name: FileBroadcaster-windows-x86_64.exe + + defaults: + run: + shell: ${{ matrix.os == 'windows-latest' && 'msys2 {0}' || 'bash' }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up MSYS2 (Windows) + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: mingw-w64-x86_64-gcc make + + - name: Build (release) + env: + VERSION: ${{ github.ref_name }} + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + make program \ + CXXFLAGS="-std=c++17 -O2 -Wall -Wextra -pthread -DFILEBROADCASTER_VERSION=\\\"${VERSION}\\\"" \ + LDLIBS_WIN="-lws2_32 -static -static-libgcc -static-libstdc++" + mv FileBroadcaster FileBroadcaster.exe + else + make program \ + CXXFLAGS="-std=c++17 -O2 -Wall -Wextra -pthread -DFILEBROADCASTER_VERSION=\"\\\"${VERSION}\\\"\"" + fi + + - name: Rename artifact + run: mv ${{ matrix.artifact_name }} ${{ matrix.asset_name }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.asset_name }} + path: ${{ matrix.asset_name }} + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: artifacts/* + generate_release_notes: true + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 04d3f6c..6e4a32d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,9 +7,10 @@ on: branches: [master, develop, tests] jobs: - gtest: + build: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] @@ -39,6 +40,14 @@ jobs: if: runner.os == 'macOS' run: brew install googletest + - name: Build program + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + make program LDLIBS_WIN="-lws2_32" + else + make program + fi + - name: Build tests run: | if [ "$RUNNER_OS" == "macOS" ]; then @@ -50,3 +59,10 @@ jobs: - name: Run tests run: ./GTests --gtest_filter=* + + - name: Run E2E loopback tests + # Skip on Windows: SO_REUSEADDR semantics on Winsock cause one socket + # to silently capture all loopback traffic, which breaks two-process + # localhost tests. The protocol works fine across separate hosts. + if: runner.os != 'Windows' + run: make e2e diff --git a/.gitignore b/.gitignore index 91ed369..612d39d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,10 +31,36 @@ *.out *.app -#Visual studio dirs +# Project binaries +/FileBroadcaster +/GTests + +# Debug symbols +*.dSYM/ +*.pdb + +# Build directories +build/ +out/ + +# Visual Studio .vs/ x64/ x86/ Debug/ Release/ -packages/ \ No newline at end of file +packages/ + +# IDE / editor metadata +.idea/ +.vscode/ +*.swp +*.swo + +# Tooling +compile_commands.json +.cache/ + +# OS noise +.DS_Store +Thumbs.db diff --git a/Makefile b/Makefile index ba1e835..efb20dd 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,34 @@ -program: - g++ src/Main.cpp src/Receiver.cpp src/Sender.cpp src/Config.cpp \ - -std=c++14 -pthread \ - -Ilib/cxxopts/include \ - -o FileBroadcaster +CXX ?= g++ +CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra -pthread +INCLUDES = -Ilib/cxxopts/include +LDLIBS_WIN ?= + +SRCS = src/Main.cpp src/Receiver.cpp src/Sender.cpp src/Config.cpp +TARGET = FileBroadcaster GTEST_CFLAGS ?= GTEST_LDFLAGS ?= +.PHONY: all program gtests e2e clean + +all: program + +program: $(TARGET) + +$(TARGET): $(SRCS) + $(CXX) $(SRCS) $(CXXFLAGS) $(INCLUDES) -o $(TARGET) $(LDLIBS_WIN) + gtests: - g++ tests/Tests.cpp \ - -std=c++17 -pthread \ - -Ilib/cxxopts/include \ - $(GTEST_CFLAGS) \ - -lgtest \ - $(GTEST_LDFLAGS) \ - -o GTests + $(CXX) tests/Tests.cpp \ + $(CXXFLAGS) \ + $(INCLUDES) \ + $(GTEST_CFLAGS) \ + -lgtest \ + $(GTEST_LDFLAGS) \ + -o GTests + +e2e: program + BINARY=./$(TARGET) bash tests/e2e.sh + +clean: + rm -f $(TARGET) $(TARGET).exe GTests GTests.exe diff --git a/README.md b/README.md index 314f99d..6b1bc73 100644 --- a/README.md +++ b/README.md @@ -13,86 +13,175 @@ License

-UDP broadcast file transfer — sends a file to all computers in the same LAN simultaneously, with automatic retransmission of lost packets. +UDP broadcast file transfer — sends a single file to every host on the same +LAN at once, with automatic retransmission of dropped packets. ## Table of Contents - [Features](#features) - [Quick Start](#quick-start) -- [Requirements](#requirements) - [Installation](#installation) - [Parameters](#parameters) +- [Examples](#examples) - [How It Works](#how-it-works) - [Packet Structure](#packet-structure) +- [Limitations](#limitations) +- [Building from Source](#building-from-source) +- [License](#license) ## Features -- Broadcast to all computers in LAN with a single send +- Broadcast to every host on a LAN with a single transmission - Unicast mode for point-to-point transfer - Automatic retransmission of lost packets -- Configurable MTU and TTL -- Windows, Linux, and macOS support +- Configurable MTU and timeout +- Windows, Linux, and macOS binaries built on every release ## Quick Start -**Sender** (machine that has the file): -``` +Download the binary for your platform from the +[releases page](https://github.com/gistrec/File-Broadcaster/releases) and run +it directly. No installation required. + +**Sender** (host that has the file): + +```sh ./FileBroadcaster --type sender --file photo.jpg ``` -**Receiver** (one or more machines in the same LAN): -``` +**Receiver** (one or more hosts on the same LAN): + +```sh ./FileBroadcaster --type receiver --file photo.jpg ``` -To send to a specific IP instead of broadcasting to the whole LAN: -``` +Send to a specific host instead of broadcasting to the whole LAN: + +```sh ./FileBroadcaster --type sender --file photo.jpg --broadcast 192.168.1.50 ``` -## Requirements +## Installation -**Windows:** -- Visual Studio 2019 or later +Pre-built binaries for Linux x86_64, macOS arm64, and Windows x86_64 are +attached to every [GitHub Release](https://github.com/gistrec/File-Broadcaster/releases). -**Linux / macOS:** -- g++ or clang++ with C++14 support -- pthreads +If your platform isn't covered, see [Building from Source](#building-from-source). -## Installation +## Parameters + +| Parameter | Default | Range | Description | +| --------- | ------- | ----- | ----------- | +| `-f, --file` | `file.out` | — | File to send or save | +| `-t, --type` | `sender` | `sender` / `receiver` | Run mode | +| `--broadcast` | `yes` | `yes` or IPv4 | `yes` for LAN broadcast, or a specific IP for unicast | +| `-p, --port` | `33333` | 1..65535 | Destination port for outgoing packets | +| `--bind-port` | `33333` | 1..65535 | Local port to bind on | +| `--mtu` | `1500` | 64..65507 | Max packet size in bytes | +| `--ttl` | `15` | > 0 | Seconds of silence before giving up | +| `-h, --help` | — | — | Print help | +| `--version` | — | — | Print version | -**Linux / macOS:** +## Examples + +**LAN broadcast** (one sender, many receivers): + +```sh +# On the sender host +./FileBroadcaster --type sender --file album.zip + +# On every receiver host +./FileBroadcaster --type receiver --file album.zip ``` -git clone https://github.com/gistrec/File-Broadcaster.git -git submodule update --init --recursive -make program + +**Targeted unicast** (when broadcast is blocked or you only have one receiver): + +```sh +# On the sender host (sends data to 10.0.0.42) +./FileBroadcaster --type sender --file album.zip --broadcast 10.0.0.42 + +# On 10.0.0.42 (receiver default --broadcast=yes broadcasts RESENDs) +./FileBroadcaster --type receiver --file album.zip ``` -**Windows:** -1. Clone the repository and run `git submodule update --init --recursive` -2. Open `FileBroadcaster.sln` in Visual Studio -3. Build the project +**Loopback test** (sender and receiver on the same host — useful for +development): -## Parameters +```sh +# Receiver listens on 33401, sends RESEND back to the sender's bind port (33402) +./FileBroadcaster --type receiver --file out.bin \ + --broadcast 127.0.0.1 --port 33402 --bind-port 33401 & -| Parameter | Default | Description | -| --------- | ------- | ----------- | -| `-p, --port` | `33333` | Port for sender and receiver | -| `-f, --file` | `file.out` | File to send or save | -| `-t, --type` | `sender` | `sender` or `receiver` | -| `--ttl` | `15` | Seconds to wait before timing out | -| `--mtu` | `1500` | Max packet size in bytes | -| `--broadcast` | `yes` | `yes` for LAN broadcast, or a specific IP for unicast | +# Sender listens on 33402, sends data to the receiver's bind port (33401) +./FileBroadcaster --type sender --file in.bin \ + --broadcast 127.0.0.1 --port 33401 --bind-port 33402 +``` ## How It Works -1. Sender broadcasts a `NEW_PACKET` with the total file size -2. Sender splits the file into chunks and broadcasts each one as a `TRANSFER` packet -3. Sender broadcasts a `FINISH` packet when all chunks are sent -4. Receiver checks for missing chunks and requests them with `RESEND` packets -5. Sender retransmits each requested chunk -6. Steps 4–5 repeat until all chunks are received or TTL expires +1. Sender broadcasts a `NEW_PACKET` packet with the total file size. +2. Each receiver allocates a buffer of that size and clears its part registry. +3. Sender splits the file into MTU-sized chunks and broadcasts each one as a + `TRANSFER` packet. +4. Sender broadcasts a `FINISH` packet when all chunks have been sent. +5. Each receiver scans for missing chunks and requests them with `RESEND` + packets. +6. Sender retransmits each requested chunk. +7. Steps 5–6 repeat until every chunk is received or the TTL expires. ## Packet Structure ![Packet structure](https://www.gistrec.ru/wp-content/uploads/2019/01/Packets.png) + +## Limitations + +- The whole file is held in RAM on both sides. The receiver enforces a 4 GiB + cap on the announced file size; the sender is bounded only by available + memory. +- No data integrity check beyond UDP's optional 16-bit checksum. If the + payload is corrupted in a way the checksum doesn't catch, the receiver will + silently produce a corrupted file. +- No authentication. Any host on the same LAN can send a `NEW_PACKET` and any + receiver bound to the chosen port will accept it. +- No encryption. The payload travels as plaintext UDP. + +## Building from Source + +### Requirements + +- **Linux / macOS:** GCC 7+ or Clang 5+ with C++17 support, GNU Make, pthreads. +- **Windows (MinGW64):** [MSYS2](https://www.msys2.org/) with + `mingw-w64-x86_64-gcc` and `make`. +- **Windows (Visual Studio):** Visual Studio 2017 or later with the v141 + platform toolset. + +### Linux / macOS / MSYS2 + +```sh +git clone https://github.com/gistrec/File-Broadcaster.git +cd File-Broadcaster +git submodule update --init --recursive +make program +``` + +The binary is written to `./FileBroadcaster`. + +### Windows (Visual Studio) + +1. Clone the repository. +2. Run `git submodule update --init --recursive`. +3. Open `FileBroadcaster.sln` in Visual Studio. +4. Build the solution. + +### Tests + +```sh +make gtests # unit tests (requires Google Test) +./GTests + +make e2e # loopback end-to-end test (small + large file) +``` + +## License + +[MIT](LICENSE). diff --git a/lib/cxxopts b/lib/cxxopts index 3d405ef..44380e5 160000 --- a/lib/cxxopts +++ b/lib/cxxopts @@ -1 +1 @@ -Subproject commit 3d405ef1639a918ea8798666e2b02eb9cef889c0 +Subproject commit 44380e5a44706ab7347f400698c703eb2a196202 diff --git a/src/Config.cpp b/src/Config.cpp index cb3e973..b8f47a1 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -8,9 +8,9 @@ int ttl_max = 0; SOCKET _socket; -SOCKADDR_IN server_address = { 0 }; -SOCKADDR_IN client_address = { 0 }; -SOCKADDR_IN broadcast_address = { 0 }; +SOCKADDR_IN server_address = {}; +SOCKADDR_IN client_address = {}; +SOCKADDR_IN broadcast_address = {}; addr_len server_address_length = sizeof(server_address); addr_len client_address_length = sizeof(client_address); diff --git a/src/Config.hpp b/src/Config.hpp index 047f449..06273ed 100644 --- a/src/Config.hpp +++ b/src/Config.hpp @@ -3,6 +3,8 @@ #include "Utils.hpp" +#include + extern std::string fileName; // File Name to transfer or receive diff --git a/src/Main.cpp b/src/Main.cpp index 5ddb563..dd60f69 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -1,10 +1,27 @@ #include "cxxopts.hpp" #include "Config.hpp" +#include +#include +#include + +#ifndef FILEBROADCASTER_VERSION +#define FILEBROADCASTER_VERSION "1.0.0" +#endif + namespace Receiver { void run(); } namespace Sender { void run(); } +static void cleanupAndExit(int code) { + if (_socket != INVALID_SOCKET) { + CLOSE_SOCKET(_socket); + } + CLEANUP_NETWORK(); + std::exit(code); +} + + int main(int argc, char* argv[]) { // Parsing input parameters from the CLI cxxopts::Options options("File-Broadcaster", "UDP Broadcast file transfer"); @@ -14,96 +31,152 @@ int main(int argc, char* argv[]) { .show_positional_help(); options.add_options() - ("f,file", "File name", cxxopts::value()->default_value("file.out")) - ("t,type", "Receiver or sender", cxxopts::value()->default_value("sender")) - ("broadcast", "Broadcast address", cxxopts::value()->default_value("yes")) - ("p,port", "Port", cxxopts::value()->default_value("33333")) - ("mtu", "MTU packet", cxxopts::value()->default_value("1500")) - ("ttl", "Time to live", cxxopts::value()->default_value("15")); - - auto result = options.parse(argc, argv); - - #if defined(_WIN32) || defined(_WIN64) // - WORD socketVer; // Initializing the use - WSADATA wsaData; // of the Winsock DLL - socketVer = MAKEWORD(2, 2); // by this process. - if (WSAStartup(socketVer, &wsaData) != 0) { // - std::cerr << "Error: WSAStartup failed" << std::endl; // - exit(1); // - } // - #endif // - - mtu = result["mtu"].as(); // - ttl = result["ttl"].as(); // Initializing some variable - ttl_max = result["ttl"].as(); // - fileName = result["file"].as(); // - - - _socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); // - if (_socket == INVALID_SOCKET) { // Create socket with - std::cout << "Error: Can't create socket" << std::endl; // datagram-based protocol - exit(1); // - } else { // - std::cout << "Ok: Socket created" << std::endl; // - } // - - client_address.sin_family = AF_INET; // - client_address.sin_port = htons(result["port"].as()); // Creating local address - client_address.sin_addr.s_addr = INADDR_ANY; // - - memcpy(&server_address, &client_address, sizeof(server_address)); // Server address == Client address - - broadcast_address.sin_family = AF_INET; // Creating broadcast - broadcast_address.sin_port = htons(result["port"].as()); // address - - if (result["broadcast"].as() == "yes") { // - #if defined(_WIN32) || defined(_WIN64) // Getting access to - char broadcastEnable = 1; // the broadcast address - #else // - int broadcastEnable = 1; // - #endif // - // - if (setsockopt(_socket, SOL_SOCKET, SO_BROADCAST, // - &broadcastEnable, sizeof(broadcastEnable)) == 0) { // - std::cout << "Ok: Got access to broadcast" << std::endl; // - } else { // - std::cerr << "Error: Can't get access to broadcast" << std::endl; // - CLOSE_SOCKET(_socket); // - exit(1); // - } // If parameter "broadcast" is "yes", then - broadcast_address.sin_addr.s_addr = INADDR_BROADCAST; // change server address - } else { // to broadcast - broadcast_address.sin_addr.s_addr = // Else change server address - inet_addr(result["broadcast"].as().c_str()); // to address in parameter - } // - - if (bind(_socket, reinterpret_cast(&client_address), sizeof(client_address)) == 0) {// - std::cout << "Ok: Socket binded" << std::endl; // - } else { // Bind socket to - std::cerr << "Error: Can't bind socket" << std::endl; // client address - CLOSE_SOCKET(_socket); // - exit(1); // - } // - - #if defined(_WIN32) || defined(_WIN64) // - int tv = 1000; // user timeout in milliseconds [ms] // - setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); // Set socket - #else // receive timeout - struct timeval tv; // to 1 sec - tv.tv_sec = 1; // - tv.tv_usec = 0; // - setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); // + ("f,file", "File name", cxxopts::value()->default_value("file.out")) + ("t,type", "Receiver or sender", cxxopts::value()->default_value("sender")) + ("broadcast", "Broadcast address", cxxopts::value()->default_value("yes")) + ("p,port", "Destination port for outgoing packets", cxxopts::value()->default_value("33333")) + ("bind-port", "Local port to bind on", cxxopts::value()->default_value("33333")) + ("mtu", "MTU packet", cxxopts::value()->default_value("1500")) + ("ttl", "Time to live", cxxopts::value()->default_value("15")) + ("h,help", "Print help") + ("version", "Print version"); + + auto result = [&]() -> cxxopts::ParseResult { + try { + return options.parse(argc, argv); + } catch (const cxxopts::exceptions::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + std::exit(1); + } + }(); + + if (result.count("help")) { + std::cout << options.help() << std::endl; + return 0; + } + if (result.count("version")) { + std::cout << "File-Broadcaster " << FILEBROADCASTER_VERSION << std::endl; + return 0; + } + + // Validate CLI parameters before touching sockets so we can fail fast + int parsed_mtu = result["mtu"].as(); + int parsed_ttl = result["ttl"].as(); + int parsed_port = result["port"].as(); + int parsed_bind_port = result["bind-port"].as(); + + if (parsed_mtu < 64 || parsed_mtu > 65507) { + std::cerr << "Error: --mtu must be between 64 and 65507" << std::endl; + return 1; + } + if (parsed_ttl <= 0) { + std::cerr << "Error: --ttl must be greater than 0" << std::endl; + return 1; + } + if (parsed_port <= 0 || parsed_port > 65535) { + std::cerr << "Error: --port must be between 1 and 65535" << std::endl; + return 1; + } + if (parsed_bind_port <= 0 || parsed_bind_port > 65535) { + std::cerr << "Error: --bind-port must be between 1 and 65535" << std::endl; + return 1; + } + + const std::string type = result["type"].as(); + if (type != "sender" && type != "receiver") { + std::cerr << "Error: --type must be 'sender' or 'receiver'" << std::endl; + return 1; + } + + const std::string broadcast_arg = result["broadcast"].as(); + + #if defined(_WIN32) || defined(_WIN64) + WORD socketVer = MAKEWORD(2, 2); + WSADATA wsaData; + if (WSAStartup(socketVer, &wsaData) != 0) { + std::cerr << "Error: WSAStartup failed" << std::endl; + return 1; + } + #endif + + mtu = parsed_mtu; + ttl = parsed_ttl; + ttl_max = parsed_ttl; + fileName = result["file"].as(); + + _socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (_socket == INVALID_SOCKET) { + std::cerr << "Error: Can't create socket" << std::endl; + CLEANUP_NETWORK(); + return 1; + } + std::cout << "Ok: Socket created" << std::endl; + + int reuseAddr = 1; + if (setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&reuseAddr), sizeof(reuseAddr)) != 0) { + std::cerr << "Warning: Failed to set SO_REUSEADDR" << std::endl; + } + #ifdef SO_REUSEPORT + int reusePort = 1; + if (setsockopt(_socket, SOL_SOCKET, SO_REUSEPORT, + reinterpret_cast(&reusePort), sizeof(reusePort)) != 0) { + std::cerr << "Warning: Failed to set SO_REUSEPORT" << std::endl; + } + #endif + + client_address.sin_family = AF_INET; + client_address.sin_port = htons(static_cast(parsed_bind_port)); + client_address.sin_addr.s_addr = INADDR_ANY; + + memcpy(&server_address, &client_address, sizeof(server_address)); + + broadcast_address.sin_family = AF_INET; + broadcast_address.sin_port = htons(static_cast(parsed_port)); + + if (broadcast_arg == "yes") { + int broadcastEnable = 1; + if (setsockopt(_socket, SOL_SOCKET, SO_BROADCAST, + reinterpret_cast(&broadcastEnable), + sizeof(broadcastEnable)) != 0) { + std::cerr << "Error: Can't get access to broadcast" << std::endl; + cleanupAndExit(1); + } + std::cout << "Ok: Got access to broadcast" << std::endl; + broadcast_address.sin_addr.s_addr = INADDR_BROADCAST; + } else { + if (inet_pton(AF_INET, broadcast_arg.c_str(), &broadcast_address.sin_addr) != 1) { + std::cerr << "Error: --broadcast must be 'yes' or a valid IPv4 address" << std::endl; + cleanupAndExit(1); + } + } + + if (bind(_socket, reinterpret_cast(&client_address), sizeof(client_address)) != 0) { + std::cerr << "Error: Can't bind socket" << std::endl; + cleanupAndExit(1); + } + std::cout << "Ok: Socket bound" << std::endl; + + #if defined(_WIN32) || defined(_WIN64) + DWORD tv = 1000; // user timeout in milliseconds [ms] + setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&tv), sizeof(tv)); + #else + struct timeval tv; + tv.tv_sec = 1; + tv.tv_usec = 0; + setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&tv), sizeof(tv)); #endif // Run receiver or sender - if (result["type"].as() == "receiver") { // - Receiver::run(); // - } else if (result["type"].as() == "sender") { // Run receiver or sender - Sender::run(); // application - } else { // - std::cerr << "Error: Type not found" << std::endl; // + if (type == "receiver") { + Receiver::run(); + } else { + Sender::run(); } CLOSE_SOCKET(_socket); + CLEANUP_NETWORK(); return 0; } diff --git a/src/Receiver.cpp b/src/Receiver.cpp index 2b85a73..1034bf8 100644 --- a/src/Receiver.cpp +++ b/src/Receiver.cpp @@ -1,21 +1,37 @@ #include "Utils.hpp" #include "Config.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + namespace Receiver { +// Hard upper bound on the file size announced by a sender. We allocate the file +// in RAM, so anything larger than this would either fail or cause OOM. Adjust +// only if you know the receiver has enough memory. +constexpr size_t MAX_FILE_LENGTH = 4ULL * 1024 * 1024 * 1024; // 4 GiB + /** * List of received parts */ -std::set parts; +std::set parts; /** * Get empty parts */ -std::vector getEmptyParts() { - std::vector result; - // For each parts - for (int i = 0; i < (int)((file_length + mtu - 1) / mtu); i++) { +std::vector getEmptyParts() { + std::vector result; + size_t total_parts = (file_length + mtu - 1) / static_cast(mtu); + for (size_t i = 0; i < total_parts; i++) { if (parts.find(i) == parts.end()) result.push_back(i); } return result; @@ -26,11 +42,17 @@ std::vector getEmptyParts() { * Gets empty parts and requests them from the server */ void checkParts() { - char* buffer = new char[2 * mtu]; + char* buffer = new (std::nothrow) char[2 * mtu]; + if (!buffer) { + std::cerr << "Error: Can't allocate receive buffer" << std::endl; + delete[] file; + file = nullptr; + return; + } - std::vector emptyParts = getEmptyParts(); + std::vector emptyParts = getEmptyParts(); - while (ttl && emptyParts.size() > 0) { + while (ttl && !emptyParts.empty()) { for (auto index : emptyParts) { snprintf(buffer, 7, "RESEND"); Utils::writeBytesFromNumber(buffer + 6, index, 4); @@ -40,7 +62,8 @@ void checkParts() { std::this_thread::sleep_for(20ms); } - SOCKADDR_IN sender_address = { 0 }; + SOCKADDR_IN sender_address; + memset(&sender_address, 0, sizeof(sender_address)); addr_len sender_address_length = sizeof(sender_address); auto length = recvfrom(_socket, buffer, 2 * mtu, 0, reinterpret_cast(&sender_address), &sender_address_length); @@ -53,14 +76,15 @@ void checkParts() { ttl = ttl_max; if (strncmp(buffer, "TRANSFER", 8) == 0) { - int part = Utils::getNumberFromBytes(buffer + 8, 4); - int size = Utils::getNumberFromBytes(buffer + 12, 4); - int total_parts = (file_length + mtu - 1) / mtu; + size_t part = Utils::getNumberFromBytes(buffer + 8, 4); + size_t size = Utils::getNumberFromBytes(buffer + 12, 4); + size_t total_parts = (file_length + mtu - 1) / static_cast(mtu); - if (part < 0 || part >= total_parts || size <= 0 || size > mtu) continue; + if (part >= total_parts || size == 0 || size > static_cast(mtu)) continue; + if (static_cast(length) < size + 16) continue; parts.insert(part); - memcpy(file + part * mtu, buffer + 16, size); + memcpy(file + part * static_cast(mtu), buffer + 16, size); std::cout << "Receive " << part << " part with size " << size << std::endl; } @@ -77,24 +101,51 @@ void checkParts() { } std::ofstream output(fileName, std::ofstream::binary); - output.write(file, file_length); + if (!output.is_open()) { + std::cerr << "Error: Can't open output file " << fileName << std::endl; + delete[] file; + file = nullptr; + return; + } + output.write(file, static_cast(file_length)); + if (!output) { + std::cerr << "Error: Failed to write output file " << fileName << std::endl; + delete[] file; + file = nullptr; + return; + } + output.close(); std::cout << "File successfully received" << std::endl; delete[] file; file = nullptr; - CLOSE_SOCKET(_socket); - exit(0); } void run() { bool finish = false; // Sender finished transferring - char* buffer = new char[2 * mtu]; + char* buffer = new (std::nothrow) char[2 * mtu]; + if (!buffer) { + std::cerr << "Error: Can't allocate receive buffer" << std::endl; + return; + } - while (auto length = recvfrom(_socket, buffer, 2 * mtu, 0, reinterpret_cast(&server_address), &server_address_length)) { + while (auto length = recvfrom(_socket, buffer, 2 * mtu, 0, + reinterpret_cast(&server_address), + &server_address_length)) { // Sender is no longer available if (ttl <= 0) { + delete[] buffer; + delete[] file; + file = nullptr; + return; + } + + // Got FINISH but never received NEW_PACKET — joined too late or sender + // misbehaving. Bail with an error rather than waiting for ttl to drain. + if (finish && file == nullptr) { + std::cerr << "Error: Received FINISH without NEW_PACKET — joined too late" << std::endl; delete[] buffer; return; } @@ -113,27 +164,46 @@ void run() { ttl = ttl_max; // Update ttl - if (strncmp(buffer, "NEW_PACKET", 10) == 0) { - file_length = Utils::getNumberFromBytes(buffer + 10, 4); // Read section "file length" + if (strncmp(buffer, "NEW_PACKET", 10) == 0 && static_cast(length) >= 14) { + size_t announced = Utils::getNumberFromBytes(buffer + 10, 4); + if (announced == 0) { + std::cerr << "Error: Sender announced empty file" << std::endl; + delete[] buffer; + return; + } + if (announced > MAX_FILE_LENGTH) { + std::cerr << "Error: Sender announced file size " << announced + << " bytes, exceeds limit of " << MAX_FILE_LENGTH << std::endl; + delete[] buffer; + return; + } + + file_length = announced; delete[] file; parts.clear(); - file = new char[file_length]; + file = new (std::nothrow) char[file_length]; + if (!file) { + std::cerr << "Error: Can't allocate " << file_length << " bytes" << std::endl; + delete[] buffer; + return; + } memset(file, 0, file_length); std::cout << "Receive information about new file size: " << file_length << std::endl; std::cout << "Number of parts: " << (file_length + mtu - 1) / mtu << std::endl; } else if (strncmp(buffer, "TRANSFER", 8) == 0 && file != nullptr) { - int part = Utils::getNumberFromBytes(buffer + 8, 4); // Read section "index" - int size = Utils::getNumberFromBytes(buffer + 12, 4); // Read section "size" - int total_parts = (file_length + mtu - 1) / mtu; + size_t part = Utils::getNumberFromBytes(buffer + 8, 4); // Read section "index" + size_t size = Utils::getNumberFromBytes(buffer + 12, 4); // Read section "size" + size_t total_parts = (file_length + mtu - 1) / static_cast(mtu); - if (part < 0 || part >= total_parts || size <= 0 || size > mtu) continue; + if (part >= total_parts || size == 0 || size > static_cast(mtu)) continue; + if (static_cast(length) < size + 16) continue; parts.insert(part); std::cout << "Receive " << part << " part with size " << size << std::endl; - memcpy(file + part * mtu, buffer + 16, size); + memcpy(file + part * static_cast(mtu), buffer + 16, size); } else if (strncmp(buffer, "FINISH", 6) == 0) { // If receiver didn't receive a finish message if (!finish) { @@ -143,6 +213,8 @@ void run() { } } delete[] buffer; + delete[] file; + file = nullptr; } } //namespace Receiver diff --git a/src/Sender.cpp b/src/Sender.cpp index 888dec3..f405921 100644 --- a/src/Sender.cpp +++ b/src/Sender.cpp @@ -1,118 +1,159 @@ #include "Utils.hpp" #include "Config.hpp" +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + namespace Sender { +constexpr int HEADER_SIZE = 16; + /** * Since server may receive many requests for sending some part * It is necessary to limit the sending of the same parts for a while * This container contains part number and time when the part was sent */ -std::map sent_part; +std::map sent_part; -void sendPart(int part_index) { - int packet_length = file_length - part_index * mtu; // Get packet length - if (packet_length > mtu) packet_length = mtu; // +void sendPart(size_t part_index) { + size_t offset = part_index * static_cast(mtu); + size_t packet_length = file_length - offset; + if (packet_length > static_cast(mtu)) packet_length = static_cast(mtu); snprintf(buffer, 9, "TRANSFER"); - Utils::writeBytesFromNumber(buffer + 8, (size_t)part_index, 4); // Write section "number" - Utils::writeBytesFromNumber(buffer + 12, (size_t)packet_length, 4); // Write section "length" - memcpy(buffer + 16, file + part_index * mtu, packet_length); // Write section "data" - - // Sending part to the broadcast address - sendto(_socket, buffer, packet_length + 16, 0, reinterpret_cast(&broadcast_address), sizeof(broadcast_address)); + Utils::writeBytesFromNumber(buffer + 8, part_index, 4); // Write section "number" + Utils::writeBytesFromNumber(buffer + 12, packet_length, 4); // Write section "length" + memcpy(buffer + 16, file + offset, packet_length); // Write section "data" + + auto sent = sendto(_socket, buffer, static_cast(packet_length + HEADER_SIZE), 0, + reinterpret_cast(&broadcast_address), sizeof(broadcast_address)); + if (sent < 0) { + std::cerr << "Warning: Failed to send part " << part_index << std::endl; + return; + } std::cout << "Part " << part_index << " with size " << packet_length << " was sent" << std::endl; } void run() { buffer = new char[2 * mtu]; - std::ifstream input(fileName, std::ios::binary); // - if (!input.is_open()) { // Opening the file - std::cout << "Error: Can't open file " << fileName << std::endl; // - delete[] buffer; // - buffer = nullptr; // - CLOSE_SOCKET(_socket); // - exit(-1); // - } // - - // Thk Windows.h, where define max(). It so horrible... // - input.ignore((std::numeric_limits::max)()); // - file_length = static_cast(input.gcount()); // Getting file length - input.seekg(0, input.beg); // And writing file to RAM - // - file = new (std::nothrow) char[file_length]; // - if (!file) { // - std::cout << "Error: Can't allocate " << file_length << " bytes" << std::endl; - delete[] buffer; - buffer = nullptr; - CLOSE_SOCKET(_socket); - exit(-1); - } + std::ifstream input(fileName, std::ios::binary); + if (!input.is_open()) { + std::cerr << "Error: Can't open file " << fileName << std::endl; + delete[] buffer; + buffer = nullptr; + CLOSE_SOCKET(_socket); + CLEANUP_NETWORK(); + exit(-1); + } + + input.seekg(0, std::ios::end); + auto end_pos = input.tellg(); + if (end_pos < 0) { + std::cerr << "Error: Can't determine file size" << std::endl; + delete[] buffer; + buffer = nullptr; + CLOSE_SOCKET(_socket); + CLEANUP_NETWORK(); + exit(-1); + } + file_length = static_cast(end_pos); + input.seekg(0, std::ios::beg); + + if (file_length == 0) { + std::cerr << "Error: File is empty" << std::endl; + delete[] buffer; + buffer = nullptr; + CLOSE_SOCKET(_socket); + CLEANUP_NETWORK(); + exit(-1); + } + + file = new (std::nothrow) char[file_length]; + if (!file) { + std::cerr << "Error: Can't allocate " << file_length << " bytes" << std::endl; + delete[] buffer; + buffer = nullptr; + CLOSE_SOCKET(_socket); + CLEANUP_NETWORK(); + exit(-1); + } input.read(file, static_cast(file_length)); if (static_cast(input.gcount()) != file_length) { std::cerr << "Error: Could not read entire file" << std::endl; delete[] file; file = nullptr; delete[] buffer; buffer = nullptr; CLOSE_SOCKET(_socket); + CLEANUP_NETWORK(); exit(-1); } std::cout << "Ok: File successfully copied to RAM" << std::endl; - snprintf(buffer, 11, "NEW_PACKET"); // - Utils::writeBytesFromNumber(buffer + 10, file_length, 4); // Sending information - sendto(_socket, buffer, 14, 0, // about size of new file - reinterpret_cast(&broadcast_address), // - sizeof(broadcast_address)); // + snprintf(buffer, 11, "NEW_PACKET"); + Utils::writeBytesFromNumber(buffer + 10, file_length, 4); + if (sendto(_socket, buffer, 14, 0, + reinterpret_cast(&broadcast_address), + sizeof(broadcast_address)) < 0) { + std::cerr << "Warning: Failed to send NEW_PACKET" << std::endl; + } std::cout << "Ok: Sent information about new file with size " << file_length << std::endl; - int part_index = 0; + size_t total_parts = (file_length + mtu - 1) / static_cast(mtu); - while (part_index * mtu < file_length) { // - sent_part.insert({ part_index, 0 }); // - // - sendPart(part_index); // Send parts - // every 20ms - part_index++; // - std::this_thread::sleep_for(20ms); // - } // + for (size_t part_index = 0; part_index < total_parts; ++part_index) { + sent_part.insert({ part_index, 0 }); + sendPart(part_index); + std::this_thread::sleep_for(20ms); + } - snprintf(buffer, 7, "FINISH"); // Sending file transfer - sendto(_socket, buffer, 6, 0, // completion information - reinterpret_cast(&broadcast_address), // - sizeof(broadcast_address)); // - std::cout << "Ok: File transfer complete" << std::endl; // + snprintf(buffer, 7, "FINISH"); + if (sendto(_socket, buffer, 6, 0, + reinterpret_cast(&broadcast_address), + sizeof(broadcast_address)) < 0) { + std::cerr << "Warning: Failed to send FINISH" << std::endl; + } + std::cout << "Ok: File transfer complete" << std::endl; long lastFinishSendTime = 0; // Last time, when sender sent file transfer completion information - SOCKADDR_IN sender_address = { 0 }; + SOCKADDR_IN sender_address; + memset(&sender_address, 0, sizeof(sender_address)); addr_len sender_address_length = sizeof(sender_address); while (ttl) { - auto result = recvfrom(_socket, buffer, 100, 0, reinterpret_cast(&sender_address), &sender_address_length); + auto result = recvfrom(_socket, buffer, 100, 0, + reinterpret_cast(&sender_address), &sender_address_length); - // sending file completion information every second + // No incoming requests for a while - resend FINISH and decrement ttl. if (result <= 0) { ttl--; snprintf(buffer, 7, "FINISH"); - sendto(_socket, buffer, 6, 0, reinterpret_cast(&broadcast_address), sizeof(broadcast_address)); + if (sendto(_socket, buffer, 6, 0, + reinterpret_cast(&broadcast_address), + sizeof(broadcast_address)) < 0) { + std::cerr << "Warning: Failed to send FINISH" << std::endl; + } continue; } if (strncmp(buffer, "RESEND", 6) == 0) { - int part = Utils::getNumberFromBytes(buffer + 6, 4); - int total_parts = (file_length + mtu - 1) / mtu; + size_t part = Utils::getNumberFromBytes(buffer + 6, 4); - if (part < 0 || part >= total_parts) continue; + if (part >= total_parts) continue; auto now = std::chrono::system_clock::now(); - auto now_ms = std::chrono::time_point_cast(now); - auto epoch = now_ms.time_since_epoch(); - auto value = std::chrono::duration_cast(epoch); - long duration = value.count(); // Unix time in second + auto epoch = now.time_since_epoch(); + long duration = std::chrono::duration_cast(epoch).count(); ttl = ttl_max; @@ -126,12 +167,15 @@ void run() { if (duration - lastFinishSendTime >= 1) { lastFinishSendTime = duration; snprintf(buffer, 7, "FINISH"); - sendto(_socket, buffer, 6, 0, reinterpret_cast(&broadcast_address), sizeof(broadcast_address)); + if (sendto(_socket, buffer, 6, 0, + reinterpret_cast(&broadcast_address), + sizeof(broadcast_address)) < 0) { + std::cerr << "Warning: Failed to send FINISH" << std::endl; + } } } - } - std::cout << "Ok: Process no longer be working" << std::endl; + std::cout << "Ok: Transfer session ended" << std::endl; delete[] buffer; delete[] file; diff --git a/src/Utils.hpp b/src/Utils.hpp index 51323e3..95c2fc1 100644 --- a/src/Utils.hpp +++ b/src/Utils.hpp @@ -3,62 +3,46 @@ #if defined(_WIN32) || defined(_WIN64) #define _WINSOCK_DEPRECATED_NO_WARNINGS -#define addr_len int // -#include // Windows -#include // socket -#pragma comment(lib, "Ws2_32.lib") // -#define CLOSE_SOCKET(s) closesocket(s) // +#define addr_len int +#include +#include +#include +#pragma comment(lib, "Ws2_32.lib") +#define CLOSE_SOCKET(s) closesocket(s) +#define CLEANUP_NETWORK() WSACleanup() #else -#define SOCKET int // -#define SOCKADDR_IN sockaddr_in // -#define addr_len socklen_t // Linux socket -#define INVALID_SOCKET (-1) // -#include // -#include // -#define CLOSE_SOCKET(s) close(s) // +#define SOCKET int +#define SOCKADDR_IN sockaddr_in +#define addr_len socklen_t +#define INVALID_SOCKET (-1) +#include +#include +#define CLOSE_SOCKET(s) close(s) +#define CLEANUP_NETWORK() ((void)0) #endif -#include -#include -#include -#include // memcpy -#include - -#include -#include -#include - -#include "cxxopts.hpp" -#include "Config.hpp" - - -using namespace std::chrono_literals; +#include namespace Utils { /** - * Return a coded number - * @param buffer - bytes array - * @param count - bytes count + * Decode an unsigned integer from a big-endian byte sequence. */ - size_t getNumberFromBytes(char* buffer, int count) { + inline size_t getNumberFromBytes(const char* buffer, int count) { size_t number = 0; for (int i = 0; i < count; i++) { number = number << 8; - number = number | (buffer[i] & 0xFF); + number = number | (static_cast(buffer[i])); } return number; } /** - * Write a coded number - * @param buffer - ptr to write - * @param value - number - * @param count - count bytes + * Encode an unsigned integer into a big-endian byte sequence. */ - void writeBytesFromNumber(char* buffer, size_t number, int count) { + inline void writeBytesFromNumber(char* buffer, size_t number, int count) { for (int i = 0; i < count; i++) { - buffer[count - i - 1] = (char) (number >> (i * 8)); + buffer[count - i - 1] = static_cast(number >> (i * 8)); } } } diff --git a/tests/e2e.sh b/tests/e2e.sh new file mode 100755 index 0000000..a45c9cc --- /dev/null +++ b/tests/e2e.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# End-to-end loopback test: spawn a receiver, then a sender, and verify the +# received bytes are bit-identical to what was sent. Run locally with: +# +# make e2e +# +# Or directly: +# +# BINARY=./FileBroadcaster tests/e2e.sh +# +set -euo pipefail + +BINARY="${BINARY:-./FileBroadcaster}" + +if [ ! -x "$BINARY" ]; then + echo "Error: $BINARY not found or not executable. Run 'make program' first." >&2 + exit 1 +fi + +WORKDIR="$(mktemp -d -t fb-e2e.XXXXXX)" +trap 'rm -rf "$WORKDIR"' EXIT + +run_test() { + local label="$1" + local size_kb="$2" + local recv_port="$3" + local send_port="$4" + local recv_ttl="$5" + local send_ttl="$6" + + local src="$WORKDIR/src-$label.bin" + local dst="$WORKDIR/dst-$label.bin" + local recv_log="$WORKDIR/recv-$label.log" + local send_log="$WORKDIR/send-$label.log" + + echo "==> [$label] generating ${size_kb} KiB file" + dd if=/dev/urandom of="$src" bs=1024 count="$size_kb" status=none + + # Receiver listens on $recv_port, sends RESEND back to $send_port (sender's bind). + echo "==> [$label] starting receiver (bind=$recv_port, target=$send_port)" + "$BINARY" --type receiver --file "$dst" --broadcast 127.0.0.1 \ + --bind-port "$recv_port" --port "$send_port" --ttl "$recv_ttl" \ + > "$recv_log" 2>&1 & + local recv_pid=$! + + # Give the receiver a moment to bind before the sender starts blasting. + sleep 1 + + # Sender listens on $send_port, sends TRANSFER to $recv_port (receiver's bind). + echo "==> [$label] starting sender (bind=$send_port, target=$recv_port)" + if ! "$BINARY" --type sender --file "$src" --broadcast 127.0.0.1 \ + --bind-port "$send_port" --port "$recv_port" --ttl "$send_ttl" \ + > "$send_log" 2>&1; then + echo "FAIL: [$label] sender exited non-zero" + echo "--- sender log:"; cat "$send_log" + kill "$recv_pid" 2>/dev/null || true + return 1 + fi + + # Wait for receiver to drain and exit on its own ttl. + if ! wait "$recv_pid"; then + echo "FAIL: [$label] receiver exited non-zero" + echo "--- receiver log:"; cat "$recv_log" + return 1 + fi + + if [ ! -f "$dst" ]; then + echo "FAIL: [$label] receiver did not produce output file" + echo "--- receiver log:"; cat "$recv_log" + return 1 + fi + + if ! cmp -s "$src" "$dst"; then + local src_size dst_size + src_size=$(wc -c < "$src") + dst_size=$(wc -c < "$dst") + echo "FAIL: [$label] received file does not match source" + echo " src size: $src_size bytes" + echo " dst size: $dst_size bytes" + echo "--- receiver log (last 20 lines):" + tail -20 "$recv_log" + echo "--- sender log (last 20 lines):" + tail -20 "$send_log" + return 1 + fi + + echo "PASS: [$label] $size_kb KiB transferred and matches source" +} + +# Args: label, size_kb, recv_bind_port, send_bind_port, recv_ttl, send_ttl +# +# Small file: 50 KiB -> ~35 parts at default MTU 1500, ~0.7s transfer. +run_test "small" 50 33401 33402 5 3 + +# Large file: 2 MiB -> ~1400 parts at default MTU 1500, ~28s transfer. +run_test "large" 2048 33403 33404 60 30 + +echo +echo "All E2E tests passed."