353 lines
11 KiB
C++
353 lines
11 KiB
C++
/*
|
||
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, <20>OneLoneCoder 2019, 2020
|
||
|
||
*/
|
||
|
||
#pragma once
|
||
|
||
#include "net_common.h"
|
||
#include "net_tsqueue.h"
|
||
#include "net_message.h"
|
||
|
||
|
||
namespace olc
|
||
{
|
||
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, tsqueue<owned_message<T>>& qIn)
|
||
: m_asioContext(asioContext), m_socket(std::move(socket)), 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(message_header<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()
|
||
{
|
||
// If this function is called, we are expecting asio to wait until it receives
|
||
// enough bytes to form a header of a message. We know the headers are a fixed
|
||
// size, so allocate a transmission buffer large enough to store it. In fact,
|
||
// we will construct the message in a "temporary" message object as it's
|
||
// convenient to work with.
|
||
asio::async_read(m_socket, asio::buffer(&m_msgTemporaryIn.header, sizeof(message_header<T>)),
|
||
[this](std::error_code ec, std::size_t length)
|
||
{
|
||
if (!ec)
|
||
{
|
||
// A complete message header has been read, check if this message
|
||
// has a body to follow...
|
||
if (m_msgTemporaryIn.header.size > 0)
|
||
{
|
||
// ...it does, so allocate enough space in the messages' body
|
||
// vector, and issue asio with the task to read the body.
|
||
m_msgTemporaryIn.body.resize(m_msgTemporaryIn.header.size);
|
||
ReadBody();
|
||
}
|
||
else
|
||
{
|
||
// it doesn't, so add this bodyless message to the connections
|
||
// incoming message queue
|
||
AddToIncomingMessageQueue();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Reading form the client went wrong, most likely a disconnect
|
||
// has occurred. Close the socket and let the system tidy it up later.
|
||
std::cout << "[" << id << "] Read Header Fail.\n";
|
||
m_socket.close();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ASYNC - Prime context ready to read a message body
|
||
void ReadBody()
|
||
{
|
||
// If this function is called, a header has already been read, and that header
|
||
// request we read a body, The space for that body has already been allocated
|
||
// in the temporary message object, so just wait for the bytes to arrive...
|
||
asio::async_read(m_socket, asio::buffer(m_msgTemporaryIn.body.data(), m_msgTemporaryIn.body.size()),
|
||
[this](std::error_code ec, std::size_t length)
|
||
{
|
||
if (!ec)
|
||
{
|
||
// ...and they have! The message is now complete, so add
|
||
// the whole message to incoming queue
|
||
AddToIncomingMessageQueue();
|
||
}
|
||
else
|
||
{
|
||
// As above!
|
||
std::cout << "[" << id << "] Read Body Fail.\n";
|
||
m_socket.close();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Once a full message is received, add it to the incoming queue
|
||
void AddToIncomingMessageQueue()
|
||
{
|
||
// Shove it in queue, converting it to an "owned message", by initialising
|
||
// with the a shared pointer from this connection object
|
||
if(m_nOwnerType == owner::server)
|
||
m_qMessagesIn.push_back({ this->shared_from_this(), m_msgTemporaryIn });
|
||
else
|
||
m_qMessagesIn.push_back({ nullptr, m_msgTemporaryIn });
|
||
|
||
// We must now prime the asio context to receive the next message. It
|
||
// wil just sit and wait for bytes to arrive, and the message construction
|
||
// process repeats itself. Clever huh?
|
||
ReadHeader();
|
||
}
|
||
|
||
protected:
|
||
// Each connection has a unique socket to a remote
|
||
asio::ip::tcp::socket m_socket;
|
||
|
||
// This context is shared with the whole asio instance
|
||
asio::io_context& m_asioContext;
|
||
|
||
// This queue holds all messages to be sent to the remote side
|
||
// of this connection
|
||
tsqueue<message<T>> m_qMessagesOut;
|
||
|
||
// This references the incoming queue of the parent object
|
||
tsqueue<owned_message<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;
|
||
|
||
};
|
||
}
|
||
} |