/* | |||||
MMO Client/Server Framework using ASIO | |||||
"Happy Birthday Mrs Javidx9!" - javidx9 | |||||
Videos: | |||||
Part #1: https://youtu.be/2hNdkYInj4g | |||||
Part #2: https://youtu.be/UbjxGvrDrbw | |||||
License (OLC-3) | |||||
~~~~~~~~~~~~~~~ | |||||
Copyright 2018 - 2020 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 | |||||
*/ | |||||
#pragma once | |||||
#include "net_common.h" | |||||
#include "net_connection.h" | |||||
namespace net | |||||
{ | |||||
template <typename T> | |||||
class ClientInterface | |||||
{ | |||||
public: | |||||
ClientInterface() | |||||
{} | |||||
virtual ~ClientInterface() | |||||
{ | |||||
// 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<T>>(Connection<T>::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<T>& msg) | |||||
{ | |||||
if (IsConnected()) | |||||
m_connection->Send(msg); | |||||
} | |||||
// Retrieve queue of messages from server | |||||
net::ts_dequeue<OwnedMessage<T>>& 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<Connection<T>> m_connection; | |||||
private: | |||||
// This is the thread safe queue of incoming messages from server | |||||
net::ts_dequeue<OwnedMessage<T>> m_qMessagesIn; | |||||
}; | |||||
} |
/* | |||||
MMO Client/Server Framework using ASIO | |||||
"Happy Birthday Mrs Javidx9!" - javidx9 | |||||
Videos: | |||||
Part #1: https://youtu.be/2hNdkYInj4g | |||||
Part #2: https://youtu.be/UbjxGvrDrbw | |||||
License (OLC-3) | |||||
~~~~~~~~~~~~~~~ | |||||
Copyright 2018 - 2020 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 | |||||
*/ | |||||
#pragma once | |||||
#include <memory> | |||||
#include <thread> | |||||
#include <mutex> | |||||
#include <deque> | |||||
#include <optional> | |||||
#include <vector> | |||||
#include <iostream> | |||||
#include <algorithm> | |||||
#include <chrono> | |||||
#include <cstdint> | |||||
#ifdef _WIN32 | |||||
#define _WIN32_WINNT 0x0A00 | |||||
#endif | |||||
#define ASIO_STANDALONE | |||||
#include <asio.hpp> | |||||
#include <asio/ts/buffer.hpp> | |||||
#include <asio/ts/internet.hpp> |
/* | |||||
MMO Client/Server Framework using ASIO | |||||
"Happy Birthday Mrs Javidx9!" - javidx9 | |||||
Videos: | |||||
Part #1: https://youtu.be/2hNdkYInj4g | |||||
Part #2: https://youtu.be/UbjxGvrDrbw | |||||
License (OLC-3) | |||||
~~~~~~~~~~~~~~~ | |||||
Copyright 2018 - 2020 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 | |||||
*/ | |||||
#pragma once | |||||
#include "net_common.h" | |||||
#include "net_dequeue_ts.h" | |||||
#include "net_message.h" | |||||
namespace net | |||||
{ | |||||
template<typename T> | |||||
class Connection : public std::enable_shared_from_this<Connection<T>> | |||||
{ | |||||
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, ts_dequeue<OwnedMessage<T>>& qIn) | |||||
: m_socket(std::move(socket)), m_asioContext(asioContext), m_qMessagesIn(qIn) | |||||
{ | |||||
m_nOwnerType = parent; | |||||
} | |||||
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(uint32_t uid = 0) | |||||
{ | |||||
if (m_nOwnerType == Owner::SERVER) | |||||
{ | |||||
if (m_socket.is_open()) | |||||
{ | |||||
id = uid; | |||||
ReadHeader(); | |||||
} | |||||
} | |||||
} | |||||
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) | |||||
{ | |||||
ReadHeader(); | |||||
} | |||||
}); | |||||
} | |||||
} | |||||
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<T>& 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(MessageHeader<T>)), | |||||
[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() | |||||
{ | |||||
// asio to waits 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. | |||||
//Call this function to set up an asynchronous listener for a certain Connection | |||||
asio::async_read(m_socket, asio::buffer(&m_msgTemporaryIn.header, sizeof(MessageHeader<T>)), | |||||
[this](std::error_code ec, std::size_t length) | |||||
{ | |||||
if (!ec) | |||||
{ | |||||
//Full header readed | |||||
//Check for message body | |||||
if (m_msgTemporaryIn.header.size > 0) | |||||
{ | |||||
//Allocate storage | |||||
m_msgTemporaryIn.body.resize(m_msgTemporaryIn.header.size); | |||||
//Read if available | |||||
ReadBody(); | |||||
} | |||||
else | |||||
{ | |||||
//Bodyless message, add to queue | |||||
AddToIncomingMessageQueue(); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
//Failure, probably a disconnection | |||||
std::cout << "[" << id << "] Error: " << ec.message() << std::endl; | |||||
m_socket.close(); | |||||
} | |||||
}); | |||||
} | |||||
// ASYNC - Prime context to read a message body | |||||
void ReadBody() | |||||
{ | |||||
//Called after header has been read successfully | |||||
//Read the body in the pre allocated storage | |||||
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) | |||||
{ | |||||
// Complete message, add to queue | |||||
AddToIncomingMessageQueue(); | |||||
} | |||||
else | |||||
{ | |||||
//Failure, probably a disconnection | |||||
std::cout << "[" << id << "] Error reading body: " << ec.message() << std::endl; | |||||
m_socket.close(); | |||||
} | |||||
}); | |||||
} | |||||
// Add a complete message to the incoming queue, with or without body | |||||
void AddToIncomingMessageQueue() | |||||
{ | |||||
//Put it in the queue, put a owner to the object to let the server know who send the message (which connection) | |||||
if(m_nOwnerType == Owner::SERVER) | |||||
m_qMessagesIn.push_back({ this->shared_from_this(), m_msgTemporaryIn }); | |||||
else | |||||
m_qMessagesIn.push_back({ nullptr, m_msgTemporaryIn }); | |||||
//Done queueing the message, now initialize a new async read to wait for next message | |||||
ReadHeader(); | |||||
} | |||||
protected: | |||||
// 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 | |||||
ts_dequeue<Message<T>> m_qMessagesOut; | |||||
// This references the incoming queue of the parent object | |||||
ts_dequeue<OwnedMessage<T>>& m_qMessagesIn; | |||||
// Incoming messages are constructed asynchronously, so we will | |||||
// store the part assembled message here, until it is ready | |||||
Message<T> m_msgTemporaryIn; | |||||
// The "owner" decides how some of the connection behaves | |||||
Owner m_nOwnerType = Owner::SERVER; | |||||
uint32_t id = 0; | |||||
}; | |||||
} |
/* | |||||
MMO Client/Server Framework using ASIO | |||||
"Happy Birthday Mrs Javidx9!" - javidx9 | |||||
Videos: | |||||
Part #1: https://youtu.be/2hNdkYInj4g | |||||
Part #2: https://youtu.be/UbjxGvrDrbw | |||||
License (OLC-3) | |||||
~~~~~~~~~~~~~~~ | |||||
Copyright 2018 - 2020 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 | |||||
*/ | |||||
#pragma once | |||||
#include "net_common.h" | |||||
namespace net | |||||
{ | |||||
template<typename T> | |||||
class ts_dequeue | |||||
{ | |||||
public: | |||||
ts_dequeue() = default; | |||||
ts_dequeue(const ts_dequeue<T>&) = delete; | |||||
virtual ~ts_dequeue() { 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<std::mutex> 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<std::mutex> 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<std::mutex> ul(muxBlocking); | |||||
cvBlocking.wait(ul); | |||||
} | |||||
} | |||||
protected: | |||||
std::mutex muxQueue; | |||||
std::deque<T> deqQueue; | |||||
std::condition_variable cvBlocking; | |||||
std::mutex muxBlocking; | |||||
}; | |||||
} |
/* | |||||
MMO Client/Server Framework using ASIO | |||||
"Happy Birthday Mrs Javidx9!" - javidx9 | |||||
Videos: | |||||
Part #1: https://youtu.be/2hNdkYInj4g | |||||
Part #2: https://youtu.be/UbjxGvrDrbw | |||||
License (OLC-3) | |||||
~~~~~~~~~~~~~~~ | |||||
Copyright 2018 - 2020 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 | |||||
*/ | |||||
#pragma once | |||||
#include "net_common.h" | |||||
#include <string> | |||||
namespace net | |||||
{ | |||||
///[OLC_HEADERIFYIER] START "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 <typename T> | |||||
struct MessageHeader | |||||
{ | |||||
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 <typename T> | |||||
struct Message | |||||
{ | |||||
// Header & Body vector | |||||
MessageHeader<T> header{}; | |||||
std::vector<uint8_t> 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<T>& 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<typename DataType> | |||||
friend Message<T>& operator << (Message<T>& msg, const DataType& data) | |||||
{ | |||||
// Check that the type of the data being pushed is trivially copyable | |||||
static_assert(std::is_standard_layout<DataType>::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(i + 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; | |||||
} | |||||
//Specified template to write string | |||||
friend Message<T>& operator << (Message<T>& msg, const std::string& data) | |||||
{ | |||||
// 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(i + sizeof(data)); | |||||
// Physically copy the data of the string character by character | |||||
msg.body.resize(i + data.size()); | |||||
for (size_t index = 0; index < data.size(); index++) { | |||||
msg.body[i+index] = data.at(index); | |||||
//std::memcpy(msg.body.data() + i, &c, sizeof(uint8_t)); | |||||
} | |||||
// Recalculate the message size | |||||
msg.header.size = (uint32_t)msg.size(); | |||||
// Return the target message so it can be "chained" | |||||
return msg; | |||||
} | |||||
// Pulls any POD-like data form the message buffer | |||||
template<typename DataType> | |||||
friend Message<T>& operator >> (Message<T>& msg, DataType& data) | |||||
{ | |||||
// Check that the type of the data being pushed is trivially copyable | |||||
static_assert(std::is_standard_layout<DataType>::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; | |||||
} | |||||
//Specified template to read string | |||||
friend Message<T>& operator >> (Message<T>& msg, std::string& data) | |||||
{ | |||||
// Cache the location towards the end of the vector where the pulled data starts | |||||
size_t i = 0; | |||||
// Physically copy the data from the vector into the user variable | |||||
std::memcpy(&data, msg.body.data(), msg.body.size()); | |||||
// Shrink the vector to remove read bytes, and reset end position | |||||
msg.body.resize(i); | |||||
// 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 <typename T> | |||||
class Connection; | |||||
template <typename T> | |||||
struct OwnedMessage | |||||
{ | |||||
std::shared_ptr<Connection<T>> remote = nullptr; | |||||
Message<T> msg; | |||||
// Again, a friendly string maker | |||||
friend std::ostream& operator<<(std::ostream& os, const OwnedMessage<T>& msg) | |||||
{ | |||||
os << msg.msg; | |||||
return os; | |||||
} | |||||
}; | |||||
} |
/* | |||||
MMO Client/Server Framework using ASIO | |||||
"Happy Birthday Mrs Javidx9!" - javidx9 | |||||
Videos: | |||||
Part #1: https://youtu.be/2hNdkYInj4g | |||||
Part #2: https://youtu.be/UbjxGvrDrbw | |||||
License (OLC-3) | |||||
~~~~~~~~~~~~~~~ | |||||
Copyright 2018 - 2020 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 | |||||
*/ | |||||
#pragma once | |||||
#include "net_dequeue_ts.h" | |||||
#include "net_common.h" | |||||
#include "net_message.h" | |||||
#include "net_connection.h" | |||||
#include "easylogging++.h" | |||||
namespace net | |||||
{ | |||||
template<typename T> | |||||
class ServerInterface | |||||
{ | |||||
public: // Create a server, ready to listen on specified port | |||||
ServerInterface(uint16_t port) | |||||
: m_asioAcceptor(m_asioContext, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)) { } | |||||
virtual ~ServerInterface() { Stop(); } | |||||
// Starts the server | |||||
virtual 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 | |||||
LOG(ERROR) << "[SERVER] Exception: " << e.what(); | |||||
return false; | |||||
} | |||||
return true; | |||||
} | |||||
bool isStopped()const { | |||||
return m_asioContext.stopped(); | |||||
} | |||||
// Stops the server! | |||||
virtual 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... | |||||
LOG(INFO) << "[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) | |||||
{ | |||||
LOG(INFO) << "[SERVER] New Connection: " << socket.remote_endpoint(); | |||||
// Create a new connection to handle this client | |||||
std::shared_ptr<Connection<T>> newconn = | |||||
std::make_shared<Connection<T>>(Connection<T>::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)); | |||||
// Issue a task to the connection's | |||||
// asio context to sit and wait for bytes to arrive! | |||||
m_deqConnections.back()->ConnectToClient(nIDCounter++); | |||||
LOG(INFO) << "[" << m_deqConnections.back()->GetID() << "] Connection Approved"; | |||||
} | |||||
else | |||||
{ | |||||
LOG(INFO) << "[SERVER] Connection Denied"; | |||||
// Connection will go out of scope with no pending tasks, so will | |||||
// get destroyed automatically (smart pointer) | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
// Error has occurred during acceptance | |||||
LOG(INFO) << "[SERVER] New Connection Error: " << ec.message(); | |||||
} | |||||
// 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<Connection<T>> client, const Message<T>& 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<T>& msg, std::shared_ptr<Connection<T>> 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 | |||||
// size_t nmaxMessages: Assign -1 to unsigned to unspecify max message count | |||||
// bool bWait: if queue is empty, wait synchronously until message arrives | |||||
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: | |||||
//Overwritable functions to customize server behaviour | |||||
// Called when a client connects, you can veto the connection by returning false | |||||
virtual bool OnClientConnect(std::shared_ptr<Connection<T>> client) = 0; | |||||
// Called when a client appears to have disconnected | |||||
virtual void OnClientDisconnect(std::shared_ptr<Connection<T>> client) = 0; | |||||
// Called when a message arrives | |||||
virtual void OnMessage(std::shared_ptr<Connection<T>> client, Message<T>& msg) = 0; | |||||
// Thread Safe Queue for incoming message packets | |||||
ts_dequeue<OwnedMessage<T>> m_qMessagesIn; | |||||
// Container of active validated connections | |||||
std::deque<std::shared_ptr<Connection<T>>> 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; | |||||
}; | |||||
} |