/* ASIO Based Networking olcPixelGameEngine Extension v1.0 Videos: Part #1: https://youtu.be/2hNdkYInj4g Part #2: https://youtu.be/UbjxGvrDrbw Part #3: https://youtu.be/hHowZ3bWsio Part #4: https://youtu.be/f_1lt9pfaEo License (OLC-3) ~~~~~~~~~~~~~~~ Copyright 2018 - 2021 OneLoneCoder.com Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions or derivations of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions or derivative works in binary form must reproduce the above copyright notice. This list of conditions and the following disclaimer must be reproduced in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Links ~~~~~ YouTube: https://www.youtube.com/javidx9 Discord: https://discord.gg/WhwHUMV Twitter: https://www.twitter.com/javidx9 Twitch: https://www.twitch.tv/javidx9 GitHub: https://www.github.com/onelonecoder Homepage: https://www.onelonecoder.com Author ~~~~~~ David Barr, aka javidx9, ŠOneLoneCoder 2019, 2020, 2021 */ #pragma once #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0A00 #endif #endif #define _WINSOCK_DEPRECATED_NO_WARNINGS #define ASIO_STANDALONE #include #include #include namespace olc { namespace net { // Message // Message Header is sent at start of all messages. The template allows us // to use "enum class" to ensure that the messages are valid at compile time template struct message_header { T id{}; uint32_t size = 0; }; // Message Body contains a header and a std::vector, containing raw bytes // of infomation. This way the message can be variable length, but the size // in the header must be updated. template struct message { // Header & Body vector message_header header{}; std::vector body; // returns size of entire message packet in bytes size_t size() const { return body.size(); } // Override for std::cout compatibility - produces friendly description of message friend std::ostream& operator << (std::ostream& os, const message& msg) { os << "ID:" << int(msg.header.id) << " Size:" << msg.header.size; return os; } // Convenience Operator overloads - These allow us to add and remove stuff from // the body vector as if it were a stack, so First in, Last Out. These are a // template in itself, because we dont know what data type the user is pushing or // popping, so lets allow them all. NOTE: It assumes the data type is fundamentally // Plain Old Data (POD). TLDR: Serialise & Deserialise into/from a vector // Pushes any POD-like data into the message buffer template friend message& operator << (message& msg, const DataType& data) { // Check that the type of the data being pushed is trivially copyable static_assert(std::is_standard_layout::value, "Data is too complex to be pushed into vector"); // Cache current size of vector, as this will be the point we insert the data size_t i = msg.body.size(); // Resize the vector by the size of the data being pushed msg.body.resize(msg.body.size() + sizeof(DataType)); // Physically copy the data into the newly allocated vector space std::memcpy(msg.body.data() + i, &data, sizeof(DataType)); // Recalculate the message size msg.header.size = msg.size(); // Return the target message so it can be "chained" return msg; } // Pulls any POD-like data form the message buffer template friend message& operator >> (message& msg, DataType& data) { // Check that the type of the data being pushed is trivially copyable static_assert(std::is_standard_layout::value, "Data is too complex to be pulled from vector"); // Cache the location towards the end of the vector where the pulled data starts size_t i = msg.body.size() - sizeof(DataType); // Physically copy the data from the vector into the user variable std::memcpy(&data, msg.body.data() + i, sizeof(DataType)); // Shrink the vector to remove read bytes, and reset end position msg.body.resize(i); // Recalculate the message size msg.header.size = msg.size(); // Return the target message so it can be "chained" return msg; } }; // An "owned" message is identical to a regular message, but it is associated with // a connection. On a server, the owner would be the client that sent the message, // on a client the owner would be the server. // Forward declare the connection template class connection; template struct owned_message { std::shared_ptr> remote = nullptr; message msg; // Again, a friendly string maker friend std::ostream& operator<<(std::ostream& os, const owned_message& msg) { os << msg.msg; return os; } }; // Queue template class tsqueue { public: tsqueue() = default; tsqueue(const tsqueue&) = delete; virtual ~tsqueue() { clear(); } public: // Returns and maintains item at front of Queue const T& front() { std::scoped_lock lock(muxQueue); return deqQueue.front(); } // Returns and maintains item at back of Queue const T& back() { std::scoped_lock lock(muxQueue); return deqQueue.back(); } // Removes and returns item from front of Queue T pop_front() { std::scoped_lock lock(muxQueue); auto t = std::move(deqQueue.front()); deqQueue.pop_front(); return t; } // Removes and returns item from back of Queue T pop_back() { std::scoped_lock lock(muxQueue); auto t = std::move(deqQueue.back()); deqQueue.pop_back(); return t; } // Adds an item to back of Queue void push_back(const T& item) { std::scoped_lock lock(muxQueue); deqQueue.emplace_back(std::move(item)); std::unique_lock ul(muxBlocking); cvBlocking.notify_one(); } // Adds an item to front of Queue void push_front(const T& item) { std::scoped_lock lock(muxQueue); deqQueue.emplace_front(std::move(item)); std::unique_lock ul(muxBlocking); cvBlocking.notify_one(); } // Returns true if Queue has no items bool empty() { std::scoped_lock lock(muxQueue); return deqQueue.empty(); } // Returns number of items in Queue size_t count() { std::scoped_lock lock(muxQueue); return deqQueue.size(); } // Clears Queue void clear() { std::scoped_lock lock(muxQueue); deqQueue.clear(); } void wait() { while (empty()) { std::unique_lock ul(muxBlocking); cvBlocking.wait(ul); } } protected: std::mutex muxQueue; std::deque deqQueue; std::condition_variable cvBlocking; std::mutex muxBlocking; }; // Connection // Forward declare template class server_interface; template class connection : public std::enable_shared_from_this> { public: // A connection is "owned" by either a server or a client, and its // behaviour is slightly different bewteen the two. enum class owner { server, client }; public: // Constructor: Specify Owner, connect to context, transfer the socket // Provide reference to incoming message queue connection(owner parent, asio::io_context& asioContext, asio::ip::tcp::socket socket, tsqueue>& qIn) : m_asioContext(asioContext), m_socket(std::move(socket)), m_qMessagesIn(qIn) { m_nOwnerType = parent; // Construct validation check data if (m_nOwnerType == owner::server) { // Connection is Server -> Client, construct random data for the client // to transform and send back for validation m_nHandshakeOut = uint64_t(std::chrono::system_clock::now().time_since_epoch().count()); // Pre-calculate the result for checking when the client responds m_nHandshakeCheck = scramble(m_nHandshakeOut); } else { // Connection is Client -> Server, so we have nothing to define, m_nHandshakeIn = 0; m_nHandshakeOut = 0; } } virtual ~connection() {} // This ID is used system wide - its how clients will understand other clients // exist across the whole system. uint32_t GetID() const { return id; } public: void ConnectToClient(olc::net::server_interface* server, uint32_t uid = 0) { if (m_nOwnerType == owner::server) { if (m_socket.is_open()) { id = uid; // Was: ReadHeader(); // A client has attempted to connect to the server, but we wish // the client to first validate itself, so first write out the // handshake data to be validated WriteValidation(); // Next, issue a task to sit and wait asynchronously for precisely // the validation data sent back from the client ReadValidation(server); } } } void ConnectToServer(const asio::ip::tcp::resolver::results_type& endpoints) { // Only clients can connect to servers if (m_nOwnerType == owner::client) { // Request asio attempts to connect to an endpoint asio::async_connect(m_socket, endpoints, [this](std::error_code ec, asio::ip::tcp::endpoint endpoint) { if (!ec) { // Was: ReadHeader(); // First thing server will do is send packet to be validated // so wait for that and respond ReadValidation(); } }); } } void Disconnect() { if (IsConnected()) asio::post(m_asioContext, [this]() { m_socket.close(); }); } bool IsConnected() const { return m_socket.is_open(); } // Prime the connection to wait for incoming messages void StartListening() { } public: // ASYNC - Send a message, connections are one-to-one so no need to specifiy // the target, for a client, the target is the server and vice versa void Send(const message& msg) { asio::post(m_asioContext, [this, msg]() { // If the queue has a message in it, then we must // assume that it is in the process of asynchronously being written. // Either way add the message to the queue to be output. If no messages // were available to be written, then start the process of writing the // message at the front of the queue. bool bWritingMessage = !m_qMessagesOut.empty(); m_qMessagesOut.push_back(msg); if (!bWritingMessage) { WriteHeader(); } }); } private: // ASYNC - Prime context to write a message header void WriteHeader() { // If this function is called, we know the outgoing message queue must have // at least one message to send. So allocate a transmission buffer to hold // the message, and issue the work - asio, send these bytes asio::async_write(m_socket, asio::buffer(&m_qMessagesOut.front().header, sizeof(message_header)), [this](std::error_code ec, std::size_t length) { // asio has now sent the bytes - if there was a problem // an error would be available... if (!ec) { // ... no error, so check if the message header just sent also // has a message body... if (m_qMessagesOut.front().body.size() > 0) { // ...it does, so issue the task to write the body bytes WriteBody(); } else { // ...it didnt, so we are done with this message. Remove it from // the outgoing message queue m_qMessagesOut.pop_front(); // If the queue is not empty, there are more messages to send, so // make this happen by issuing the task to send the next header. if (!m_qMessagesOut.empty()) { WriteHeader(); } } } else { // ...asio failed to write the message, we could analyse why but // for now simply assume the connection has died by closing the // socket. When a future attempt to write to this client fails due // to the closed socket, it will be tidied up. std::cout << "[" << id << "] Write Header Fail.\n"; m_socket.close(); } }); } // ASYNC - Prime context to write a message body void WriteBody() { // If this function is called, a header has just been sent, and that header // indicated a body existed for this message. Fill a transmission buffer // with the body data, and send it! asio::async_write(m_socket, asio::buffer(m_qMessagesOut.front().body.data(), m_qMessagesOut.front().body.size()), [this](std::error_code ec, std::size_t length) { if (!ec) { // Sending was successful, so we are done with the message // and remove it from the queue m_qMessagesOut.pop_front(); // If the queue still has messages in it, then issue the task to // send the next messages' header. if (!m_qMessagesOut.empty()) { WriteHeader(); } } else { // Sending failed, see WriteHeader() equivalent for description :P std::cout << "[" << id << "] Write Body Fail.\n"; m_socket.close(); } }); } // ASYNC - Prime context ready to read a message header void ReadHeader() { // If this function is called, we are expecting asio to wait until it receives // enough bytes to form a header of a message. We know the headers are a fixed // size, so allocate a transmission buffer large enough to store it. In fact, // we will construct the message in a "temporary" message object as it's // convenient to work with. asio::async_read(m_socket, asio::buffer(&m_msgTemporaryIn.header, sizeof(message_header)), [this](std::error_code ec, std::size_t length) { if (!ec) { // A complete message header has been read, check if this message // has a body to follow... if (m_msgTemporaryIn.header.size > 0) { // ...it does, so allocate enough space in the messages' body // vector, and issue asio with the task to read the body. m_msgTemporaryIn.body.resize(m_msgTemporaryIn.header.size); ReadBody(); } else { // it doesn't, so add this bodyless message to the connections // incoming message queue AddToIncomingMessageQueue(); } } else { // Reading form the client went wrong, most likely a disconnect // has occurred. Close the socket and let the system tidy it up later. std::cout << "[" << id << "] Read Header Fail.\n"; m_socket.close(); } }); } // ASYNC - Prime context ready to read a message body void ReadBody() { // If this function is called, a header has already been read, and that header // request we read a body, The space for that body has already been allocated // in the temporary message object, so just wait for the bytes to arrive... asio::async_read(m_socket, asio::buffer(m_msgTemporaryIn.body.data(), m_msgTemporaryIn.body.size()), [this](std::error_code ec, std::size_t length) { if (!ec) { // ...and they have! The message is now complete, so add // the whole message to incoming queue AddToIncomingMessageQueue(); } else { // As above! std::cout << "[" << id << "] Read Body Fail.\n"; m_socket.close(); } }); } // "Encrypt" data uint64_t scramble(uint64_t nInput) { uint64_t out = nInput ^ 0xDEADBEEFC0DECAFE; out = (out & 0xF0F0F0F0F0F0F0) >> 4 | (out & 0x0F0F0F0F0F0F0F) << 4; return out ^ 0xC0DEFACE12345678; } // ASYNC - Used by both client and server to write validation packet void WriteValidation() { asio::async_write(m_socket, asio::buffer(&m_nHandshakeOut, sizeof(uint64_t)), [this](std::error_code ec, std::size_t length) { if (!ec) { // Validation data sent, clients should sit and wait // for a response (or a closure) if (m_nOwnerType == owner::client) ReadHeader(); } else { m_socket.close(); } }); } void ReadValidation(olc::net::server_interface* server = nullptr) { asio::async_read(m_socket, asio::buffer(&m_nHandshakeIn, sizeof(uint64_t)), [this, server](std::error_code ec, std::size_t length) { if (!ec) { if (m_nOwnerType == owner::server) { // Connection is a server, so check response from client // Compare sent data to actual solution if (m_nHandshakeIn == m_nHandshakeCheck) { // Client has provided valid solution, so allow it to connect properly std::cout << "Client Validated" << std::endl; server->OnClientValidated(this->shared_from_this()); // Sit waiting to receive data now ReadHeader(); } else { // Client gave incorrect data, so disconnect std::cout << "Client Disconnected (Fail Validation)" << std::endl; m_socket.close(); } } else { // Connection is a client, so solve puzzle m_nHandshakeOut = scramble(m_nHandshakeIn); // Write the result WriteValidation(); } } else { // Some biggerfailure occured std::cout << "Client Disconnected (ReadValidation)" << std::endl; m_socket.close(); } }); } // Once a full message is received, add it to the incoming queue void AddToIncomingMessageQueue() { // Shove it in queue, converting it to an "owned message", by initialising // with the a shared pointer from this connection object if(m_nOwnerType == owner::server) m_qMessagesIn.push_back({ this->shared_from_this(), m_msgTemporaryIn }); else m_qMessagesIn.push_back({ nullptr, m_msgTemporaryIn }); // We must now prime the asio context to receive the next message. It // wil just sit and wait for bytes to arrive, and the message construction // process repeats itself. Clever huh? ReadHeader(); } protected: // Each connection has a unique socket to a remote asio::ip::tcp::socket m_socket; // This context is shared with the whole asio instance asio::io_context& m_asioContext; // This queue holds all messages to be sent to the remote side // of this connection tsqueue> m_qMessagesOut; // This references the incoming queue of the parent object tsqueue>& m_qMessagesIn; // Incoming messages are constructed asynchronously, so we will // store the part assembled message here, until it is ready message m_msgTemporaryIn; // The "owner" decides how some of the connection behaves owner m_nOwnerType = owner::server; // Handshake Validation uint64_t m_nHandshakeOut = 0; uint64_t m_nHandshakeIn = 0; uint64_t m_nHandshakeCheck = 0; bool m_bValidHandshake = false; bool m_bConnectionEstablished = false; uint32_t id = 0; }; // Client template class client_interface { public: client_interface() {} virtual ~client_interface() { // If the client is destroyed, always try and disconnect from server Disconnect(); } public: // Connect to server with hostname/ip-address and port bool Connect(const std::string& host, const uint16_t port) { try { // Resolve hostname/ip-address into tangiable physical address asio::ip::tcp::resolver resolver(m_context); asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, std::to_string(port)); // Create connection m_connection = std::make_unique>(connection::owner::client, m_context, asio::ip::tcp::socket(m_context), m_qMessagesIn); // Tell the connection object to connect to server m_connection->ConnectToServer(endpoints); // Start Context Thread thrContext = std::thread([this]() { m_context.run(); }); } catch (std::exception& e) { std::cerr << "Client Exception: " << e.what() << "\n"; return false; } return true; } // Disconnect from server void Disconnect() { // If connection exists, and it's connected then... if(IsConnected()) { // ...disconnect from server gracefully m_connection->Disconnect(); } // Either way, we're also done with the asio context... m_context.stop(); // ...and its thread if (thrContext.joinable()) thrContext.join(); // Destroy the connection object m_connection.release(); } // Check if client is actually connected to a server bool IsConnected() { if (m_connection) return m_connection->IsConnected(); else return false; } public: // Send message to server void Send(const message& msg) { if (IsConnected()) m_connection->Send(msg); } // Retrieve queue of messages from server tsqueue>& Incoming() { return m_qMessagesIn; } protected: // asio context handles the data transfer... asio::io_context m_context; // ...but needs a thread of its own to execute its work commands std::thread thrContext; // The client has a single instance of a "connection" object, which handles data transfer std::unique_ptr> m_connection; private: // This is the thread safe queue of incoming messages from server tsqueue> m_qMessagesIn; }; // Server template class server_interface { public: // Create a server, ready to listen on specified port server_interface(uint16_t port) : m_asioAcceptor(m_asioContext, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)) { } virtual ~server_interface() { // May as well try and tidy up Stop(); } // Starts the server! bool Start() { try { // Issue a task to the asio context - This is important // as it will prime the context with "work", and stop it // from exiting immediately. Since this is a server, we // want it primed ready to handle clients trying to // connect. WaitForClientConnection(); // Launch the asio context in its own thread m_threadContext = std::thread([this]() { m_asioContext.run(); }); } catch (std::exception& e) { // Something prohibited the server from listening std::cerr << "[SERVER] Exception: " << e.what() << "\n"; return false; } std::cout << "[SERVER] Started!\n"; return true; } // Stops the server! void Stop() { // Request the context to close m_asioContext.stop(); // Tidy up the context thread if (m_threadContext.joinable()) m_threadContext.join(); // Inform someone, anybody, if they care... std::cout << "[SERVER] Stopped!\n"; } // ASYNC - Instruct asio to wait for connection void WaitForClientConnection() { // Prime context with an instruction to wait until a socket connects. This // is the purpose of an "acceptor" object. It will provide a unique socket // for each incoming connection attempt m_asioAcceptor.async_accept( [this](std::error_code ec, asio::ip::tcp::socket socket) { // Triggered by incoming connection request if (!ec) { // Display some useful(?) information std::cout << "[SERVER] New Connection: " << socket.remote_endpoint() << "\n"; // Create a new connection to handle this client std::shared_ptr> newconn = std::make_shared>(connection::owner::server, m_asioContext, std::move(socket), m_qMessagesIn); // Give the user server a chance to deny connection if (OnClientConnect(newconn)) { // Connection allowed, so add to container of new connections m_deqConnections.push_back(std::move(newconn)); // And very important! Issue a task to the connection's // asio context to sit and wait for bytes to arrive! m_deqConnections.back()->ConnectToClient(this, nIDCounter++); std::cout << "[" << m_deqConnections.back()->GetID() << "] Connection Approved\n"; } else { std::cout << "[-----] Connection Denied\n"; // Connection will go out of scope with no pending tasks, so will // get destroyed automagically due to the wonder of smart pointers } } else { // Error has occurred during acceptance std::cout << "[SERVER] New Connection Error: " << ec.message() << "\n"; } // Prime the asio context with more work - again simply wait for // another connection... WaitForClientConnection(); }); } // Send a message to a specific client void MessageClient(std::shared_ptr> client, const message& msg) { // Check client is legitimate... if (client && client->IsConnected()) { // ...and post the message via the connection client->Send(msg); } else { // If we cant communicate with client then we may as // well remove the client - let the server know, it may // be tracking it somehow OnClientDisconnect(client); // Off you go now, bye bye! client.reset(); // Then physically remove it from the container m_deqConnections.erase( std::remove(m_deqConnections.begin(), m_deqConnections.end(), client), m_deqConnections.end()); } } // Send message to all clients void MessageAllClients(const message& msg, std::shared_ptr> pIgnoreClient = nullptr) { bool bInvalidClientExists = false; // Iterate through all clients in container for (auto& client : m_deqConnections) { // Check client is connected... if (client && client->IsConnected()) { // ..it is! if(client != pIgnoreClient) client->Send(msg); } else { // The client couldnt be contacted, so assume it has // disconnected. OnClientDisconnect(client); client.reset(); // Set this flag to then remove dead clients from container bInvalidClientExists = true; } } // Remove dead clients, all in one go - this way, we dont invalidate the // container as we iterated through it. if (bInvalidClientExists) m_deqConnections.erase( std::remove(m_deqConnections.begin(), m_deqConnections.end(), nullptr), m_deqConnections.end()); } // Force server to respond to incoming messages void Update(size_t nMaxMessages = -1, bool bWait = false) { if (bWait) m_qMessagesIn.wait(); // Process as many messages as you can up to the value // specified size_t nMessageCount = 0; while (nMessageCount < nMaxMessages && !m_qMessagesIn.empty()) { // Grab the front message auto msg = m_qMessagesIn.pop_front(); // Pass to message handler OnMessage(msg.remote, msg.msg); nMessageCount++; } } protected: // This server class should override thse functions to implement // customised functionality // Called when a client connects, you can veto the connection by returning false virtual bool OnClientConnect(std::shared_ptr> client) { return false; } // Called when a client appears to have disconnected virtual void OnClientDisconnect(std::shared_ptr> client) { } // Called when a message arrives virtual void OnMessage(std::shared_ptr> client, message& msg) { } public: // Called when a client is validated virtual void OnClientValidated(std::shared_ptr> client) { } protected: // Thread Safe Queue for incoming message packets tsqueue> m_qMessagesIn; // Container of active validated connections std::deque>> m_deqConnections; // Order of declaration is important - it is also the order of initialisation asio::io_context m_asioContext; std::thread m_threadContext; // These things need an asio context asio::ip::tcp::acceptor m_asioAcceptor; // Handles new incoming connection attempts... // Clients will be identified in the "wider system" via an ID uint32_t nIDCounter = 10000; }; } }