@@ -0,0 +1,157 @@ | |||
/* | |||
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; | |||
}; | |||
} |
@@ -0,0 +1,76 @@ | |||
/* | |||
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> |
@@ -0,0 +1,343 @@ | |||
/* | |||
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; | |||
}; | |||
} |
@@ -0,0 +1,161 @@ | |||
/* | |||
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; | |||
}; | |||
} |
@@ -0,0 +1,214 @@ | |||
/* | |||
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; | |||
} | |||
}; | |||
} |
@@ -0,0 +1,276 @@ | |||
/* | |||
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; | |||
}; | |||
} |