|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- /*
- 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;
-
- };
- }
|