|
|
/*
|
|
|
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, <EFBFBD>OneLoneCoder 2019, 2020
|
|
|
|
|
|
*/
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
#include "net_common.h"
|
|
|
#include "net_tsqueue.h"
|
|
|
#include "net_message.h"
|
|
|
#include "net_connection.h"
|
|
|
|
|
|
namespace olc
|
|
|
{
|
|
|
namespace net
|
|
|
{
|
|
|
template<typename T>
|
|
|
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<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));
|
|
|
|
|
|
// And very important! Issue a task to the connection's
|
|
|
// asio context to sit and wait for bytes to arrive!
|
|
|
m_deqConnections.back()->ConnectToClient(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<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
|
|
|
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<connection<T>> client)
|
|
|
{
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
// Called when a client appears to have disconnected
|
|
|
virtual void OnClientDisconnect(std::shared_ptr<connection<T>> client)
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
// Called when a message arrives
|
|
|
virtual void OnMessage(std::shared_ptr<connection<T>> client, message<T>& msg)
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
protected:
|
|
|
// Thread Safe Queue for incoming message packets
|
|
|
tsqueue<owned_message<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;
|
|
|
};
|
|
|
}
|
|
|
} |