Merge branch 'lowlevel-networking' of git://github.com/Erdroy/FlaxEngine into Erdroy-lowlevel-networking

This commit is contained in:
Wojtek Figat
2021-07-02 09:53:05 +02:00
17 changed files with 7746 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ public class Engine : EngineModule
options.PublicDependencies.Add("Input");
options.PublicDependencies.Add("Level");
options.PublicDependencies.Add("Navigation");
options.PublicDependencies.Add("Networking");
options.PublicDependencies.Add("Physics");
options.PublicDependencies.Add("Particles");
options.PublicDependencies.Add("Scripting");

View File

@@ -0,0 +1,250 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
//
// TODO: Check defines so we can disable ENet
#include "ENetDriver.h"
#include "Engine/Networking/NetworkConfig.h"
#include "Engine/Networking/NetworkChannelType.h"
#include "Engine/Networking/NetworkEvent.h"
#include "Engine/Networking/NetworkPeer.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Collections/Array.h"
#define ENET_IMPLEMENTATION
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <enet/enet.h>
#undef _WINSOCK_DEPRECATED_NO_WARNINGS
#undef SendMessage
ENetPacketFlag ChannelTypeToPacketFlag(const NetworkChannelType channel)
{
int flag = 0; // Maybe use ENET_PACKET_FLAG_NO_ALLOCATE?
// Add reliable flag when it is "reliable" channel
if(channel > NetworkChannelType::UnreliableOrdered)
flag |= ENET_PACKET_FLAG_RELIABLE;
// Use unsequenced flag when the flag is unreliable. We have to sequence all other packets.
if(channel == NetworkChannelType::Unreliable)
flag |= ENET_PACKET_FLAG_UNSEQUENCED;
// Note that all reliable channels are exactly the same. TODO: How to handle unordered reliable packets...?
return static_cast<ENetPacketFlag>(flag);
}
void SendPacketToPeer(ENetPeer* peer, const NetworkChannelType channelType, const NetworkMessage& message)
{
// Covert our channel type to the internal ENet packet flags
const ENetPacketFlag flag = ChannelTypeToPacketFlag(channelType);
// This will copy the data into the packet when ENET_PACKET_FLAG_NO_ALLOCATE is not set.
// Tho, we cannot use it, because we're releasing the message right after the send - and the packet might not
// be sent, yet. To avoid data corruption, we're just using the copy method. We might fix that later, but I'll take
// the smaller risk.
ENetPacket* packet = enet_packet_create(message.Buffer, message.Length, flag);
// And send it!
enet_peer_send (peer, 0, packet);
// TODO: To reduce latency, we can use `enet_host_flush` to flush all packets. Maybe some API, like NetworkManager::FlushQueues()?
}
void ENetDriver::Initialize(NetworkPeer* host, const NetworkConfig& config)
{
_networkHost = host;
_config = config;
_peerMap = Dictionary<uint32, void*>();
if (enet_initialize () != 0) {
LOG(Error, "Failed to initialize ENet driver!");
}
LOG(Info, "Initialized ENet driver!");
}
void ENetDriver::Dispose()
{
if(_peer)
enet_peer_disconnect_now((ENetPeer*)_peer, 0);
enet_host_destroy((ENetHost*)_host);
enet_deinitialize();
_peerMap.Clear();
_peerMap = {};
_peer = nullptr;
_host = nullptr;
LOG(Info, "ENet driver stopped!");
}
bool ENetDriver::Listen()
{
ENetAddress address = {0};
address.port = _config.Port;
address.host = ENET_HOST_ANY;
// Set host address if needed
if(_config.Address != String("any"))
enet_address_set_host(&address, _config.Address.ToStringAnsi().GetText());
// Create ENet host
_host = enet_host_create(&address, _config.ConnectionsLimit, 1, 0, 0);
if(_host == nullptr)
{
LOG(Error, "Failed to initialize ENet host!");
return false;
}
LOG(Info, "Created ENet server!");
return true;
}
bool ENetDriver::Connect()
{
LOG(Info, "Connecting using ENet...");
ENetAddress address = {0};
address.port = _config.Port;
enet_address_set_host(&address, _config.Address.ToStringAnsi().GetText());
// Create ENet host
_host = enet_host_create(nullptr, 1, 1, 0, 0);
if(_host == nullptr)
{
LOG(Error, "Failed to initialize ENet host!");
return false;
}
// Create ENet peer/connect to the server
_peer = enet_host_connect((ENetHost*)_host, &address, 1, 0);
if(_peer == nullptr)
{
LOG(Error, "Failed to create ENet host!");
enet_host_destroy((ENetHost*)_host);
return false;
}
return true;
}
void ENetDriver::Disconnect()
{
ASSERT(_peer != nullptr);
if(_peer)
{
enet_peer_disconnect_now((ENetPeer*)_peer, 0);
_peer = nullptr;
LOG(Info, "Disconnected");
}
}
void ENetDriver::Disconnect(const NetworkConnection& connection)
{
const int connectionId = connection.ConnectionId;
void* peer = nullptr;
if(_peerMap.TryGet(connectionId, peer))
{
enet_peer_disconnect_now((ENetPeer*)_peer, 0);
_peerMap.Remove(connectionId);
}
else
{
LOG(Error, "Failed to kick connection({0}). ENetPeer not found!", connection.ConnectionId);
}
}
bool ENetDriver::PopEvent(NetworkEvent* eventPtr)
{
ENetEvent event;
const int result = enet_host_service((ENetHost*)_host, &event, 0);
if(result < 0)
LOG(Error, "Failed to check ENet events!");
if(result > 0)
{
// Copy sender data
const uint32 connectionId = enet_peer_get_id(event.peer);
eventPtr->Sender = NetworkConnection();
eventPtr->Sender.ConnectionId = connectionId;
switch(event.type)
{
case ENET_EVENT_TYPE_CONNECT:
eventPtr->EventType = NetworkEventType::Connected;
if(IsServer())
_peerMap.Add(connectionId, event.peer);
break;
case ENET_EVENT_TYPE_DISCONNECT:
eventPtr->EventType = NetworkEventType::Disconnected;
if(IsServer())
_peerMap.Remove(connectionId);
break;
case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
eventPtr->EventType = NetworkEventType::Timeout;
if(IsServer())
_peerMap.Remove(connectionId);
break;
case ENET_EVENT_TYPE_RECEIVE:
eventPtr->EventType = NetworkEventType::Message;
// Acquire message and copy message data
eventPtr->Message = _networkHost->CreateMessage();
eventPtr->Message.Length = event.packet->dataLength;
Memory::CopyItems(eventPtr->Message.Buffer, event.packet->data, event.packet->dataLength);
break;
default: break;
}
return true; // Event
}
return false; // No events
}
void ENetDriver::SendMessage(const NetworkChannelType channelType, const NetworkMessage& message)
{
ASSERT(IsServer() == false);
SendPacketToPeer((ENetPeer*)_peer, channelType, message);
}
void ENetDriver::SendMessage(NetworkChannelType channelType, const NetworkMessage& message, NetworkConnection target)
{
ASSERT(IsServer());
ENetPeer* peer = *(ENetPeer**)_peerMap.TryGet(target.ConnectionId);
ASSERT(peer != nullptr);
ASSERT(peer->state == ENET_PEER_STATE_CONNECTED);
SendPacketToPeer(peer, channelType, message);
}
void ENetDriver::SendMessage(const NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection, HeapAllocation>& targets)
{
ASSERT(IsServer());
for(NetworkConnection target : targets)
{
ENetPeer* peer = *(ENetPeer**)_peerMap.TryGet(target.ConnectionId);
ASSERT(peer != nullptr);
ASSERT(peer->state == ENET_PEER_STATE_CONNECTED);
SendPacketToPeer(peer, channelType, message);
}
}

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Networking/Types.h"
#include "Engine/Networking/INetworkDriver.h"
#include "Engine/Networking/NetworkConnection.h"
#include "Engine/Networking/NetworkConfig.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Low-level network transport interface implementation based on ENet library.
/// </summary>
API_CLASS(Namespace="FlaxEngine.Networking", Sealed) class FLAXENGINE_API ENetDriver : public INetworkDriver
{
DECLARE_SCRIPTING_TYPE_MINIMAL(ENetDriver);
public:
void Initialize(NetworkPeer* host, const NetworkConfig& config) override;
void Dispose() override;
bool Listen() override;
bool Connect() override;
void Disconnect() override;
void Disconnect(const NetworkConnection& connection) override;
bool PopEvent(NetworkEvent* eventPtr) override;
void SendMessage(NetworkChannelType channelType, const NetworkMessage& message) override;
void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, NetworkConnection target) override;
void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection, HeapAllocation>& targets) override;
private:
bool IsServer() const
{
return _host != nullptr && _peer == nullptr;
}
private:
NetworkConfig _config;
NetworkPeer* _networkHost;
void* _host = nullptr;
void* _peer = nullptr;
Dictionary<uint32, void*> _peerMap;
};

View File

@@ -0,0 +1,110 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingType.h"
#include "Types.h"
/// <summary>
/// Basic interface for the low-level network transport/driver.
/// </summary>
API_INTERFACE(Namespace="FlaxEngine.Networking") class FLAXENGINE_API INetworkDriver
{
DECLARE_SCRIPTING_TYPE_MINIMAL(INetworkDriver);
public:
/// <summary>
/// Finalizes an instance of the <see cref="INetworkDriver"/> class.
/// </summary>
virtual ~INetworkDriver() = default;
public:
/// <summary>
/// Initializes the instance of this network driver using given configuration.
/// </summary>
/// <param name="host">The peer that this driver has been assigned to.</param>
/// <param name="config">The network config to use to configure this driver.</param>
virtual void Initialize(NetworkPeer* host, const NetworkConfig& config) = 0;
/// <summary>
/// Disposes this driver making it no longer usable.
/// Reserved for resource deallocation etc.
/// </summary>
virtual void Dispose() = 0;
/// <summary>
/// Starts listening for incoming connections.
/// Once this is called, this driver becomes a server.
/// </summary>
/// <returns>True when succeeded.</returns>
virtual bool Listen() = 0;
/// <summary>
/// Starts connection handshake with the end point specified in the <seealso cref="NetworkConfig"/> structure.
/// Once this is called, this driver becomes a client.
/// </summary>
/// <returns>True when succeeded.</returns>
virtual bool Connect() = 0;
/// <summary>
/// Disconnects from the server.
/// </summary>
/// <remarks>Can be used only by the client!</remarks>
virtual void Disconnect() = 0;
/// <summary>
/// Disconnects given connection from the server.
/// </summary>
/// <remarks>Can be used only by the server!</remarks>
virtual void Disconnect(const NetworkConnection& connection) = 0;
/// <summary>
/// Tries to pop an network event from the queue.
/// </summary>
/// <param name="eventPtr">The pointer to event structure.</param>
/// <returns>True when succeeded and the event can be processed.</returns>
virtual bool PopEvent(NetworkEvent* eventPtr) = 0;
/// <summary>
/// Sends given message over specified channel to the server.
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <remarks>Can be used only by the client!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
virtual void SendMessage(NetworkChannelType channelType, const NetworkMessage& message) = 0;
/// <summary>
/// Sends given message over specified channel to the given client connection (target).
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <param name="target">The client connection to send the message to.</param>
/// <remarks>Can be used only by the server!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
virtual void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, NetworkConnection target) = 0;
/// <summary>
/// Sends given message over specified channel to the given client connection (target).
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <param name="targets">The connections list to send the message to.</param>
/// <remarks>Can be used only by the server!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
virtual void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection, HeapAllocation>& targets) = 0;
// TODO: Stats API
// TODO: Simulation API
};

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Config.h"
/// <summary>
/// The low-level network channel type for message sending.
/// </summary>
API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkChannelType
{
/// <summary>
/// Invalid channel type.
/// </summary>
None = 0,
/// <summary>
/// Unreliable channel type.
/// Messages can be lost or arrive out-of-order.
/// </summary>
Unreliable,
/// <summary>
/// Unreliable-ordered channel type.
/// Messages can be lost but always arrive in order.
/// </summary>
UnreliableOrdered,
/// <summary>
/// Reliable channel type.
/// Messages won't be lost but may arrive out-of-order.
/// </summary>
Reliable,
/// <summary>
/// Reliable-ordered channel type.
/// Messages won't be lost and always arrive in order.
/// </summary>
ReliableOrdered
};

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Platform/Network.h"
/// <summary>
/// Network driver implementations enum.
/// </summary>
API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkDriverType
{
/// <summary>
/// Invalid network driver implementation.
/// </summary>
Undefined = 0,
/// <summary>
/// ENet library based network driver implementation.
/// </summary>
ENet
};
/// <summary>
/// Low-level network configuration structure. Provides settings for the network driver and all internal components.
/// </summary>
API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkConfig
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkConfig);
public:
/// <summary>
/// The network driver that will be used to create the peer.
/// To allow two peers to connect, they must use the same host.
/// </summary>
API_FIELD()
NetworkDriverType NetworkDriverType = NetworkDriverType::ENet;
// TODO: Expose INetworkDriver as a ref not enum, when C++/C# interfaces are done.
public:
/// <summary>
/// The upper limit on how many peers can join when we're listening.
/// </summary>
API_FIELD()
uint16 ConnectionsLimit = 32;
/// <summary>
/// Address used to connect to or listen at.
/// </summary>
/// <remarks>Set it to "any" when you want to listen at all available addresses.</remarks>
/// <remarks>Only IPv4 is supported.</remarks>
API_FIELD()
String Address = String("127.0.0.1");
/// <summary>
/// The port to connect to or listen at.
/// </summary>
API_FIELD()
uint16 Port = 7777;
/// <summary>
/// The size of a message buffer in bytes.
/// Should be lower than the MTU (maximal transmission unit) - typically 1500 bytes.
/// </summary>
API_FIELD()
uint16 MessageSize = 1500;
/// <summary>
/// The amount of pooled messages that can be used at once (receiving and sending!).
/// </summary>
/// <remarks>
/// Creating more messages than this limit will result in a crash!
/// This should be tweaked manually to fit the needs (adjusting this value will increase/decrease memory usage)!
/// </remarks>
API_FIELD()
uint16 MessagePoolSize = 2048;
};

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Network connection structure - used to identify connected peers when we're listening.
/// </summary>
API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkConnection
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkConnection);
public:
/// <summary>
/// The identifier of the connection.
/// </summary>
/// <remarks>Used by network driver implementations.</remarks>
API_FIELD()
uint32 ConnectionId;
};

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "NetworkConnection.h"
#include "NetworkMessage.h"
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Network event type enum contains all possible events that can be returned by PopEvent function.
/// </summary>
API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkEventType
{
/// <summary>
/// Invalid network event type.
/// </summary>
Undefined = 0,
/// <summary>
/// Event "connected" - client connected to our server or we've connected to the server.
/// </summary>
Connected,
/// <summary>
/// Event "disconnected" - client disconnected from our server or we've been kicked from the server.
/// </summary>
Disconnected,
/// <summary>
/// Event "disconnected" - client got a timeout from our server or we've list the connection to the server.
/// </summary>
Timeout,
/// <summary>
/// Event "message" - message received from some client or the server.
/// </summary>
Message
};
/// <summary>
/// Network event structure that wraps all data needed to identify and process it.
/// </summary>
API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkEvent
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkEvent);
public:
/// <summary>
/// The type of the received event.
/// </summary>
API_FIELD();
NetworkEventType EventType;
/// <summary>
/// The message when this event is an "message" event - not valid in any other cases.
/// If this is an message-event, make sure to return the message using RecycleMessage function of the peer after processing it!
/// </summary>
API_FIELD();
NetworkMessage Message;
/// <summary>
/// The connected of the client that has sent message, connected, disconnected or got a timeout.
/// </summary>
/// <remarks>Only valid when event has been received on server-peer.</remarks>
API_FIELD();
NetworkConnection Sender;
};

View File

@@ -0,0 +1,365 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
using System.Text;
using FlaxEngine.Assertions;
namespace FlaxEngine.Networking
{
public unsafe partial struct NetworkMessage
{
/// <summary>
/// Writes raw bytes into the message.
/// </summary>
/// <param name="bytes">The bytes that will be written.</param>
/// <param name="length">The amount of bytes to write from the bytes pointer.</param>
public void WriteBytes(byte* bytes, int length)
{
Assert.IsTrue(Position + length <= BufferSize, $"Could not write data of length {length} into message with id={MessageId}! Current write position={Position}");
Utils.MemoryCopy(new IntPtr(bytes), new IntPtr(Buffer + Position), (ulong)length);
Position += (uint)length;
Length = Position;
}
/// <summary>
/// Reads raw bytes from the message into the given byte array.
/// </summary>
/// <param name="buffer">
/// The buffer pointer that will be used to store the bytes.
/// Should be of the same length as length or longer.
/// </param>
/// <param name="length">The minimal amount of bytes that the buffer contains.</param>
public void ReadBytes(byte* buffer, int length)
{
Assert.IsTrue(Position + length <= Length, $"Could not read data of length {length} from message with id={MessageId} and size of {Length}B! Current read position={Position}");
Utils.MemoryCopy(new IntPtr(Buffer + Position), new IntPtr(buffer), (ulong)length);
Position += (uint)length;
}
/// <summary>
/// Writes raw bytes into the message.
/// </summary>
/// <param name="bytes">The bytes that will be written.</param>
/// <param name="length">The amount of bytes to write from the bytes array.</param>
public void WriteBytes(byte[] bytes, int length)
{
fixed (byte* bytesPtr = bytes)
{
WriteBytes(bytesPtr, length);
}
}
/// <summary>
/// Reads raw bytes from the message into the given byte array.
/// </summary>
/// <param name="buffer">
/// The buffer that will be used to store the bytes.
/// Should be of the same length as length or longer.
/// </param>
/// <param name="length">The minimal amount of bytes that the buffer contains.</param>
public void ReadBytes(byte[] buffer, int length)
{
fixed (byte* bufferPtr = buffer)
{
ReadBytes(bufferPtr, length);
}
}
/// <summary>
/// Writes data of type <see cref="UInt64"/> into the message.
/// </summary>
public void WriteInt64(long value)
{
WriteBytes((byte*)&value, sizeof(long));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt64"/> from the message.
/// </summary>
public long ReadInt64()
{
long value = 0;
ReadBytes((byte*)&value, sizeof(long));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt32"/> into the message.
/// </summary>
public void WriteInt32(int value)
{
WriteBytes((byte*)&value, sizeof(int));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt32"/> from the message.
/// </summary>
public int ReadInt32()
{
int value = 0;
ReadBytes((byte*)&value, sizeof(int));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt16"/> into the message.
/// </summary>
public void WriteInt16(short value)
{
WriteBytes((byte*)&value, sizeof(short));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt16"/> from the message.
/// </summary>
public short ReadInt16()
{
short value = 0;
ReadBytes((byte*)&value, sizeof(short));
return value;
}
/// <summary>
/// Writes data of type <see cref="SByte"/> into the message.
/// </summary>
public void WriteSByte(sbyte value)
{
WriteBytes((byte*)&value, sizeof(sbyte));
}
/// <summary>
/// Reads and returns data of type <see cref="SByte"/> from the message.
/// </summary>
public sbyte ReadSByte()
{
sbyte value = 0;
ReadBytes((byte*)&value, sizeof(sbyte));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt64"/> into the message.
/// </summary>
public void WriteUInt64(ulong value)
{
WriteBytes((byte*)&value, sizeof(ulong));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt64"/> from the message.
/// </summary>
public ulong ReadUInt64()
{
ulong value = 0;
ReadBytes((byte*)&value, sizeof(ulong));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt32"/> into the message.
/// </summary>
public void WriteUInt32(uint value)
{
WriteBytes((byte*)&value, sizeof(uint));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt32"/> from the message.
/// </summary>
public uint ReadUInt32()
{
uint value = 0;
ReadBytes((byte*)&value, sizeof(uint));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt16"/> into the message.
/// </summary>
public void WriteUInt16(ushort value)
{
WriteBytes((byte*)&value, sizeof(ushort));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt16"/> from the message.
/// </summary>
public ushort ReadUInt16()
{
ushort value = 0;
ReadBytes((byte*)&value, sizeof(ushort));
return value;
}
/// <summary>
/// Writes data of type <see cref="Byte"/> into the message.
/// </summary>
public void WriteByte(byte value)
{
WriteBytes(&value, sizeof(byte));
}
/// <summary>
/// Reads and returns data of type <see cref="Byte"/> from the message.
/// </summary>
public byte ReadByte()
{
byte value = 0;
ReadBytes(&value, sizeof(byte));
return value;
}
/// <summary>
/// Writes data of type <see cref="Single"/> into the message.
/// </summary>
public void WriteSingle(float value)
{
WriteBytes((byte*)&value, sizeof(float));
}
/// <summary>
/// Reads and returns data of type <see cref="Single"/> from the message.
/// </summary>
public float ReadSingle()
{
float value = 0.0f;
ReadBytes((byte*)&value, sizeof(float));
return value;
}
/// <summary>
/// Writes data of type <see cref="Double"/> into the message.
/// </summary>
public void WriteDouble(double value)
{
WriteBytes((byte*)&value, sizeof(double));
}
/// <summary>
/// Reads and returns data of type <see cref="Double"/> from the message.
/// </summary>
public double ReadDouble()
{
double value = 0.0;
ReadBytes((byte*)&value, sizeof(double));
return value;
}
/// <summary>
/// Writes data of type <see cref="string"/> into the message. UTF-16 encoded.
/// </summary>
public void WriteString(string value)
{
// Note: Make sure that this is consistent with the C++ message API!
var data = Encoding.Unicode.GetBytes(value);
WriteUInt16((ushort)data.Length); // TODO: Use 1-byte length when possible
WriteBytes(data, data.Length * sizeof(ushort));
}
/// <summary>
/// Reads and returns data of type <see cref="string"/> from the message. UTF-16 encoded.
/// </summary>
public string ReadString()
{
// Note: Make sure that this is consistent with the C++ message API!
var stringLength = ReadUInt16(); // In chars
var stringSize = stringLength * sizeof(ushort); // In bytes
var bytes = stackalloc char[stringSize];
ReadBytes((byte*)bytes, stringSize);
return new string(bytes);
}
/// <summary>
/// Writes data of type <see cref="Vector2"/> into the message.
/// </summary>
public void WriteVector2(Vector2 value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
}
/// <summary>
/// Reads and returns data of type <see cref="Vector2"/> from the message.
/// </summary>
public Vector2 ReadVector2()
{
return new Vector2(ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type <see cref="Vector3"/> into the message.
/// </summary>
public void WriteVector3(Vector3 value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
}
/// <summary>
/// Reads and returns data of type <see cref="Vector3"/> from the message.
/// </summary>
public Vector3 ReadVector3()
{
return new Vector3(ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type <see cref="Vector4"/> into the message.
/// </summary>
public void WriteVector4(Vector4 value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
WriteSingle(value.W);
}
/// <summary>
/// Reads and returns data of type <see cref="Vector4"/> from the message.
/// </summary>
public Vector4 ReadVector4()
{
return new Vector4(ReadSingle(), ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type <see cref="Quaternion"/> into the message.
/// </summary>
public void WriteQuaternion(Quaternion value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
WriteSingle(value.W);
}
/// <summary>
/// Reads and returns data of type <see cref="Quaternion"/> from the message.
/// </summary>
public Quaternion ReadQuaternion()
{
return new Quaternion(ReadSingle(), ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type <see cref="Boolean"/> into the message.
/// </summary>
public void WriteBoolean(bool value)
{
WriteBytes((byte*)&value, sizeof(bool));
}
/// <summary>
/// Reads and returns data of type <see cref="Boolean"/> from the message.
/// </summary>
public bool ReadBoolean()
{
bool value = default;
ReadBytes((byte*)&value, sizeof(bool));
return value;
}
}
}

View File

@@ -0,0 +1,213 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Math/Vector2.h"
#include "Engine/Core/Math/Vector3.h"
#include "Engine/Core/Math/Vector4.h"
#include "Engine/Core/Math/Quaternion.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Network message structure. Provides raw data writing and reading to the message buffer.
/// </summary>
API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkMessage
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkMessage);
public:
/// <summary>
/// The raw message buffer.
/// </summary>
API_FIELD()
uint8* Buffer = nullptr;
/// <summary>
/// The unique, internal message identifier.
/// </summary>
API_FIELD()
uint32 MessageId = 0;
/// <summary>
/// The size in bytes of the buffer that this message has.
/// </summary>
API_FIELD()
uint32 BufferSize = 0;
/// <summary>
/// The length in bytes of this message.
/// </summary>
API_FIELD()
uint32 Length = 0;
/// <summary>
/// The position in bytes in buffer where the next read/write will occur.
/// </summary>
API_FIELD()
uint32 Position = 0;
public:
/// <summary>
/// Initializes default values of the <seealso cref="NetworkMessage"/> structure.
/// </summary>
NetworkMessage() = default;
/// <summary>
/// Initializes values of the <seealso cref="NetworkMessage"/> structure.
/// </summary>
NetworkMessage(uint8* buffer, uint32 messageId, uint32 bufferSize, uint32 length, uint32 position) :
Buffer(buffer), MessageId(messageId), BufferSize(bufferSize), Length(length), Position(position)
{ }
~NetworkMessage() = default;
public:
/// <summary>
/// Writes raw bytes into the message.
/// </summary>
/// <param name="bytes">The bytes that will be written.</param>
/// <param name="numBytes">The amount of bytes to write from the bytes pointer.</param>
FORCE_INLINE void WriteBytes(uint8* bytes, const int numBytes)
{
ASSERT(Position + numBytes < BufferSize);
Platform::MemoryCopy(Buffer + Position, bytes, numBytes);
Position += numBytes;
}
/// <summary>
/// Reads raw bytes from the message into the given byte array.
/// </summary>
/// <param name="bytes">
/// The buffer pointer that will be used to store the bytes.
/// Should be of the same length as length or longer.
/// </param>
/// <param name="numBytes">The minimal amount of bytes that the buffer contains.</param>
FORCE_INLINE void ReadBytes(uint8* bytes, const int numBytes)
{
ASSERT(Position + numBytes < BufferSize);
Platform::MemoryCopy(bytes, Buffer + Position, numBytes);
Position += numBytes;
}
#define DECL_READWRITE(type, name) \
FORCE_INLINE void Write##name(type value) { WriteBytes(reinterpret_cast<uint8*>(&value), sizeof(type)); } \
FORCE_INLINE type Read##name() { type value = 0; ReadBytes(reinterpret_cast<uint8*>(&value), sizeof(type)); return value; }
public:
DECL_READWRITE(int8, Int8)
DECL_READWRITE(uint8, UInt8)
DECL_READWRITE(int16, Int16)
DECL_READWRITE(uint16, UInt16)
DECL_READWRITE(int32, Int32)
DECL_READWRITE(uint32, UInt32)
DECL_READWRITE(int64, Int64)
DECL_READWRITE(uint64, UInt64)
DECL_READWRITE(float, Single)
DECL_READWRITE(double, Double)
DECL_READWRITE(bool, Boolean)
public:
/// <summary>
/// Writes data of type Vector2 into the message.
/// </summary>
FORCE_INLINE void WriteVector2(const Vector2& value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
}
/// <summary>
/// Reads and returns data of type Vector2 from the message.
/// </summary>
FORCE_INLINE Vector2 ReadVector2()
{
return Vector2(ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type Vector3 into the message.
/// </summary>
FORCE_INLINE void WriteVector3(const Vector3& value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
}
/// <summary>
/// Reads and returns data of type Vector3 from the message.
/// </summary>
FORCE_INLINE Vector3 ReadVector3()
{
return Vector3(ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type Vector4 into the message.
/// </summary>
FORCE_INLINE void WriteVector4(const Vector4& value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
WriteSingle(value.W);
}
/// <summary>
/// Reads and returns data of type Vector4 from the message.
/// </summary>
FORCE_INLINE Vector4 ReadVector4()
{
return Vector4(ReadSingle(), ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type Quaternion into the message.
/// </summary>
FORCE_INLINE void WriteQuaternion(const Quaternion& value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
WriteSingle(value.W);
}
/// <summary>
/// Reads and returns data of type Quaternion from the message.
/// </summary>
FORCE_INLINE Quaternion ReadQuaternion()
{
return Quaternion(ReadSingle(), ReadSingle(), ReadSingle(), ReadSingle());
}
public:
/// <summary>
/// Writes data of type String into the message. UTF-16 encoded.
/// </summary>
FORCE_INLINE void WriteString(const String& value)
{
WriteUInt16(value.Length()); // TODO: Use 1-byte length when possible
WriteBytes((uint8*)value.Get(), value.Length() * sizeof(wchar_t));
}
/// <summary>
/// Reads and returns data of type String from the message. UTF-16 encoded.
/// </summary>
FORCE_INLINE String ReadString()
{
uint16 length = ReadUInt16();
String value;
value.Resize(length);
ReadBytes((uint8*)value.Get(), length * sizeof(wchar_t));
return value;
}
public:
/// <summary>
/// Returns true if the message is valid for reading or writing.
/// </summary>
bool IsValid() const
{
return Buffer != nullptr && BufferSize > 0;
}
};

View File

@@ -0,0 +1,192 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#include "NetworkPeer.h"
#include "NetworkEvent.h"
#include "Drivers/ENetDriver.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Platform/CPUInfo.h"
namespace
{
Array<NetworkPeer*, HeapAllocation> Peers;
uint32 LastHostId = 0;
}
void NetworkPeer::Initialize(const NetworkConfig& config)
{
Config = config;
ASSERT(NetworkDriver == nullptr);
ASSERT(Config.NetworkDriverType != NetworkDriverType::Undefined);
ASSERT(Config.ConnectionsLimit > 0);
ASSERT(Config.MessageSize > 32); // TODO: Adjust this, not sure what the lowest limit should be.
ASSERT(Config.MessagePoolSize > 128);
// TODO: Dynamic message pool allocation
// Setup messages
CreateMessageBuffers();
MessagePool.Clear();
// Warmup message pool
for (uint32 messageId = Config.MessagePoolSize; messageId > 0; messageId --)
MessagePool.Push(messageId);
// Setup network driver
NetworkDriver = New<ENetDriver>();
NetworkDriver->Initialize(this, Config);
LOG(Info, "NetworkManager initialized using driver = {0}", static_cast<int>(Config.NetworkDriverType));
}
void NetworkPeer::Shutdown()
{
NetworkDriver->Dispose();
Delete(NetworkDriver);
DisposeMessageBuffers();
NetworkDriver = nullptr;
}
void NetworkPeer::CreateMessageBuffers()
{
ASSERT(MessageBuffer == nullptr);
const uint32 pageSize = Platform::GetCPUInfo().PageSize;
// Calculate total size in bytes
const uint64 totalSize = static_cast<uint64>(Config.MessagePoolSize) * Config.MessageSize;
// Calculate the amount of pages that we need
const uint32 numPages = totalSize > pageSize ? Math::CeilToInt(totalSize / static_cast<float>(pageSize)) : 1;
MessageBuffer = static_cast<uint8*>(Platform::AllocatePages(numPages, pageSize));
Platform::MemorySet(MessageBuffer, 0, numPages * pageSize);
}
void NetworkPeer::DisposeMessageBuffers()
{
ASSERT(MessageBuffer != nullptr);
Platform::FreePages(MessageBuffer);
MessageBuffer = nullptr;
}
bool NetworkPeer::Listen()
{
LOG(Info, "NetworkManager starting to listen on address = {0}:{1}", Config.Address, Config.Port);
return NetworkDriver->Listen();
}
bool NetworkPeer::Connect()
{
LOG(Info, "Connecting to {0}:{1}...", Config.Address, Config.Port);
return NetworkDriver->Connect();
}
void NetworkPeer::Disconnect()
{
LOG(Info, "Disconnecting...");
NetworkDriver->Disconnect();
}
void NetworkPeer::Disconnect(const NetworkConnection& connection)
{
LOG(Info, "Disconnecting connection with id = {0}...", connection.ConnectionId);
NetworkDriver->Disconnect(connection);
}
bool NetworkPeer::PopEvent(NetworkEvent& eventRef)
{
return NetworkDriver->PopEvent(&eventRef);
}
NetworkMessage NetworkPeer::CreateMessage()
{
const uint32 messageId = MessagePool.Pop();
uint8* messageBuffer = GetMessageBuffer(messageId);
return NetworkMessage(messageBuffer, messageId, Config.MessageSize, 0, 0);
}
void NetworkPeer::RecycleMessage(const NetworkMessage& message)
{
ASSERT(message.IsValid());
#ifdef BUILD_DEBUG
ASSERT(MessagePool.Contains(message.MessageId) == false);
#endif
// Return the message id
MessagePool.Push(message.MessageId);
}
NetworkMessage NetworkPeer::BeginSendMessage()
{
return CreateMessage();
}
void NetworkPeer::AbortSendMessage(const NetworkMessage& message)
{
ASSERT(message.IsValid());
RecycleMessage(message);
}
bool NetworkPeer::EndSendMessage(const NetworkChannelType channelType, const NetworkMessage& message)
{
ASSERT(message.IsValid());
NetworkDriver->SendMessage(channelType, message);
RecycleMessage(message);
return false;
}
bool NetworkPeer::EndSendMessage(const NetworkChannelType channelType, const NetworkMessage& message, const NetworkConnection& target)
{
ASSERT(message.IsValid());
NetworkDriver->SendMessage(channelType, message, target);
RecycleMessage(message);
return false;
}
bool NetworkPeer::EndSendMessage(const NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection>& targets)
{
ASSERT(message.IsValid());
NetworkDriver->SendMessage(channelType, message, targets);
RecycleMessage(message);
return false;
}
NetworkPeer* NetworkPeer::CreatePeer(const NetworkConfig& config)
{
// Validate the address for listen/connect
NetworkEndPoint endPoint = {};
const bool isValidEndPoint = NetworkBase::CreateEndPoint(config.Address, String("7777"), NetworkIPVersion::IPv4, endPoint, false);
ASSERT(config.Address == String("any") || isValidEndPoint);
// Alloc new host
Peers.Add(New<NetworkPeer>());
NetworkPeer* host = Peers.Last();
host->HostId = LastHostId++;
// Initialize the host
host->Initialize(config);
return host;
}
void NetworkPeer::ShutdownPeer(NetworkPeer* peer)
{
ASSERT(peer->IsValid());
peer->Shutdown();
peer->HostId = -1;
Peers.Remove(peer);
Delete(peer);
}

View File

@@ -0,0 +1,194 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Scripting/ScriptingObjectReference.h"
#include "Types.h"
#include "NetworkConfig.h"
#include "Engine/Core/Collections/Array.h"
/// <summary>
/// Low-level network peer class. Provides server-client communication functions, message processing and sending.
/// </summary>
API_CLASS(sealed, NoSpawn, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkPeer final : public PersistentScriptingObject
{
DECLARE_SCRIPTING_TYPE_NO_SPAWN(NetworkPeer);
friend class NetworkManager;
public:
int HostId = -1;
NetworkConfig Config;
INetworkDriver* NetworkDriver = nullptr;
uint8* MessageBuffer = nullptr;
Array<uint32, HeapAllocation> MessagePool;
public:
/// <summary>
/// Initializes a new instance of the <see cref="NetworkPeer"/> class.
/// </summary>
NetworkPeer() : PersistentScriptingObject(SpawnParams(Guid::New(), TypeInitializer))
{
}
private:
void Initialize(const NetworkConfig& config);
void Shutdown();
private:
void CreateMessageBuffers();
void DisposeMessageBuffers();
public:
/// <summary>
/// Starts listening for incoming connections.
/// Once this is called, this peer becomes a server.
/// </summary>
/// <returns>True when succeeded.</returns>
API_FUNCTION()
bool Listen();
/// <summary>
/// Starts connection handshake with the end point specified in the <seealso cref="NetworkConfig"/> structure.
/// Once this is called, this peer becomes a client.
/// </summary>
/// <returns>True when succeeded.</returns>
API_FUNCTION()
bool Connect();
/// <summary>
/// Disconnects from the server.
/// </summary>
/// <remarks>Can be used only by the client!</remarks>
API_FUNCTION()
void Disconnect();
/// <summary>
/// Disconnects given connection from the server.
/// </summary>
/// <remarks>Can be used only by the server!</remarks>
API_FUNCTION()
void Disconnect(const NetworkConnection& connection);
/// <summary>
/// Tries to pop an network event from the queue.
/// </summary>
/// <param name="eventRef">The reference to event structure.</param>
/// <returns>True when succeeded and the event can be processed.</returns>
/// <remarks>If this returns message event, make sure to recycle the message using <see cref="RecycleMessage"/> function after processing it!</remarks>
API_FUNCTION()
bool PopEvent(API_PARAM(out) NetworkEvent& eventRef);
/// <summary>
/// Acquires new message from the pool.
/// Cannot acquire more messages than the limit specified in the <seealso cref="NetworkConfig"/> structure.
/// </summary>
/// <returns>The acquired message.</returns>
/// <remarks>Make sure to recycle the message to this peer once it is no longer needed!</remarks>
API_FUNCTION()
NetworkMessage CreateMessage();
/// <summary>
/// Returns given message to the pool.
/// </summary>
/// <remarks>Make sure that this message belongs to the peer and has not been recycled already (debug build checks for this)!</remarks>
API_FUNCTION()
void RecycleMessage(const NetworkMessage& message);
/// <summary>
/// Acquires new message from the pool and setups it for sending.
/// </summary>
/// <returns>The acquired message.</returns>
API_FUNCTION()
NetworkMessage BeginSendMessage();
/// <summary>
/// Aborts given message send. This effectively deinitializes the message and returns it to the pool.
/// </summary>
/// <param name="message">The message.</param>
API_FUNCTION()
void AbortSendMessage(const NetworkMessage& message);
/// <summary>
/// Sends given message over specified channel to the server.
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <remarks>Can be used only by the client!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
API_FUNCTION()
bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message);
/// <summary>
/// Sends given message over specified channel to the given client connection (target).
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <param name="target">The client connection to send the message to.</param>
/// <remarks>Can be used only by the server!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
API_FUNCTION()
bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message, const NetworkConnection& target);
/// <summary>
/// Sends given message over specified channel to the given client connection (target).
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <param name="targets">The connections list to send the message to.</param>
/// <remarks>Can be used only by the server!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
API_FUNCTION()
bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection, HeapAllocation>& targets);
public:
/// <summary>
/// Creates new peer using given configuration.
/// </summary>
/// <param name="config">The configuration to create and setup new peer.</param>
/// <returns>The peer.</returns>
/// <remarks>Peer should be destroyed using <see cref="ShutdownPeer"/> once it is no longer in use.</remarks>
API_FUNCTION()
static NetworkPeer* CreatePeer(const NetworkConfig& config);
/// <summary>
/// Shutdowns and destroys given peer.
/// </summary>
/// <param name="peer">The peer to destroy.</param>
API_FUNCTION()
static void ShutdownPeer(NetworkPeer* peer);
public:
bool IsValid() const
{
return NetworkDriver != nullptr && HostId >= 0;
}
uint8* GetMessageBuffer(const uint32 messageId) const
{
// Calculate and return the buffer slice using previously calculated slice.
return MessageBuffer + Config.MessageSize * messageId;
}
public:
FORCE_INLINE bool operator==(const NetworkPeer& other) const
{
return HostId == other.HostId;
}
FORCE_INLINE bool operator!=(const NetworkPeer& other) const
{
return HostId != other.HostId;
}
};

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// Networking module.
/// </summary>
public class Networking : EngineModule
{
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
options.PublicDefinitions.Add("COMPILE_WITH_NETWORKING");
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
enum class NetworkChannelType;
enum class NetworkEventType;
class INetworkDriver;
class NetworkPeer;
struct NetworkEvent;
struct NetworkConnection;
struct NetworkMessage;
struct NetworkConfig;