diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 11a0e2b..35605fc 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -11,8 +11,11 @@ set(ENGINE_SOURCE_FILES Systems/Renderer/Shader/Shader.cpp Systems/Renderer/OGL/Renderer.cpp Components/Mesh3D.cpp + Systems/Networks/NetworkServerService.cpp + Systems/Networks/NetworkClientService.cpp # Renderer/CPU/RayTracer.cpp Utils/AssetManager.cpp + Utils/Networks/WindowsSocket.cpp ) add_library(Engine ${ENGINE_SOURCE_FILES}) @@ -23,6 +26,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/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 new file mode 100644 index 0000000..f6918b9 --- /dev/null +++ b/Engine/Systems/Networks/NetworkServerService.cpp @@ -0,0 +1,142 @@ +#include "NetworkServerService.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include "../../Utils/Networks/WindowsSocket.hpp" +#endif + +namespace Bored::Net { + +Server::Server(std::string compatible_id) { + initSocket(); + compatible_id_ = compatible_id; +}; + +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::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() { + std::lock_guard lk(q_mtx_); + std::vector out; + out.swap(mqueue_); + 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(); +#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_); + 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); + } + } + } +}; + +} // namespace Bored::Net diff --git a/Engine/Systems/Networks/NetworkServerService.hpp b/Engine/Systems/Networks/NetworkServerService.hpp new file mode 100644 index 0000000..5a1334b --- /dev/null +++ b/Engine/Systems/Networks/NetworkServerService.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "../../Utils/Networks/ISocket.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace Bored::Net { + +constexpr int kRetried = 3; + +class Server { +public: + Server(std::string compatible_id = ""); + ~Server(); + void Start(int port = 8080); + void Stop(); + 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::unordered_set clients_; + std::mutex c_mtx_; + + std::vector mqueue_; + std::mutex q_mtx_; + 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 new file mode 100644 index 0000000..8cdc49e --- /dev/null +++ b/Engine/Utils/Networks/ISocket.hpp @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include + +namespace Bored::Net { + +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: + 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 bool HasReadable(int timeout_ms = 0) = 0; +}; + +} // 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); + return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); + } +}; +} // namespace std diff --git a/Engine/Utils/Networks/WindowsSocket.cpp b/Engine/Utils/Networks/WindowsSocket.cpp new file mode 100644 index 0000000..5b9f338 --- /dev/null +++ b/Engine/Utils/Networks/WindowsSocket.cpp @@ -0,0 +1,168 @@ +#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()); +} + +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) { + 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) { + 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); +} + +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 new file mode 100644 index 0000000..41bae34 --- /dev/null +++ b/Engine/Utils/Networks/WindowsSocket.hpp @@ -0,0 +1,31 @@ +#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; + bool HasReadable(int timeout_ms = 0) 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..cbe1b74 --- /dev/null +++ b/demo/network/src/main.cpp @@ -0,0 +1,124 @@ +#include "Utils/Networks/WindowsSocket.hpp" +#include +#include +#include +#include + +using namespace Bored::Net; + +int run_server(int port) { + + 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; + } + + case 's': { + std::string msg = "Broadcasting"; + server->BroadCastMessage(msg); + break; + } + + default: + std::cout << "Unrecognized command" << std::endl; + } + }; + + return 0; +} + +int run_client(int port, const char *msg, const char *host) { + + 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; +} + +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; +}