From d33f965419743c336ff234aafd37ea6c153003eb Mon Sep 17 00:00:00 2001 From: hoanghohohaha Date: Tue, 7 Oct 2025 19:47:04 -0700 Subject: [PATCH 1/8] temp --- Engine/CMakeLists.txt | 5 ++ Engine/Utils/Networks/ISocket.cpp | 0 Engine/Utils/Networks/ISocket.hpp | 25 ++++++ Engine/Utils/Networks/WindowsSocket.cpp | 109 ++++++++++++++++++++++++ Engine/Utils/Networks/WindowsSocket.hpp | 30 +++++++ demo/CMakeLists.txt | 2 + demo/network/CMakeLists.txt | 7 ++ demo/network/src/main.cpp | 30 +++++++ 8 files changed, 208 insertions(+) create mode 100644 Engine/Utils/Networks/ISocket.cpp create mode 100644 Engine/Utils/Networks/ISocket.hpp create mode 100644 Engine/Utils/Networks/WindowsSocket.cpp create mode 100644 Engine/Utils/Networks/WindowsSocket.hpp create mode 100644 demo/network/CMakeLists.txt create mode 100644 demo/network/src/main.cpp diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 11a0e2b..7e972e8 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -13,6 +13,7 @@ set(ENGINE_SOURCE_FILES Components/Mesh3D.cpp # Renderer/CPU/RayTracer.cpp Utils/AssetManager.cpp + Utils/Networks/WindowsSocket.cpp ) add_library(Engine ${ENGINE_SOURCE_FILES}) @@ -23,6 +24,10 @@ target_link_libraries(Engine PRIVATE glfw) target_link_libraries(Engine PRIVATE assimp::assimp) target_include_directories(Engine PRIVATE ${Stb_INCLUDE_DIR}) +if (WIN32) + target_link_libraries(Engine PUBLIC Ws2_32) +endif() + if (OpenMP_CXX_FOUND) target_link_libraries(Engine PRIVATE OpenMP::OpenMP_CXX) endif() diff --git a/Engine/Utils/Networks/ISocket.cpp b/Engine/Utils/Networks/ISocket.cpp new file mode 100644 index 0000000..e69de29 diff --git a/Engine/Utils/Networks/ISocket.hpp b/Engine/Utils/Networks/ISocket.hpp new file mode 100644 index 0000000..12925ec --- /dev/null +++ b/Engine/Utils/Networks/ISocket.hpp @@ -0,0 +1,25 @@ +#pragma once +#include +#include + +namespace Bored::Net { + +enum Family : uint8_t { Unspec, IPv4, IPv6 }; +enum Type : uint8_t { Stream, Datagram }; +enum Protocol: uint8_t { UDP, TCP }; + + +class ISocket { + +public: + ISocket() = default; + virtual ~ISocket() = default; + + virtual void Open(Family family, Type type, Protocol proto) = 0; + virtual void Close() = 0; + virtual void Bind( const std::string& address, const int port) = 0; + virtual void SendTo( const std::string& address, const int port, const std::string& message) = 0; + virtual std::string ReceiveFrom(std::string& out_address, int& out_port) = 0; +}; + +} // namespace Bored::Net diff --git a/Engine/Utils/Networks/WindowsSocket.cpp b/Engine/Utils/Networks/WindowsSocket.cpp new file mode 100644 index 0000000..717f9f6 --- /dev/null +++ b/Engine/Utils/Networks/WindowsSocket.cpp @@ -0,0 +1,109 @@ +#include "WindowsSocket.hpp" +#include +#include +#include +#include // for InetPtonA + +namespace Bored::Net { + +std::string WindowsSocket::getLastError() { + return std::to_string(WSAGetLastError()); +} + +int WindowsSocket::mapFamily(Family fam) { + switch (fam) { + case Unspec: + return AF_UNSPEC; + case IPv4: + return AF_INET; + case IPv6: + return AF_INET6; + default: + return -1; + } +} + +int WindowsSocket::mapProtocol(Protocol proto) { + switch (proto) { + case UDP: + return IPPROTO_UDP; + case TCP: + return IPPROTO_TCP; + default: + return -1; + } +} + +int WindowsSocket::mapType(Type type) { + switch (type) { + case Stream: + return SOCK_STREAM; + case Datagram: + return SOCK_DGRAM; + default: + return -1; + } +} + +WindowsSocket::WindowsSocket() { + WSADATA wsadata; + + int result = 0; + result = WSAStartup(MAKEWORD(2, 2), &wsadata); + if (result != NO_ERROR) { + std::cout << "WSAStartup failed to initialized" << std::endl; + }; +} + + +void WindowsSocket::Open(Family family, Type type, Protocol proto) { + if (sock_ != INVALID_SOCKET) { + std::cout << "Socket already open, create a new one if needed" << std::endl; + } + sock_ = socket(mapFamily(family), mapType(type), mapProtocol(proto)); + if (sock_ == INVALID_SOCKET) { + std::cout << "Failed to open a new socket" << std::endl; + } +} + +void WindowsSocket::Close() { + if (sock_ == INVALID_SOCKET) + return; + + if (closesocket(sock_) == SOCKET_ERROR) { + throw std::runtime_error("Failed to close socket"); + } + + WSACleanup(); +} + +void WindowsSocket::Bind(const std::string &address, const int port) { + sockaddr_in bind_address; + // TODO: Fix this hardcode later + bind_address.sin_family = AF_INET; + bind_address.sin_port = htons(port); + if (address.empty() || address == "*" || address == "0.0.0.0") { + bind_address.sin_addr.s_addr = INADDR_ANY; + } else { + if (InetPtonA(AF_INET, address.c_str(), &bind_address.sin_addr) != 1) { + throw std::runtime_error("InetPton failed for '" + address + "'"); + } + } + + if (bind(sock_, reinterpret_cast(&bind_address), + sizeof(bind_address)) == SOCKET_ERROR) { + throw std::runtime_error("bind() failed: " + getLastError()); + } +} + +void WindowsSocket::SendTo(const std::string &address, const int port, + const std::string &message) { + return; +} + +std::string WindowsSocket::ReceiveFrom(std::string &out_address, + int &out_port) { + return ""; +} + +} // namespace Bored::Net diff --git a/Engine/Utils/Networks/WindowsSocket.hpp b/Engine/Utils/Networks/WindowsSocket.hpp new file mode 100644 index 0000000..bbf38ab --- /dev/null +++ b/Engine/Utils/Networks/WindowsSocket.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "ISocket.hpp" +#include +#include + +namespace Bored::Net { + +class WindowsSocket : public ISocket { +public: + WindowsSocket(); + ~WindowsSocket() override {}; + + void Open(Family family, Type type, Protocol proto) override; + void Close() override; + void Bind(const std::string &address, const int port) override; + void SendTo(const std::string &address, const int port, + const std::string &message) override; + std::string ReceiveFrom(std::string &out_address, int &out_port) override; + +private: + SOCKET sock_ = INVALID_SOCKET; + + static std::string getLastError(); + + int mapFamily(Family fam); + int mapProtocol(Protocol proto); + int mapType(Type type); +}; + +} // namespace Bored::Net diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index f7296e3..24cd278 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -3,3 +3,5 @@ add_subdirectory("demo1") add_subdirectory("demo2") add_subdirectory("maze") + +add_subdirectory("network") diff --git a/demo/network/CMakeLists.txt b/demo/network/CMakeLists.txt new file mode 100644 index 0000000..5b62532 --- /dev/null +++ b/demo/network/CMakeLists.txt @@ -0,0 +1,7 @@ +set(SOURCE_FILES + src/main.cpp +) + +add_executable(network ${SOURCE_FILES}) +target_link_libraries(network PRIVATE Engine) +target_include_directories(network PRIVATE "${PROJECT_SOURCE_DIR}/Engine") diff --git a/demo/network/src/main.cpp b/demo/network/src/main.cpp new file mode 100644 index 0000000..0456a13 --- /dev/null +++ b/demo/network/src/main.cpp @@ -0,0 +1,30 @@ +#include "Utils/Networks/WindowsSocket.hpp" +#include + +using namespace Bored::Net; + +int main(int argc, char **argv) { + const int port = (argc > 1) ? std::atoi(argv[1]) : 9000; + + try { + WindowsSocket srv; + srv.Open(IPv4, Datagram, UDP); + srv.Bind("", port); // "" or "0.0.0.0" listens on all interfaces + + std::cout << "UDP echo server on port " << port << "...\n"; + // for (;;) { + // std::string fromAddr; + // int fromPort = 0; + // std::string payload = srv.ReceiveFrom(fromAddr, fromPort); + // std::cout << "Got " << payload.size() << " bytes from " << fromAddr << ":" + // << fromPort << " -> '" << payload << "'\n"; + // + // // Echo it back + // srv.SendTo(fromAddr, fromPort, payload); + // } + } catch (const std::exception &e) { + std::cerr << "Server error: " << e.what() << "\n"; + return 1; + } + return 0; +} From edd4024d94619a8f800476254a196b6c106de143 Mon Sep 17 00:00:00 2001 From: hoanghohohaha Date: Wed, 8 Oct 2025 14:08:05 -0700 Subject: [PATCH 2/8] done --- Engine/Utils/Networks/WindowsSocket.cpp | 45 +++++++- demo/network/src/main.cpp | 65 +++++++++-- implementatin.md | 144 ++++++++++++++++++++++++ research.md | 108 ++++++++++++++++++ 4 files changed, 347 insertions(+), 15 deletions(-) create mode 100644 implementatin.md create mode 100644 research.md diff --git a/Engine/Utils/Networks/WindowsSocket.cpp b/Engine/Utils/Networks/WindowsSocket.cpp index 717f9f6..f92c089 100644 --- a/Engine/Utils/Networks/WindowsSocket.cpp +++ b/Engine/Utils/Networks/WindowsSocket.cpp @@ -1,11 +1,14 @@ #include "WindowsSocket.hpp" #include #include +#include #include #include // for InetPtonA namespace Bored::Net { +constexpr int RECEIVE_BUF_LEN = 2048; + std::string WindowsSocket::getLastError() { return std::to_string(WSAGetLastError()); } @@ -55,7 +58,6 @@ WindowsSocket::WindowsSocket() { }; } - void WindowsSocket::Open(Family family, Type type, Protocol proto) { if (sock_ != INVALID_SOCKET) { std::cout << "Socket already open, create a new one if needed" << std::endl; @@ -98,12 +100,49 @@ void WindowsSocket::Bind(const std::string &address, const int port) { void WindowsSocket::SendTo(const std::string &address, const int port, const std::string &message) { - return; + sockaddr_in receive_addr; + // TODO: Fix this hardcode later + receive_addr.sin_family = AF_INET; + receive_addr.sin_port = htons(port); + if (address.empty() || address == "*" || address == "0.0.0.0") { + receive_addr.sin_addr.s_addr = INADDR_ANY; + } else { + if (InetPtonA(AF_INET, address.c_str(), &receive_addr.sin_addr) != 1) { + throw std::runtime_error("InetPton failed for '" + address + "'"); + } + } + + if (sendto(sock_, message.data(), message.size(), 0, + reinterpret_cast(&receive_addr), + sizeof(receive_addr)) == SOCKET_ERROR) { + throw std::runtime_error("sendto() failed: " + getLastError()); + } } std::string WindowsSocket::ReceiveFrom(std::string &out_address, int &out_port) { - return ""; + std::vector buf(RECEIVE_BUF_LEN); + sockaddr_storage from{}; + int fromlen = sizeof(from); + + int n = recvfrom(sock_, buf.data(), (int)buf.size(), 0, (sockaddr *)&from, + &fromlen); + if (n == SOCKET_ERROR) { + throw std::runtime_error("recvfrom() failed: " + getLastError()); + } + + // Set sender + char host[NI_MAXHOST]{}, serv[NI_MAXSERV]{}; + if (getnameinfo((sockaddr *)&from, fromlen, host, sizeof(host), serv, + sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV) == 0) { + out_address = host; + out_port = std::atoi(serv); + } else { + out_address.clear(); + out_port = 0; + } + + return std::string(buf.data(), n); } } // namespace Bored::Net diff --git a/demo/network/src/main.cpp b/demo/network/src/main.cpp index 0456a13..8186c64 100644 --- a/demo/network/src/main.cpp +++ b/demo/network/src/main.cpp @@ -3,8 +3,7 @@ using namespace Bored::Net; -int main(int argc, char **argv) { - const int port = (argc > 1) ? std::atoi(argv[1]) : 9000; +int run_server(int port) { try { WindowsSocket srv; @@ -12,19 +11,61 @@ int main(int argc, char **argv) { srv.Bind("", port); // "" or "0.0.0.0" listens on all interfaces std::cout << "UDP echo server on port " << port << "...\n"; - // for (;;) { - // std::string fromAddr; - // int fromPort = 0; - // std::string payload = srv.ReceiveFrom(fromAddr, fromPort); - // std::cout << "Got " << payload.size() << " bytes from " << fromAddr << ":" - // << fromPort << " -> '" << payload << "'\n"; - // - // // Echo it back - // srv.SendTo(fromAddr, fromPort, payload); - // } + for (;;) { + std::string fromAddr; + int fromPort = 0; + std::string payload = srv.ReceiveFrom(fromAddr, fromPort); + std::cout << "Got " << payload.size() << " bytes from " << fromAddr << ":" + << fromPort << " -> '" << payload << "'\n"; + + // Echo it back + srv.SendTo(fromAddr, fromPort, payload); + } } catch (const std::exception &e) { std::cerr << "Server error: " << e.what() << "\n"; return 1; } return 0; } + +int run_client(int port, const char *msg, const char *host) { + try { + WindowsSocket cli; + cli.Open(IPv4, Datagram, UDP); + cli.SendTo(host, port, msg); + std::cout << "Sent: '" << msg << "' to " << host << ":" << port << "\n"; + + std::string fromAddr; + int fromPort = 0; + std::string reply = cli.ReceiveFrom(fromAddr, fromPort); + std::cout << "Reply from " << fromAddr << ":" << fromPort << " -> '" + << reply << "'\n"; + } catch (const std::exception &e) { + std::cerr << "Client error: " << e.what() << "\n"; + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + if (argc <= 1) { + std::cout << "Usage network.exe " + << std::endl; + return 0; + } + const std::string mode = argv[1]; + + const int port = (argc > 2) ? std::atoi(argv[2]) : 9000; + const char *msg = (argc > 3) ? argv[3] : "hello via ISocket"; + + const char *host = (argc > 4) ? argv[4] : "127.0.0.1"; + std::cout << "Using mode: " << mode << " with port " << port << "& host " << host + << "& mes " << msg << std::endl; + + if (mode == "server") { + return run_server(port); + } else if (mode == "client") { + return run_client(port, msg, host); + } + return 0; +} diff --git a/implementatin.md b/implementatin.md new file mode 100644 index 0000000..79ef9ae --- /dev/null +++ b/implementatin.md @@ -0,0 +1,144 @@ +## Networking Implementation Plan (WinSock from scratch) + +### Scope and Constraints +• Transport: UDP via WinSock2 from scratch; custom reliability/ordering. +• OS: Windows 10+ desktop. IPv4 first, IPv6 later. +• No external net libs: Build our own framing, acks, retransmit, fragmentation. + +### Milestones +1) Bootstrap WinSock and minimal loopback echo (UDP). +2) Non-blocking sockets + basic send/recv API + graceful shutdown. +3) Connection model (pseudo-connection over UDP): handshakes, heartbeats, timeouts. +4) Channels/QoS: unreliable, reliable-ordered, reliable-unordered. +5) Sequencing, ack bitfields, retransmission, congestion/back-pressure. +6) Message framing, MTU discovery, fragmentation/reassembly. +7) Time sync (RTT/offset), tick stamping, clock smoothing. +8) Client prediction & server reconciliation hooks (movement only). +9) Replication scaffolding (snapshot/delta) and interest filtering. +10) Observability: counters, logs, debug overlay; soak tests. +11) Performance pass; optional IOCP migration. + +### Threading Model (phase 1) +• One dedicated networking thread per process. +• Lock-free MPSC queues between main thread and net thread: + – toNetworkQueue: game→net messages (outgoing). + – fromNetworkQueue: net→game events (incoming). +• Non-blocking sockets with WSAPoll initially; IOCP in phase 2. + +### WinSock Setup +• Call WSAStartup(MAKEWORD(2, 2), &wsaData) on init; WSACleanup() on shutdown. +• Create UDP socket: socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP). +• Set non-blocking: u_long mode = 1; ioctlsocket(sock, FIONBIO, &mode). +• Set buffer sizes via setsockopt (SO_RCVBUF/SO_SNDBUF), enable SIO_UDP_CONNRESET workaround. +• Bind for server; for client, bind ephemeral and remember server endpoint. + +### Optional IO Model (phase 2) +• IOCP: CreateIoCompletionPort, OVERLAPPED, WSARecvFrom/WSASendTo posting. +• Pool of IO worker threads sized to cores. +• Maintain per-connection endpoint state keyed by (addr, port). + +### Protocol Framing +• Fixed header (little-endian): + – u16 protocolVersion + – u8 messageType + – u8 channelId + – u32 seq + – u32 ack (last received on this channel) + – u32 ackBits (bitmap of previous 32) + – u32 serverTick + – u32 timestamp (client local send time) +• Payload: one or more frames (TLV or simple concatenation with length fields). +• Keep total payload under discovered MTU minus IP/UDP headers. + +### Channels and Reliability +• Channel 0: unreliable (inputs, state deltas) +• Channel 1: reliable-ordered (spawns, inventory, RPC) +• Channel 2: reliable-unordered (assets, control) +• Per-channel sequence numbers, ack/ackBits logic. +• Retransmit queue for reliable channels with per-packet send time and retry policy. + +### Connection State Machine (UDP pseudo-connection) +• States: Idle → SendingHello → AwaitingChallenge → Active → Disconnecting. +• Heartbeat: send keepalive every N ticks; drop on timeout. +• Stateless cookie in handshake to mitigate spoofing. + +### MTU, Fragmentation, and Reassembly +• Start with conservative payload (e.g., 1200 bytes). +• Probe up to discovered path MTU with DF toggles if possible. +• Fragment large reliable payloads; reassemble with timeouts and max in-flight limits. + +### Time Sync +• Ping/Pong with timestamp reflection; compute RTT and clock offset. +• Expose getNetworkTime() with smoothed offset. + +### Public Engine API (C++) +• class INetworkTransport + – bool initialize(const TransportConfig&) + – void shutdown() + – bool send(const Endpoint&, const ByteSpan&, ChannelId) + – bool pollReceived(Packet&, int maxPackets) +• class NetClient + – bool connect(const Endpoint&, const ClientConfig&) + – void disconnect(DisconnectReason) + – void enqueueInput(const InputSample&) +• class NetServer + – bool listen(const Endpoint&, const ServerConfig&) + – void broadcast(const Message&, ChannelId) + – void sendTo(ClientId, const Message&, ChannelId) +• class Replicator + – buildSnapshot(deltaMode); applySnapshot(...) +• class PredictionManager + – pushLocalInput(...); reconcile(authoritativeState, ackSeq) + +### Data Structures +• Endpoint{ sockaddr_storage addr; int len; } +• Packet{ Header hdr; std::vector payload; } +• SendQueue{ per-channel ring buffers, retransmit heap by deadline } +• RecvState{ per-channel expectedSeq, reassembly maps } +• Lock-free queues: reusable ring buffers with sequence cursors. + +### Serialization +• Start with custom binary writer/reader (fixed endianness, aligned primitives, varints for lengths). +• Later: bit-packing for hot paths. + +### Integration with Engine +• NetworkSystem tick order: input capture → enqueue → net tick → update prediction → renderer. +• ECS: NetEntityId mapping; Replicated tag; component serializers registered at startup. + +### Testing +• Unit tests: header encode/decode, ack/seq logic, retransmit policy, fragmentation. +• Loopback tests: client/server in-process with artificial RTT/loss. +• Soak: bots move/shoot for hours; monitor leaks and counters. + +### Diagnostics +• Counters: sent/received packets, loss, RTT, resend rate, bandwidth. +• Logs: connection lifecycle, handshake, timeouts, unusual packet sizes. +• Debug UI overlay hooks for graphs. + +### Security +• Handshake cookie; rate limiting per IP; sanity checks on payload sizes and frequencies. +• Optional XOR/AEAD later; validate state transitions server-side. + +### Migration Path to IOCP (optional) +• Replace WSAPoll loop with IOCP. +• Per-socket binding to completion port; pre-post WSARecvFrom buffers. +• Lock-free freelists for OVERLAPPED structures and buffers. + +### Deliverables per Milestone +• M1: NetClient/NetServer create sockets, send/recv echo over loopback. +• M2: Non-blocking, clean shutdown, config for buffers and timeouts. +• M3: Handshake, heartbeats, disconnect reasons; counters for RTT/loss. +• M4: Reliable channels with acks; retransmit and ordering tests. +• M5: Fragmentation, MTU probe; big message tests. +• M6: Time sync; expose getNetworkTime(); tick stamps in headers. +• M7: Prediction/reconciliation demo controlling a character. +• M8: Snapshot/delta production and application; interest filtering stub. +• M9: Debug overlay, logs, soak tests. +• M10: IOCP backend (optional) parity with WSAPoll. + +### Risks & Mitigations +• Head-of-line blocking in reliable channel → separate channels, careful batching. +• Timer granularity and sleep jitter → use QueryPerformanceCounter for timing. +• Buffer thrash → ring buffers, object pools, contiguous storage. + + diff --git a/research.md b/research.md new file mode 100644 index 0000000..d5ea706 --- /dev/null +++ b/research.md @@ -0,0 +1,108 @@ +## Networking Layer Plan + +### Goals +- **Authoritative server**: Prevent cheating, ensure consistent world state. +- **Low-latency gameplay**: Prioritize real-time inputs and state updates. +- **Scalable sessions**: Support singleplayer, LAN, and internet multiplayer. +- **Engine-first design**: Clean API integrated with ECS and Systems. + +### Architecture +- **Topology**: Client-Server (authoritative server). Optional listen-server for quick demos. +- **Transport**: UDP-based with reliability layer (e.g., ENet-style channels) or QUIC. TCP only for control/metadata if needed. +- **Message Framing**: Binary messages with header (protocol version, msg type, seq, timestamp). +- **Serialization**: Protobuf or FlatBuffers. Keep hot-path messages minimal and fixed-size where possible. +- **Threading**: Dedicated networking thread per process; lock-free queues to/from main thread. +- **Platforms**: Abstract `INetworkTransport` with implementations for desktop; allow stubs/mocks for tests. + +### Core Concepts +- **Sessions/Rooms**: Server hosts `Session` objects; clients join via `sessionId`. +- **Connections**: State machine (Connecting, Handshaking, Active, Disconnecting). Heartbeats and timeouts. +- **Channels/QoS**: + - Reliable-ordered: joins, RPCs, inventory changes, entity spawns/despawns. + - Unreliable: input samples, state deltas, snapshots. + - Reliable-unordered: asset manifests, chunked transfers. +- **Tick Model**: Server fixed tick (e.g., 60Hz). Messages carry `serverTick` for reconciliation. + +### ECS Integration +- **NetEntityId**: Stable network id separate from local entity handles. +- **Components**: Opt-in `Replicated` tag; optional `Prediction` marker. +- **Replication**: Server builds per-client interest-filtered snapshots/deltas of component subsets. +- **Authority**: Server writes authoritative state; client writes predicted components locally. + +### Client-Side Techniques +- **Client Prediction**: Predict local player movement; buffer inputs with sequence numbers. +- **Server Reconciliation**: On authoritative state, rewind to last acked tick, reapply pending inputs. +- **Interpolation/Extrapolation**: Interpolate remote entities using buffered snapshots; moderate extrapolation for short gaps. + +### Server-Side Techniques +- **Lag Compensation**: For hitscan/server validation, rewind to client `fireTime` within bounds. +- **Interest Management**: Spatial partitioning (grid/quadtree) and relevance rules to reduce bandwidth. +- **Rate Limiting**: Per-connection budgets for bytes/tick and messages/tick. + +### Reliability, Ordering, and Flow Control +- **Sequence/Ack**: Per-channel sequence numbers; ack bitfields for fast retransmit. +- **Fragmentation/MTU**: Detect MTU, fragment large reliable messages, reassemble with timeouts. +- **Clock Sync**: Lightweight NTP-style RTT/offset estimation; expose `getNetworkTime()`. + +### Protocol and Message Schema (Illustrative) +- `Hello`, `Challenge`, `Auth`, `JoinSession`, `LeaveSession` +- `InputSample{seq, dt, axes/buttons}` +- `StateDelta{serverTick, entities:[id, changedComponents...]}` +- `Snapshot{serverTick, entities:[id, fullState...]}` +- `RPC{target, methodId, payload}` +- `Pong/Ping`, `Disconnect{reason}` + +### Security and Integrity +- **Authentication**: Token-based (opaque session token). Optional guest mode. +- **Cheat Mitigation**: Authoritative checks, sanity validation on inputs, max speeds/teleport caps. +- **Encryption**: DTLS or QUIC if available; otherwise optional libsodium box per-connection. +- **DoS Protections**: Stateless cookie during handshake, IP rate limiting, proof-of-work toggle for public servers. + +### API Surface (Engine) +- `NetworkSystem` (engine system): lifecycle, tick integration, dispatch. +- `INetworkTransport`: send/receive datagrams, time, connection events. +- `NetServer`, `NetClient`: high-level roles, session management. +- `Replicator`: builds deltas/snapshots from ECS, interest filtering. +- `PredictionManager`: client reconciliation and smoothing. +- `RpcRegistry`: register callable functions with ids; route calls. +- `Serializer`: encode/decode with chosen schema. + +### Configuration +- Protocol version, tick rate, snapshot rate, send/recv budgets, interpolation delay. +- Interest rules, reliability timeouts, max entities per packet. + +### Observability +- Counters: RTT, packet loss, goodput, resend rate, snapshot sizes, interest cull ratios. +- Logs: connection lifecycle, drops, fragmentation, unusual input patterns. +- Debug UI overlay: network graphs; server console commands. + +### Testing Strategy +- Unit: serializer roundtrips, ack/seq logic, fragmentation. +- Deterministic sims: lockstep harness with scripted RTT/loss/jitter profiles. +- Soak tests: bots moving/shooting; measure stability and memory. +- Replay: record input and snapshots, deterministic replays for repros. + +### Migration/Compatibility +- Schema versioning in headers; support minor backward compatibility window. +- Feature flags for experimental channels or message types. + +### Performance Targets (initial) +- < 80 bytes avg per entity delta per snapshot for typical scenes. +- 60Hz server tick; 20–30Hz snapshot send rate; < 120ms playable RTT. + +### Roadmap +1) Prototype transport + message framing (loopback, no encryption). +2) Add reliable/unreliable channels, seq/ack, heartbeats, time sync. +3) Basic client prediction + reconciliation for character movement. +4) Snapshot/delta replication with interest management. +5) RPC system and server commands. +6) Security hardening (auth, encryption), rate limiting. +7) Observability and tools (overlay, counters, captures). +8) QUIC transport option and listen-server mode. + +### Open Questions +- QUIC vs ENet-style UDP for first implementation? +- Protobuf vs FlatBuffers vs custom bitpacking for hot paths? +- Minimum supported platforms and NAT traversal requirements? + + From 03fbcc7ab582108f0685a2c8d6a9f488864c9ba1 Mon Sep 17 00:00:00 2001 From: hoanghohohaha Date: Wed, 8 Oct 2025 14:13:51 -0700 Subject: [PATCH 3/8] remove junk --- implementatin.md | 144 ----------------------------------------------- research.md | 108 ----------------------------------- 2 files changed, 252 deletions(-) delete mode 100644 implementatin.md delete mode 100644 research.md diff --git a/implementatin.md b/implementatin.md deleted file mode 100644 index 79ef9ae..0000000 --- a/implementatin.md +++ /dev/null @@ -1,144 +0,0 @@ -## Networking Implementation Plan (WinSock from scratch) - -### Scope and Constraints -• Transport: UDP via WinSock2 from scratch; custom reliability/ordering. -• OS: Windows 10+ desktop. IPv4 first, IPv6 later. -• No external net libs: Build our own framing, acks, retransmit, fragmentation. - -### Milestones -1) Bootstrap WinSock and minimal loopback echo (UDP). -2) Non-blocking sockets + basic send/recv API + graceful shutdown. -3) Connection model (pseudo-connection over UDP): handshakes, heartbeats, timeouts. -4) Channels/QoS: unreliable, reliable-ordered, reliable-unordered. -5) Sequencing, ack bitfields, retransmission, congestion/back-pressure. -6) Message framing, MTU discovery, fragmentation/reassembly. -7) Time sync (RTT/offset), tick stamping, clock smoothing. -8) Client prediction & server reconciliation hooks (movement only). -9) Replication scaffolding (snapshot/delta) and interest filtering. -10) Observability: counters, logs, debug overlay; soak tests. -11) Performance pass; optional IOCP migration. - -### Threading Model (phase 1) -• One dedicated networking thread per process. -• Lock-free MPSC queues between main thread and net thread: - – toNetworkQueue: game→net messages (outgoing). - – fromNetworkQueue: net→game events (incoming). -• Non-blocking sockets with WSAPoll initially; IOCP in phase 2. - -### WinSock Setup -• Call WSAStartup(MAKEWORD(2, 2), &wsaData) on init; WSACleanup() on shutdown. -• Create UDP socket: socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP). -• Set non-blocking: u_long mode = 1; ioctlsocket(sock, FIONBIO, &mode). -• Set buffer sizes via setsockopt (SO_RCVBUF/SO_SNDBUF), enable SIO_UDP_CONNRESET workaround. -• Bind for server; for client, bind ephemeral and remember server endpoint. - -### Optional IO Model (phase 2) -• IOCP: CreateIoCompletionPort, OVERLAPPED, WSARecvFrom/WSASendTo posting. -• Pool of IO worker threads sized to cores. -• Maintain per-connection endpoint state keyed by (addr, port). - -### Protocol Framing -• Fixed header (little-endian): - – u16 protocolVersion - – u8 messageType - – u8 channelId - – u32 seq - – u32 ack (last received on this channel) - – u32 ackBits (bitmap of previous 32) - – u32 serverTick - – u32 timestamp (client local send time) -• Payload: one or more frames (TLV or simple concatenation with length fields). -• Keep total payload under discovered MTU minus IP/UDP headers. - -### Channels and Reliability -• Channel 0: unreliable (inputs, state deltas) -• Channel 1: reliable-ordered (spawns, inventory, RPC) -• Channel 2: reliable-unordered (assets, control) -• Per-channel sequence numbers, ack/ackBits logic. -• Retransmit queue for reliable channels with per-packet send time and retry policy. - -### Connection State Machine (UDP pseudo-connection) -• States: Idle → SendingHello → AwaitingChallenge → Active → Disconnecting. -• Heartbeat: send keepalive every N ticks; drop on timeout. -• Stateless cookie in handshake to mitigate spoofing. - -### MTU, Fragmentation, and Reassembly -• Start with conservative payload (e.g., 1200 bytes). -• Probe up to discovered path MTU with DF toggles if possible. -• Fragment large reliable payloads; reassemble with timeouts and max in-flight limits. - -### Time Sync -• Ping/Pong with timestamp reflection; compute RTT and clock offset. -• Expose getNetworkTime() with smoothed offset. - -### Public Engine API (C++) -• class INetworkTransport - – bool initialize(const TransportConfig&) - – void shutdown() - – bool send(const Endpoint&, const ByteSpan&, ChannelId) - – bool pollReceived(Packet&, int maxPackets) -• class NetClient - – bool connect(const Endpoint&, const ClientConfig&) - – void disconnect(DisconnectReason) - – void enqueueInput(const InputSample&) -• class NetServer - – bool listen(const Endpoint&, const ServerConfig&) - – void broadcast(const Message&, ChannelId) - – void sendTo(ClientId, const Message&, ChannelId) -• class Replicator - – buildSnapshot(deltaMode); applySnapshot(...) -• class PredictionManager - – pushLocalInput(...); reconcile(authoritativeState, ackSeq) - -### Data Structures -• Endpoint{ sockaddr_storage addr; int len; } -• Packet{ Header hdr; std::vector payload; } -• SendQueue{ per-channel ring buffers, retransmit heap by deadline } -• RecvState{ per-channel expectedSeq, reassembly maps } -• Lock-free queues: reusable ring buffers with sequence cursors. - -### Serialization -• Start with custom binary writer/reader (fixed endianness, aligned primitives, varints for lengths). -• Later: bit-packing for hot paths. - -### Integration with Engine -• NetworkSystem tick order: input capture → enqueue → net tick → update prediction → renderer. -• ECS: NetEntityId mapping; Replicated tag; component serializers registered at startup. - -### Testing -• Unit tests: header encode/decode, ack/seq logic, retransmit policy, fragmentation. -• Loopback tests: client/server in-process with artificial RTT/loss. -• Soak: bots move/shoot for hours; monitor leaks and counters. - -### Diagnostics -• Counters: sent/received packets, loss, RTT, resend rate, bandwidth. -• Logs: connection lifecycle, handshake, timeouts, unusual packet sizes. -• Debug UI overlay hooks for graphs. - -### Security -• Handshake cookie; rate limiting per IP; sanity checks on payload sizes and frequencies. -• Optional XOR/AEAD later; validate state transitions server-side. - -### Migration Path to IOCP (optional) -• Replace WSAPoll loop with IOCP. -• Per-socket binding to completion port; pre-post WSARecvFrom buffers. -• Lock-free freelists for OVERLAPPED structures and buffers. - -### Deliverables per Milestone -• M1: NetClient/NetServer create sockets, send/recv echo over loopback. -• M2: Non-blocking, clean shutdown, config for buffers and timeouts. -• M3: Handshake, heartbeats, disconnect reasons; counters for RTT/loss. -• M4: Reliable channels with acks; retransmit and ordering tests. -• M5: Fragmentation, MTU probe; big message tests. -• M6: Time sync; expose getNetworkTime(); tick stamps in headers. -• M7: Prediction/reconciliation demo controlling a character. -• M8: Snapshot/delta production and application; interest filtering stub. -• M9: Debug overlay, logs, soak tests. -• M10: IOCP backend (optional) parity with WSAPoll. - -### Risks & Mitigations -• Head-of-line blocking in reliable channel → separate channels, careful batching. -• Timer granularity and sleep jitter → use QueryPerformanceCounter for timing. -• Buffer thrash → ring buffers, object pools, contiguous storage. - - diff --git a/research.md b/research.md deleted file mode 100644 index d5ea706..0000000 --- a/research.md +++ /dev/null @@ -1,108 +0,0 @@ -## Networking Layer Plan - -### Goals -- **Authoritative server**: Prevent cheating, ensure consistent world state. -- **Low-latency gameplay**: Prioritize real-time inputs and state updates. -- **Scalable sessions**: Support singleplayer, LAN, and internet multiplayer. -- **Engine-first design**: Clean API integrated with ECS and Systems. - -### Architecture -- **Topology**: Client-Server (authoritative server). Optional listen-server for quick demos. -- **Transport**: UDP-based with reliability layer (e.g., ENet-style channels) or QUIC. TCP only for control/metadata if needed. -- **Message Framing**: Binary messages with header (protocol version, msg type, seq, timestamp). -- **Serialization**: Protobuf or FlatBuffers. Keep hot-path messages minimal and fixed-size where possible. -- **Threading**: Dedicated networking thread per process; lock-free queues to/from main thread. -- **Platforms**: Abstract `INetworkTransport` with implementations for desktop; allow stubs/mocks for tests. - -### Core Concepts -- **Sessions/Rooms**: Server hosts `Session` objects; clients join via `sessionId`. -- **Connections**: State machine (Connecting, Handshaking, Active, Disconnecting). Heartbeats and timeouts. -- **Channels/QoS**: - - Reliable-ordered: joins, RPCs, inventory changes, entity spawns/despawns. - - Unreliable: input samples, state deltas, snapshots. - - Reliable-unordered: asset manifests, chunked transfers. -- **Tick Model**: Server fixed tick (e.g., 60Hz). Messages carry `serverTick` for reconciliation. - -### ECS Integration -- **NetEntityId**: Stable network id separate from local entity handles. -- **Components**: Opt-in `Replicated` tag; optional `Prediction` marker. -- **Replication**: Server builds per-client interest-filtered snapshots/deltas of component subsets. -- **Authority**: Server writes authoritative state; client writes predicted components locally. - -### Client-Side Techniques -- **Client Prediction**: Predict local player movement; buffer inputs with sequence numbers. -- **Server Reconciliation**: On authoritative state, rewind to last acked tick, reapply pending inputs. -- **Interpolation/Extrapolation**: Interpolate remote entities using buffered snapshots; moderate extrapolation for short gaps. - -### Server-Side Techniques -- **Lag Compensation**: For hitscan/server validation, rewind to client `fireTime` within bounds. -- **Interest Management**: Spatial partitioning (grid/quadtree) and relevance rules to reduce bandwidth. -- **Rate Limiting**: Per-connection budgets for bytes/tick and messages/tick. - -### Reliability, Ordering, and Flow Control -- **Sequence/Ack**: Per-channel sequence numbers; ack bitfields for fast retransmit. -- **Fragmentation/MTU**: Detect MTU, fragment large reliable messages, reassemble with timeouts. -- **Clock Sync**: Lightweight NTP-style RTT/offset estimation; expose `getNetworkTime()`. - -### Protocol and Message Schema (Illustrative) -- `Hello`, `Challenge`, `Auth`, `JoinSession`, `LeaveSession` -- `InputSample{seq, dt, axes/buttons}` -- `StateDelta{serverTick, entities:[id, changedComponents...]}` -- `Snapshot{serverTick, entities:[id, fullState...]}` -- `RPC{target, methodId, payload}` -- `Pong/Ping`, `Disconnect{reason}` - -### Security and Integrity -- **Authentication**: Token-based (opaque session token). Optional guest mode. -- **Cheat Mitigation**: Authoritative checks, sanity validation on inputs, max speeds/teleport caps. -- **Encryption**: DTLS or QUIC if available; otherwise optional libsodium box per-connection. -- **DoS Protections**: Stateless cookie during handshake, IP rate limiting, proof-of-work toggle for public servers. - -### API Surface (Engine) -- `NetworkSystem` (engine system): lifecycle, tick integration, dispatch. -- `INetworkTransport`: send/receive datagrams, time, connection events. -- `NetServer`, `NetClient`: high-level roles, session management. -- `Replicator`: builds deltas/snapshots from ECS, interest filtering. -- `PredictionManager`: client reconciliation and smoothing. -- `RpcRegistry`: register callable functions with ids; route calls. -- `Serializer`: encode/decode with chosen schema. - -### Configuration -- Protocol version, tick rate, snapshot rate, send/recv budgets, interpolation delay. -- Interest rules, reliability timeouts, max entities per packet. - -### Observability -- Counters: RTT, packet loss, goodput, resend rate, snapshot sizes, interest cull ratios. -- Logs: connection lifecycle, drops, fragmentation, unusual input patterns. -- Debug UI overlay: network graphs; server console commands. - -### Testing Strategy -- Unit: serializer roundtrips, ack/seq logic, fragmentation. -- Deterministic sims: lockstep harness with scripted RTT/loss/jitter profiles. -- Soak tests: bots moving/shooting; measure stability and memory. -- Replay: record input and snapshots, deterministic replays for repros. - -### Migration/Compatibility -- Schema versioning in headers; support minor backward compatibility window. -- Feature flags for experimental channels or message types. - -### Performance Targets (initial) -- < 80 bytes avg per entity delta per snapshot for typical scenes. -- 60Hz server tick; 20–30Hz snapshot send rate; < 120ms playable RTT. - -### Roadmap -1) Prototype transport + message framing (loopback, no encryption). -2) Add reliable/unreliable channels, seq/ack, heartbeats, time sync. -3) Basic client prediction + reconciliation for character movement. -4) Snapshot/delta replication with interest management. -5) RPC system and server commands. -6) Security hardening (auth, encryption), rate limiting. -7) Observability and tools (overlay, counters, captures). -8) QUIC transport option and listen-server mode. - -### Open Questions -- QUIC vs ENet-style UDP for first implementation? -- Protobuf vs FlatBuffers vs custom bitpacking for hot paths? -- Minimum supported platforms and NAT traversal requirements? - - From f38e0e6cae518efd00d690b0725cfa51219ba788 Mon Sep 17 00:00:00 2001 From: kipiiler Date: Mon, 13 Oct 2025 00:02:55 -0700 Subject: [PATCH 4/8] temp --- .../Systems/Networks/NetworkServerService.cpp | 0 .../Systems/Networks/NetworkServerService.hpp | 34 +++++++++++++++++++ Engine/Utils/Networks/ISocket.hpp | 17 +++++----- Engine/Utils/Networks/WindowsSocket.cpp | 20 +++++++++++ Engine/Utils/Networks/WindowsSocket.hpp | 1 + demo/network/src/main.cpp | 9 +++-- 6 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 Engine/Systems/Networks/NetworkServerService.cpp create mode 100644 Engine/Systems/Networks/NetworkServerService.hpp diff --git a/Engine/Systems/Networks/NetworkServerService.cpp b/Engine/Systems/Networks/NetworkServerService.cpp new file mode 100644 index 0000000..e69de29 diff --git a/Engine/Systems/Networks/NetworkServerService.hpp b/Engine/Systems/Networks/NetworkServerService.hpp new file mode 100644 index 0000000..4c3f0a0 --- /dev/null +++ b/Engine/Systems/Networks/NetworkServerService.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "../../Utils/Networks/ISocket.hpp" +#include +#include + +#ifdef _WIN32 +#include "../../Utils/Networks/WindowsSocket.hpp"; +#endif + +namespace Bored::Net { + +struct client_conn { + std::string address; + int port; +}; + +class Server { +public: + Server(); + ~Server(); + void Start(); + void BroadCastMessage(); + std::vector GetAllMessage(); + void Stop(); + +private: + void initSocket(); + +private: + std::shared_ptr sock_; + std::vector clients_; +}; + +} // namespace Bored::Net diff --git a/Engine/Utils/Networks/ISocket.hpp b/Engine/Utils/Networks/ISocket.hpp index 12925ec..ef7840e 100644 --- a/Engine/Utils/Networks/ISocket.hpp +++ b/Engine/Utils/Networks/ISocket.hpp @@ -6,20 +6,21 @@ namespace Bored::Net { enum Family : uint8_t { Unspec, IPv4, IPv6 }; enum Type : uint8_t { Stream, Datagram }; -enum Protocol: uint8_t { UDP, TCP }; - +enum Protocol : uint8_t { UDP, TCP }; class ISocket { public: - ISocket() = default; + ISocket() = default; virtual ~ISocket() = default; - virtual void Open(Family family, Type type, Protocol proto) = 0; - virtual void Close() = 0; - virtual void Bind( const std::string& address, const int port) = 0; - virtual void SendTo( const std::string& address, const int port, const std::string& message) = 0; - virtual std::string ReceiveFrom(std::string& out_address, int& out_port) = 0; + virtual void Open(Family family, Type type, Protocol proto) = 0; + virtual void Close() = 0; + virtual void Bind(const std::string &address, const int port) = 0; + virtual void SendTo(const std::string &address, const int port, + const std::string &message) = 0; + virtual std::string ReceiveFrom(std::string &out_address, int &out_port) = 0; + virtual bool HasReadable(int timeout_ms = 0) = 0; }; } // namespace Bored::Net diff --git a/Engine/Utils/Networks/WindowsSocket.cpp b/Engine/Utils/Networks/WindowsSocket.cpp index f92c089..5b9f338 100644 --- a/Engine/Utils/Networks/WindowsSocket.cpp +++ b/Engine/Utils/Networks/WindowsSocket.cpp @@ -145,4 +145,24 @@ std::string WindowsSocket::ReceiveFrom(std::string &out_address, return std::string(buf.data(), n); } +bool WindowsSocket::HasReadable(int timeout_ms){ + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(sock_, &rfds); + +timeval tv{}; + timeval* ptv = nullptr; + if (timeout_ms >= 0) { + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + ptv = &tv; + } + int ready = select(0, &rfds, nullptr, nullptr, ptv); + if (ready == SOCKET_ERROR) { + throw std::runtime_error("select() failed: " + getLastError()); + } + return ready > 0 && FD_ISSET(sock_, &rfds); + +}; + } // namespace Bored::Net diff --git a/Engine/Utils/Networks/WindowsSocket.hpp b/Engine/Utils/Networks/WindowsSocket.hpp index bbf38ab..41bae34 100644 --- a/Engine/Utils/Networks/WindowsSocket.hpp +++ b/Engine/Utils/Networks/WindowsSocket.hpp @@ -16,6 +16,7 @@ class WindowsSocket : public ISocket { void SendTo(const std::string &address, const int port, const std::string &message) override; std::string ReceiveFrom(std::string &out_address, int &out_port) override; + bool HasReadable(int timeout_ms = 0) override; private: SOCKET sock_ = INVALID_SOCKET; diff --git a/demo/network/src/main.cpp b/demo/network/src/main.cpp index 8186c64..d393d4b 100644 --- a/demo/network/src/main.cpp +++ b/demo/network/src/main.cpp @@ -33,10 +33,15 @@ int run_client(int port, const char *msg, const char *host) { WindowsSocket cli; cli.Open(IPv4, Datagram, UDP); cli.SendTo(host, port, msg); + bool haveSth = cli.HasReadable(); + + std::cout << "Have message?: " << haveSth << std::endl; std::cout << "Sent: '" << msg << "' to " << host << ":" << port << "\n"; std::string fromAddr; int fromPort = 0; + haveSth = cli.HasReadable(500); + std::cout << "Have message?: " << haveSth << std::endl; std::string reply = cli.ReceiveFrom(fromAddr, fromPort); std::cout << "Reply from " << fromAddr << ":" << fromPort << " -> '" << reply << "'\n"; @@ -59,8 +64,8 @@ int main(int argc, char **argv) { const char *msg = (argc > 3) ? argv[3] : "hello via ISocket"; const char *host = (argc > 4) ? argv[4] : "127.0.0.1"; - std::cout << "Using mode: " << mode << " with port " << port << "& host " << host - << "& mes " << msg << std::endl; + std::cout << "Using mode: " << mode << " with port " << port << "& host " + << host << "& mes " << msg << std::endl; if (mode == "server") { return run_server(port); From 90022aaeef4a197b90ffcc98b160ef5bee82118a Mon Sep 17 00:00:00 2001 From: kipiiler Date: Mon, 13 Oct 2025 15:30:37 -0700 Subject: [PATCH 5/8] server abstraction done --- Engine/CMakeLists.txt | 1 + .../Systems/Networks/NetworkServerService.cpp | 115 ++++++++++++++++++ .../Systems/Networks/NetworkServerService.hpp | 31 +++-- demo/network/src/main.cpp | 84 +++++++++---- 4 files changed, 199 insertions(+), 32 deletions(-) diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 7e972e8..d2e2b56 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -10,6 +10,7 @@ set(ENGINE_SOURCE_FILES Systems/Input/IOService.cpp Systems/Renderer/Shader/Shader.cpp Systems/Renderer/OGL/Renderer.cpp + Systems/Networks/NetworkServerService.cpp Components/Mesh3D.cpp # Renderer/CPU/RayTracer.cpp Utils/AssetManager.cpp diff --git a/Engine/Systems/Networks/NetworkServerService.cpp b/Engine/Systems/Networks/NetworkServerService.cpp index e69de29..0081c11 100644 --- a/Engine/Systems/Networks/NetworkServerService.cpp +++ b/Engine/Systems/Networks/NetworkServerService.cpp @@ -0,0 +1,115 @@ +#include "NetworkServerService.hpp" +#include +#include +#include +#ifdef _WIN32 +#include "../../Utils/Networks/WindowsSocket.hpp" +#endif + +namespace Bored::Net { + +Server::Server() { initSocket(); }; + +Server::~Server() { Stop(); }; + +void Server::Start(int port) { + if (running_.load()) { + return; + } + + running_ = true; + try { + sock_->Open(IPv4, Datagram, UDP); + sock_->Bind("", port); + } catch (const std::runtime_error &err) { + std::cout << "Error trying to start server: " << err.what() << std::endl; + } + + listener_ = std::thread(&Server::listenLoop, this, port); +}; + +void Server::Stop() { + if (!running_.load()) + return; + running_ = false; + if (listener_.joinable()) { + listener_.join(); + } + + sock_->Close(); +}; + +void Server::BroadCastMessage() { + +}; + +std::vector Server::GetAllMessage() { + std::lock_guard lk(q_mtx_); + std::vector out; + out.swap(mqueue_); + return out; +}; + +void Server::initSocket() { +#ifdef _WIN32 + sock_ = std::make_shared(); +#else + // TODO: extend to more than win + throw std::runtime_error("Haven't got support for current platform yet"); +#endif +}; + +void Server::listenLoop(int port) { + while (running_.load()) { + + bool have_msg = false; +#ifdef _WIN32 + auto *ws = dynamic_cast(sock_.get()); + if (ws->HasReadable(200)) { + have_msg = true; + } +#endif + + if (!running_.load()) { + break; + }; + if (!have_msg) { + continue; + } + + std::string from; + int port; + std::string payload; + try { + payload = sock_->ReceiveFrom(from, port); + } catch (const std::runtime_error &e) { + std::cout << "Recevied from error: " << e.what() << std::endl; + if (!running_.load()) { + break; + } + continue; + } + + { + std::lock_guard lk(c_mtx_); + ClientConn new_c(from, port); + bool is_added = false; + for (auto c : clients_) { + if (c.port == port && c.address == from) + is_added = true; + } + + if (!is_added) { + clients_.push_back(new_c); + }; + } + + { + std::lock_guard lk(q_mtx_); + Msg new_m(payload, port, from); + mqueue_.push_back(new_m); + } + } +}; + +} // namespace Bored::Net diff --git a/Engine/Systems/Networks/NetworkServerService.hpp b/Engine/Systems/Networks/NetworkServerService.hpp index 4c3f0a0..279d735 100644 --- a/Engine/Systems/Networks/NetworkServerService.hpp +++ b/Engine/Systems/Networks/NetworkServerService.hpp @@ -1,34 +1,47 @@ #pragma once #include "../../Utils/Networks/ISocket.hpp" +#include #include +#include +#include #include -#ifdef _WIN32 -#include "../../Utils/Networks/WindowsSocket.hpp"; -#endif - namespace Bored::Net { -struct client_conn { +struct ClientConn { std::string address; int port; }; +struct Msg { + std::string from; + int port; + std::string payload; +}; + class Server { public: Server(); ~Server(); - void Start(); - void BroadCastMessage(); - std::vector GetAllMessage(); + void Start(int port = 8080); void Stop(); + void BroadCastMessage(); + std::vector GetAllMessage(); private: void initSocket(); + void listenLoop(int port); private: std::shared_ptr sock_; - std::vector clients_; + std::vector clients_; + std::mutex c_mtx_; + + std::vector mqueue_; + std::mutex q_mtx_; + std::atomic running_; + + std::thread listener_; }; } // namespace Bored::Net diff --git a/demo/network/src/main.cpp b/demo/network/src/main.cpp index d393d4b..bb8e497 100644 --- a/demo/network/src/main.cpp +++ b/demo/network/src/main.cpp @@ -1,31 +1,69 @@ #include "Utils/Networks/WindowsSocket.hpp" +#include #include using namespace Bored::Net; int run_server(int port) { - try { - WindowsSocket srv; - srv.Open(IPv4, Datagram, UDP); - srv.Bind("", port); // "" or "0.0.0.0" listens on all interfaces - - std::cout << "UDP echo server on port " << port << "...\n"; - for (;;) { - std::string fromAddr; - int fromPort = 0; - std::string payload = srv.ReceiveFrom(fromAddr, fromPort); - std::cout << "Got " << payload.size() << " bytes from " << fromAddr << ":" - << fromPort << " -> '" << payload << "'\n"; - - // Echo it back - srv.SendTo(fromAddr, fromPort, payload); + std::shared_ptr server = std::make_shared(); + server->Start(port); + + while (true) { + char command; + std::cout << "Input your action: " << std::endl; + std::cin >> command; + + switch (command) { + case 'q': + std::cout << "Exit!"; + return 0; + + case 'e': + std::cout << "e"; + break; + + case 'g': { + std::vector msg = server->GetAllMessage(); + if (msg.size() == 0) { + std::cout << "No message found" << std::endl; + break; + }; + for (Msg m : msg) { + std::cout << m.payload << " from " << m.from << ":" << m.port + << std::endl; + }; + break; } - } catch (const std::exception &e) { - std::cerr << "Server error: " << e.what() << "\n"; - return 1; - } + + default: + std::cout << "Unrecognized command" << std::endl; + } + }; + return 0; + // try { + // WindowsSocket srv; + // srv.Open(IPv4, Datagram, UDP); + // srv.Bind("", port); // "" or "0.0.0.0" listens on all interfaces + // + // std::cout << "UDP echo server on port " << port << "...\n"; + // for (;;) { + // std::string fromAddr; + // int fromPort = 0; + // std::string payload = srv.ReceiveFrom(fromAddr, fromPort); + // std::cout << "Got " << payload.size() << " bytes from " << fromAddr << + // ":" + // << fromPort << " -> '" << payload << "'\n"; + // + // // Echo it back + // srv.SendTo(fromAddr, fromPort, payload); + // } + // } catch (const std::exception &e) { + // std::cerr << "Server error: " << e.what() << "\n"; + // return 1; + // } + // return 0; } int run_client(int port, const char *msg, const char *host) { @@ -41,10 +79,10 @@ int run_client(int port, const char *msg, const char *host) { std::string fromAddr; int fromPort = 0; haveSth = cli.HasReadable(500); - std::cout << "Have message?: " << haveSth << std::endl; - std::string reply = cli.ReceiveFrom(fromAddr, fromPort); - std::cout << "Reply from " << fromAddr << ":" << fromPort << " -> '" - << reply << "'\n"; + // std::cout << "Have message?: " << haveSth << std::endl; + // std::string reply = cli.ReceiveFrom(fromAddr, fromPort); + // std::cout << "Reply from " << fromAddr << ":" << fromPort << " -> '" + // << reply << "'\n"; } catch (const std::exception &e) { std::cerr << "Client error: " << e.what() << "\n"; return 1; From 2e8114e21bbb4cff0405714408f7b57a77bc4a9a Mon Sep 17 00:00:00 2001 From: kipiiler Date: Mon, 13 Oct 2025 18:29:51 -0700 Subject: [PATCH 6/8] add client & connect protocol --- Engine/CMakeLists.txt | 3 +- .../Systems/Networks/NetworkClientService.cpp | 135 ++++++++++++++++++ .../Systems/Networks/NetworkClientService.hpp | 38 +++++ .../Systems/Networks/NetworkServerService.cpp | 61 +++++--- .../Systems/Networks/NetworkServerService.hpp | 24 ++-- Engine/Utils/Networks/ISocket.hpp | 31 ++++ demo/network/src/main.cpp | 93 +++++++++--- 7 files changed, 333 insertions(+), 52 deletions(-) create mode 100644 Engine/Systems/Networks/NetworkClientService.cpp create mode 100644 Engine/Systems/Networks/NetworkClientService.hpp diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index d2e2b56..35605fc 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -10,8 +10,9 @@ set(ENGINE_SOURCE_FILES Systems/Input/IOService.cpp Systems/Renderer/Shader/Shader.cpp Systems/Renderer/OGL/Renderer.cpp - Systems/Networks/NetworkServerService.cpp Components/Mesh3D.cpp + Systems/Networks/NetworkServerService.cpp + Systems/Networks/NetworkClientService.cpp # Renderer/CPU/RayTracer.cpp Utils/AssetManager.cpp Utils/Networks/WindowsSocket.cpp diff --git a/Engine/Systems/Networks/NetworkClientService.cpp b/Engine/Systems/Networks/NetworkClientService.cpp new file mode 100644 index 0000000..b8b43b6 --- /dev/null +++ b/Engine/Systems/Networks/NetworkClientService.cpp @@ -0,0 +1,135 @@ +#include "NetworkClientService.hpp" +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include "../../Utils/Networks/WindowsSocket.hpp" +#endif + +namespace Bored::Net { + +Client::Client(std::string compatible_id) { + initSocket(); + compatible_id_ = compatible_id; +}; + +Client::~Client() { Disconnect(); }; + +bool Client::Connect(int port, std::string addr, int server_port) { + if (running_.load()) + return false; + + running_ = true; + + try { + + sock_->Open(IPv4, Datagram, UDP); + sock_->Bind("", port); + } catch (const std::runtime_error &err) { + std::cout << "Unable to connect: " << err.what() << std::endl; + running_ = false; + } + + server_addr = Conn(addr, server_port); + sock_->SendTo(addr, server_port, compatible_id_ + ":connect_req"); + + bool success = false; + auto start_time = std::chrono::high_resolution_clock().now(); + while (std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - start_time) + .count() < kConnectTimeOut * 1000) { + if (!sock_->HasReadable(100)) + continue; + try { + std::string ack = + sock_->ReceiveFrom(server_addr.address, server_addr.port); + if (ack == compatible_id_) { + success = true; + } + } catch (const std::runtime_error &e) { + continue; + } + } + + if (!success) { + running_ = false; + sock_->Close(); + std::cout << "Time out while connecting with server" << std::endl; + return false; + } + + listener_ = std::thread(&Client::listenLoop, this, port); + return true; +}; + +void Client::Disconnect() { + if (!running_.load()) { + return; + } + + running_ = false; + sock_->Close(); + + if (listener_.joinable()) + listener_.join(); +}; + +void Client::initSocket() { +#ifdef _WIN32 + sock_ = std::make_shared(); +#else + throw std::runtime_error("No implementation for other platform yet"); +#endif +}; + +void Client::listenLoop(int port) { + while (running_.load()) { + bool have_msg = false; +#ifdef _WIN32 + auto *ws = dynamic_cast(sock_.get()); + if (ws->HasReadable(100)) { + have_msg = true; + } +#endif + + if (!have_msg) { + continue; + }; + + std::string from, payload; + int port; + try { + payload = sock_->ReceiveFrom(from, port); + if (Conn(from, port) != server_addr) { + continue; + }; + } catch (const std::runtime_error &err) { + continue; + } + + { + std::lock_guard lk(q_mtx_); + mqueue_.push_back(payload); + } + } +}; + +std::vector Client::GetAllMessage() { + std::lock_guard lk(q_mtx_); + std::vector out; + out.swap(mqueue_); + return out; +}; + +void Client::SendToServer(std::string payload) { + try { + sock_->SendTo(server_addr.address, server_addr.port, payload); + } catch (const std::runtime_error &e) { + std::cout << "Failed to send " << e.what() << std::endl; + } +} + +} // namespace Bored::Net diff --git a/Engine/Systems/Networks/NetworkClientService.hpp b/Engine/Systems/Networks/NetworkClientService.hpp new file mode 100644 index 0000000..5096a7f --- /dev/null +++ b/Engine/Systems/Networks/NetworkClientService.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "../../Utils/Networks/ISocket.hpp" +#include +#include +#include +#include +#include + +namespace Bored::Net { + +constexpr int kConnectTimeOut = 2; // ms + +class Client { +public: + Client(std::string compatible_id = ""); + ~Client(); + + bool Connect(int port, std::string addr, int server_port); + void Disconnect(); + + void SendToServer(std::string payload); + std::vector GetAllMessage(); + +private: + void initSocket(); + void listenLoop(int port); + Conn server_addr; + std::string compatible_id_; + + std::shared_ptr sock_; + + std::vector mqueue_; + std::mutex q_mtx_; + std::atomic running_; + std::thread listener_; +}; + +} // namespace Bored::Net diff --git a/Engine/Systems/Networks/NetworkServerService.cpp b/Engine/Systems/Networks/NetworkServerService.cpp index 0081c11..9da6cf5 100644 --- a/Engine/Systems/Networks/NetworkServerService.cpp +++ b/Engine/Systems/Networks/NetworkServerService.cpp @@ -1,6 +1,7 @@ #include "NetworkServerService.hpp" #include #include +#include #include #ifdef _WIN32 #include "../../Utils/Networks/WindowsSocket.hpp" @@ -8,7 +9,10 @@ namespace Bored::Net { -Server::Server() { initSocket(); }; +Server::Server(std::string compatible_id) { + initSocket(); + compatible_id_ = compatible_id; +}; Server::~Server() { Stop(); }; @@ -39,8 +43,15 @@ void Server::Stop() { sock_->Close(); }; -void Server::BroadCastMessage() { - +void Server::BroadCastMessage(std::string payload) { + for (auto c : clients_) { + try { + sock_->SendTo(c.address, c.port, payload); + } catch (const std::runtime_error &err) { + std::cout << "Failed to broadcast: " << err.what() << std::endl; + handleFailedSendConn(c); + } + } }; std::vector Server::GetAllMessage() { @@ -50,6 +61,23 @@ std::vector Server::GetAllMessage() { return out; }; +void Server::handleNewConn(Conn conn) { + std::cout << "Recevied connect req from: " << conn.address << ":" << conn.port + << std::endl; + sock_->SendTo(conn.address, conn.port, compatible_id_); + clients_.insert(conn); +}; + +void Server::handleFailedSendConn(Conn conn) { + if (retried_.count(conn) >= kRetried) { + std::lock_guard lk(c_mtx_); + retried_.erase(conn); + clients_.erase(conn); + } + + retried_[conn]++; +} + void Server::initSocket() { #ifdef _WIN32 sock_ = std::make_shared(); @@ -92,22 +120,19 @@ void Server::listenLoop(int port) { { std::lock_guard lk(c_mtx_); - ClientConn new_c(from, port); - bool is_added = false; - for (auto c : clients_) { - if (c.port == port && c.address == from) - is_added = true; + Conn new_c(from, port); + const std::string suffix = ":connect_req"; + if (!clients_.count(new_c) && payload.ends_with(suffix)) { + std::string sent_id = payload.substr(0, payload.size() - suffix.size()); + if (sent_id == compatible_id_) { + handleNewConn(new_c); + } + } else { + if(retried_.find(new_c) != retried_.end()) {retried_.erase(new_c);}; + std::lock_guard lk(q_mtx_); + Msg new_m(from, port, payload); + mqueue_.push_back(new_m); } - - if (!is_added) { - clients_.push_back(new_c); - }; - } - - { - std::lock_guard lk(q_mtx_); - Msg new_m(payload, port, from); - mqueue_.push_back(new_m); } } }; diff --git a/Engine/Systems/Networks/NetworkServerService.hpp b/Engine/Systems/Networks/NetworkServerService.hpp index 279d735..5a1334b 100644 --- a/Engine/Systems/Networks/NetworkServerService.hpp +++ b/Engine/Systems/Networks/NetworkServerService.hpp @@ -4,37 +4,34 @@ #include #include #include +#include +#include #include namespace Bored::Net { -struct ClientConn { - std::string address; - int port; -}; - -struct Msg { - std::string from; - int port; - std::string payload; -}; +constexpr int kRetried = 3; class Server { public: - Server(); + Server(std::string compatible_id = ""); ~Server(); void Start(int port = 8080); void Stop(); - void BroadCastMessage(); + void BroadCastMessage(std::string payload); std::vector GetAllMessage(); private: void initSocket(); void listenLoop(int port); + void handleNewConn(Conn conn); + void handleFailedSendConn(Conn conn); + private: + std::string compatible_id_; std::shared_ptr sock_; - std::vector clients_; + std::unordered_set clients_; std::mutex c_mtx_; std::vector mqueue_; @@ -42,6 +39,7 @@ class Server { std::atomic running_; std::thread listener_; + std::unordered_map retried_; }; } // namespace Bored::Net diff --git a/Engine/Utils/Networks/ISocket.hpp b/Engine/Utils/Networks/ISocket.hpp index ef7840e..4598c23 100644 --- a/Engine/Utils/Networks/ISocket.hpp +++ b/Engine/Utils/Networks/ISocket.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include namespace Bored::Net { @@ -8,6 +9,22 @@ enum Family : uint8_t { Unspec, IPv4, IPv6 }; enum Type : uint8_t { Stream, Datagram }; enum Protocol : uint8_t { UDP, TCP }; +struct Conn { + std::string address; + int port; + + bool operator==(const Conn &other) const noexcept { + return address == other.address && port == other.port; + } +}; + + +struct Msg { + std::string from; + int port; + std::string payload; +}; + class ISocket { public: @@ -24,3 +41,17 @@ class ISocket { }; } // namespace Bored::Net + + +// hash +namespace std { +template<> +struct hash { + size_t operator()(const Bored::Net::Conn& c) const noexcept { + size_t h1 = hash{}(c.address); + size_t h2 = hash{}(c.port); + // Better combine than simple XOR: + return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); + } +}; +} // namespace std diff --git a/demo/network/src/main.cpp b/demo/network/src/main.cpp index bb8e497..a683573 100644 --- a/demo/network/src/main.cpp +++ b/demo/network/src/main.cpp @@ -1,6 +1,8 @@ #include "Utils/Networks/WindowsSocket.hpp" +#include #include #include +#include using namespace Bored::Net; @@ -36,6 +38,12 @@ int run_server(int port) { break; } + case 's': { + std::string msg = "Broadcasting"; + server->BroadCastMessage(msg); + break; + } + default: std::cout << "Unrecognized command" << std::endl; } @@ -67,27 +75,72 @@ int run_server(int port) { } int run_client(int port, const char *msg, const char *host) { - try { - WindowsSocket cli; - cli.Open(IPv4, Datagram, UDP); - cli.SendTo(host, port, msg); - bool haveSth = cli.HasReadable(); - - std::cout << "Have message?: " << haveSth << std::endl; - std::cout << "Sent: '" << msg << "' to " << host << ":" << port << "\n"; - - std::string fromAddr; - int fromPort = 0; - haveSth = cli.HasReadable(500); - // std::cout << "Have message?: " << haveSth << std::endl; - // std::string reply = cli.ReceiveFrom(fromAddr, fromPort); - // std::cout << "Reply from " << fromAddr << ":" << fromPort << " -> '" - // << reply << "'\n"; - } catch (const std::exception &e) { - std::cerr << "Client error: " << e.what() << "\n"; - return 1; - } + + std::shared_ptr client = std::make_shared(); + bool ok = client->Connect(9000, host, port); + if (!ok) { + return 0; + }; + + while (true) { + char command; + std::cout << "Input your action: " << std::endl; + std::cin >> command; + + switch (command) { + case 'q': + std::cout << "Exit!"; + return 0; + + case 'e': + std::cout << "e"; + break; + + case 'g': { + std::vector msg = client->GetAllMessage(); + if (msg.size() == 0) { + std::cout << "No message found" << std::endl; + break; + }; + for (auto m : msg) { + std::cout << "Received: " << m << std::endl; + }; + break; + } + + case 's': { + std::string msg = "Boom!"; + client->SendToServer(msg); + break; + } + + default: + std::cout << "Unrecognized command" << std::endl; + } + }; + return 0; + // try { + // WindowsSocket cli; + // cli.Open(IPv4, Datagram, UDP); + // cli.SendTo(host, port, msg); + // bool haveSth = cli.HasReadable(); + // + // std::cout << "Have message?: " << haveSth << std::endl; + // std::cout << "Sent: '" << msg << "' to " << host << ":" << port << "\n"; + // + // std::string fromAddr; + // int fromPort = 0; + // haveSth = cli.HasReadable(500); + // // std::cout << "Have message?: " << haveSth << std::endl; + // // std::string reply = cli.ReceiveFrom(fromAddr, fromPort); + // // std::cout << "Reply from " << fromAddr << ":" << fromPort << " -> '" + // // << reply << "'\n"; + // } catch (const std::exception &e) { + // std::cerr << "Client error: " << e.what() << "\n"; + // return 1; + // } + // return 0; } int main(int argc, char **argv) { From 7c6c2e6e80aad72a76e5bd55e3c0d27543d7cffe Mon Sep 17 00:00:00 2001 From: kipiiler Date: Mon, 13 Oct 2025 18:49:54 -0700 Subject: [PATCH 7/8] ? --- .../Systems/Networks/NetworkServerService.cpp | 4 +- Engine/Utils/Networks/ISocket.hpp | 1 - demo/network/src/main.cpp | 51 ++----------------- 3 files changed, 7 insertions(+), 49 deletions(-) diff --git a/Engine/Systems/Networks/NetworkServerService.cpp b/Engine/Systems/Networks/NetworkServerService.cpp index 9da6cf5..f6918b9 100644 --- a/Engine/Systems/Networks/NetworkServerService.cpp +++ b/Engine/Systems/Networks/NetworkServerService.cpp @@ -128,7 +128,9 @@ void Server::listenLoop(int port) { handleNewConn(new_c); } } else { - if(retried_.find(new_c) != retried_.end()) {retried_.erase(new_c);}; + if (retried_.find(new_c) != retried_.end()) { + retried_.erase(new_c); + }; std::lock_guard lk(q_mtx_); Msg new_m(from, port, payload); mqueue_.push_back(new_m); diff --git a/Engine/Utils/Networks/ISocket.hpp b/Engine/Utils/Networks/ISocket.hpp index 4598c23..8cdc49e 100644 --- a/Engine/Utils/Networks/ISocket.hpp +++ b/Engine/Utils/Networks/ISocket.hpp @@ -50,7 +50,6 @@ struct hash { size_t operator()(const Bored::Net::Conn& c) const noexcept { size_t h1 = hash{}(c.address); size_t h2 = hash{}(c.port); - // Better combine than simple XOR: return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); } }; diff --git a/demo/network/src/main.cpp b/demo/network/src/main.cpp index a683573..cbe1b74 100644 --- a/demo/network/src/main.cpp +++ b/demo/network/src/main.cpp @@ -50,28 +50,6 @@ int run_server(int port) { }; return 0; - // try { - // WindowsSocket srv; - // srv.Open(IPv4, Datagram, UDP); - // srv.Bind("", port); // "" or "0.0.0.0" listens on all interfaces - // - // std::cout << "UDP echo server on port " << port << "...\n"; - // for (;;) { - // std::string fromAddr; - // int fromPort = 0; - // std::string payload = srv.ReceiveFrom(fromAddr, fromPort); - // std::cout << "Got " << payload.size() << " bytes from " << fromAddr << - // ":" - // << fromPort << " -> '" << payload << "'\n"; - // - // // Echo it back - // srv.SendTo(fromAddr, fromPort, payload); - // } - // } catch (const std::exception &e) { - // std::cerr << "Server error: " << e.what() << "\n"; - // return 1; - // } - // return 0; } int run_client(int port, const char *msg, const char *host) { @@ -109,10 +87,10 @@ int run_client(int port, const char *msg, const char *host) { } case 's': { - std::string msg = "Boom!"; - client->SendToServer(msg); - break; - } + std::string msg = "Boom!"; + client->SendToServer(msg); + break; + } default: std::cout << "Unrecognized command" << std::endl; @@ -120,27 +98,6 @@ int run_client(int port, const char *msg, const char *host) { }; return 0; - // try { - // WindowsSocket cli; - // cli.Open(IPv4, Datagram, UDP); - // cli.SendTo(host, port, msg); - // bool haveSth = cli.HasReadable(); - // - // std::cout << "Have message?: " << haveSth << std::endl; - // std::cout << "Sent: '" << msg << "' to " << host << ":" << port << "\n"; - // - // std::string fromAddr; - // int fromPort = 0; - // haveSth = cli.HasReadable(500); - // // std::cout << "Have message?: " << haveSth << std::endl; - // // std::string reply = cli.ReceiveFrom(fromAddr, fromPort); - // // std::cout << "Reply from " << fromAddr << ":" << fromPort << " -> '" - // // << reply << "'\n"; - // } catch (const std::exception &e) { - // std::cerr << "Client error: " << e.what() << "\n"; - // return 1; - // } - // return 0; } int main(int argc, char **argv) { From a098672fde207ad91fbd6c5890f6cfee6fe641eb Mon Sep 17 00:00:00 2001 From: kipiiler Date: Mon, 13 Oct 2025 18:57:29 -0700 Subject: [PATCH 8/8] remove --- Engine/Utils/Networks/ISocket.cpp | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Engine/Utils/Networks/ISocket.cpp diff --git a/Engine/Utils/Networks/ISocket.cpp b/Engine/Utils/Networks/ISocket.cpp deleted file mode 100644 index e69de29..0000000