Initial commit

This commit is contained in:
alexpete
2021-03-05 11:26:34 -08:00
commit a10351f38d
27091 changed files with 5521199 additions and 0 deletions
@@ -0,0 +1,234 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/DtlsEndpoint.h>
#include <AzNetworking/UdpTransport/DtlsSocket.h>
#include <AzNetworking/Utilities/EncryptionCommon.h>
#include <AzNetworking/Utilities/NetworkIncludes.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Console/ILogger.h>
#if AZ_TRAIT_USE_OPENSSL
# include <openssl/ssl.h>
# include <openssl/err.h>
#endif
namespace AzNetworking
{
AZ_CVAR(bool, net_UseDtlsCookies, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Enables DTLS cookie exchange during the connection handshake");
DtlsEndpoint::DtlsEndpoint()
: m_state(HandshakeState::None)
, m_sslSocket(nullptr)
, m_readBio(nullptr)
, m_writeBio(nullptr)
{
;
}
DtlsEndpoint::~DtlsEndpoint()
{
Close(m_sslSocket); // Note this also closes any attached BIO instances
m_readBio = nullptr;
m_writeBio = nullptr;
}
DtlsEndpoint::ConnectResult DtlsEndpoint::Connect(const DtlsSocket& socket, const IpAddress& address, [[maybe_unused]] UdpPacketEncodingBuffer& outDtlsData)
{
const ConnectResult result = ConstructEndpointInternal(socket, address);
#if AZ_TRAIT_USE_OPENSSL
if (result != ConnectResult::Failed)
{
// This SSL should be configured to initiate connections
SSL_set_connect_state(m_sslSocket);
m_state = HandshakeState::Connecting;
return PerformHandshakeInternal(outDtlsData);
}
#endif
return result;
}
DtlsEndpoint::ConnectResult DtlsEndpoint::Accept(const DtlsSocket& socket, const IpAddress& address, const UdpPacketEncodingBuffer& dtlsData)
{
if (dtlsData.GetSize() <= 0)
{
AZLOG_WARN("Encryption is enabled on accepting endpoint, but connector provided an empty DTLS handshake blob. Check that encryption is properly disabled on *BOTH* endpoints");
return DtlsEndpoint::ConnectResult::Failed;
}
const ConnectResult result = ConstructEndpointInternal(socket, address);
#if AZ_TRAIT_USE_OPENSSL
if (result != ConnectResult::Failed)
{
// This SSL should be configured to accept connections
SSL_set_accept_state(m_sslSocket);
m_state = HandshakeState::Accepting;
const uint8_t* encryptedData = dtlsData.GetBuffer();
const uint32_t encryptedSize = dtlsData.GetSize();
BIO_write(m_readBio, encryptedData, encryptedSize);
return CompleteHandshake(socket);
}
#endif
return result;
}
bool DtlsEndpoint::IsConnecting() const
{
return ((m_state == HandshakeState::Connecting)
|| (m_state == HandshakeState::Accepting)
|| (m_state == HandshakeState::Failed)); // In all cases caller should call CompleteHandshake() next and check the return value
}
DtlsEndpoint::ConnectResult DtlsEndpoint::CompleteHandshake(const UdpSocket& socket)
{
UdpPacketEncodingBuffer responseData;
const ConnectResult result = PerformHandshakeInternal(responseData);
if ((result != ConnectResult::Failed) && (responseData.GetSize() > 0))
{
struct sockaddr_in dest;
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_addr.s_addr = m_address.GetAddress(ByteOrder::Network);
dest.sin_port = m_address.GetPort(ByteOrder::Network);
sendto(static_cast<int32_t>(socket.GetSocketFd()), reinterpret_cast<char*>(responseData.GetBuffer()), responseData.GetSize(), 0, (sockaddr*)&dest, sizeof(dest));
AZLOG(NET_DebugDtls, "Replying to DTLS handshake datagram, %u bytes", static_cast<int32_t>(responseData.GetSize()));
}
return result;
}
const uint8_t* DtlsEndpoint::DecodePacket
(
[[maybe_unused]] const UdpSocket& socket,
[[maybe_unused]] const uint8_t* encryptedData,
[[maybe_unused]] int32_t encryptedSize,
[[maybe_unused]] uint8_t* outDecodedData,
[[maybe_unused]] int32_t& outDecodedSize
)
{
if (m_sslSocket == nullptr)
{
// If the ssl socket is nullptr, it means encryption is not enabled, just passthrough the received data
outDecodedSize = encryptedSize;
return encryptedData;
}
#if AZ_TRAIT_USE_OPENSSL
BIO_write(m_readBio, encryptedData, encryptedSize);
if (IsConnecting())
{
CompleteHandshake(socket);
outDecodedSize = 0;
}
// CompleteHandshake() above may have failed and destroyed the SSL context, check here that state is valid so we don't crash on SSL_read
if (m_state != HandshakeState::Failed)
{
outDecodedSize = SSL_read(m_sslSocket, outDecodedData, encryptedSize);
}
#endif
return outDecodedData;
}
DtlsEndpoint::ConnectResult DtlsEndpoint::ConstructEndpointInternal([[maybe_unused]] const DtlsSocket& socket, [[maybe_unused]] const IpAddress& address)
{
if (m_sslSocket != nullptr)
{
AZLOG_WARN("An existing SSL socket was open during a call to connect, closing old socket");
Close(m_sslSocket); // Note this also closes any attached BIO instances
}
#if AZ_TRAIT_USE_OPENSSL
m_address = address;
m_sslSocket = SSL_new(socket.m_sslContext);
m_readBio = BIO_new(BIO_s_mem());
BIO_set_mem_eof_return(m_readBio, -1);
m_writeBio = BIO_new(BIO_s_mem());
BIO_set_mem_eof_return(m_writeBio, -1);
SSL_set_bio(m_sslSocket, m_readBio, m_writeBio);
if (net_UseDtlsCookies)
{
SSL_set_options(m_sslSocket, SSL_OP_COOKIE_EXCHANGE);
}
if (m_sslSocket == nullptr)
{
AZLOG_ERROR("SSL_new failed, could not create SSL socket wrapper instance");
PrintSslErrorStack();
return ConnectResult::Failed;
}
#endif
return ConnectResult::Pending;
}
DtlsEndpoint::ConnectResult DtlsEndpoint::PerformHandshakeInternal([[maybe_unused]] UdpPacketEncodingBuffer& outHandshakeData)
{
if (m_state == HandshakeState::Failed)
{
return ConnectResult::Failed;
}
ConnectResult connectResult = ConnectResult::Pending;
#if AZ_TRAIT_USE_OPENSSL
if (SSL_is_init_finished(m_sslSocket))
{
const char* stateString = GetEnumString(m_state);
AZLOG(NET_DebugDtls, "dtls handshake is completed, unblocking connection for game traffic, prior state: %s", stateString);
m_state = HandshakeState::Complete;
connectResult = ConnectResult::Complete;
}
ERR_clear_error();
const int32_t result = SSL_do_handshake(m_sslSocket);
if (result <= 0)
{
const int32_t error = SSL_get_error(m_sslSocket, result);
if ((error != SSL_ERROR_WANT_READ)
&& (error != SSL_ERROR_WANT_WRITE))
{
AZLOG_ERROR("SSL handshake negotiation failed (%d), terminating connection", error);
PrintSslErrorStack();
Close(m_sslSocket);
m_readBio = nullptr;
m_writeBio = nullptr;
m_state = HandshakeState::Failed;
connectResult = ConnectResult::Failed;
}
}
// Need to do this... connection negotiation may have left data in the write bio that we need to send out
if (BIO_ctrl_pending(m_writeBio) > 0)
{
const uint32_t maxBufferSize = outHandshakeData.GetCapacity();
outHandshakeData.Resize(maxBufferSize);
const int32_t dataSize = BIO_read(m_writeBio, outHandshakeData.GetBuffer(), maxBufferSize);
outHandshakeData.Resize(dataSize);
}
#else
connectResult = ConnectResult::Complete;
#endif
return connectResult;
}
const char* GetEnumString(DtlsEndpoint::HandshakeState value)
{
switch (value)
{
case DtlsEndpoint::HandshakeState::None:
return "None";
case DtlsEndpoint::HandshakeState::Connecting:
return "Connecting";
case DtlsEndpoint::HandshakeState::Accepting:
return "Accepting";
case DtlsEndpoint::HandshakeState::Complete:
return "Complete";
case DtlsEndpoint::HandshakeState::Failed:
return "Failed";
}
return "UNKNOWN";
}
}
@@ -0,0 +1,110 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/Utilities/IpAddress.h>
#include <AzNetworking/DataStructures/ByteBuffer.h>
// OpenSSL forward declarations
typedef struct ssl_st SSL;
typedef struct ssl_ctx_st SSL_CTX;
typedef struct bio_st BIO;
namespace AzNetworking
{
class UdpSocket;
class DtlsSocket;
//! @class DtlsEndpoint
//! @brief Helper class defining an encrypted DTLS endpoint.
//! Note that multiple connections are multiplexed onto a single DTLS socket
class DtlsEndpoint final
{
friend class DtlsSocket;
public:
enum class ConnectResult
{
Failed,
Pending,
Complete
};
enum class HandshakeState
{
None, // Not an active dtls endpoint, Connect has not been called
Connecting, // This is a connecting endpoint, initiating the connection
Accepting, // This is an accepting endpoint
Complete, // Handshake is complete, connection is established and encrypted
Failed // Handshake failed
};
DtlsEndpoint();
~DtlsEndpoint();
//! Opens a connection with the remote encrypted endpoint.
//! @param socket the dtls socket being used for data transmission
//! @param address the address of the remote endpoint being connected to
//! @param outDtlsData data buffer to store the dtls handshake packet
//! @return a connect result specifying whether the connection is still pending, failed, or complete
ConnectResult Connect(const DtlsSocket& socket, const IpAddress& address, UdpPacketEncodingBuffer& outDtlsData);
//! Accepts a connection from the remote encrypted endpoint.
//! @param socket the dtls socket being used for data transmission
//! @param address the address of the remote endpoint connecting to us
//! @param dtlsData data buffer containing the initial dtls handshake packet
//! @return a connect result specifying whether the connection is still pending, failed, or complete
ConnectResult Accept(const DtlsSocket& socket, const IpAddress& address, const UdpPacketEncodingBuffer& dtlsData);
//! Returns whether or not the endpoint is still negotiating the dtls handshake.
//! @return true if the endpoint is still in a connecting state
bool IsConnecting() const;
//! Attempts to complete the dtls handshake and establish an encrypted connection.
//! @param socket the dtls socket being used for data transmission
//! @return a connect result specifying whether the connection is still pending, failed, or complete
ConnectResult CompleteHandshake(const UdpSocket& socket);
//! If the endpoint has encryption enabled, this will decrypt the transmitted data and return the result.
//! @note sizes have to be signed since OpenSSL often returns negative values to represent error results
//! @param socket the DTLS socket being used for data transmission
//! @param encryptedData the potentially encrypted data received from the socket
//! @param encryptedSize the size of the received raw data
//! @param outDecodedData an appropriately sized output buffer to store decrypted data
//! @param outDecodedSize the size of the output buffer
//! @return pointer to the decoded data
const uint8_t* DecodePacket(const UdpSocket& socket, const uint8_t* encryptedData, int32_t encryptedSize, uint8_t* outDecodedData, int32_t& outDecodedSize);
private:
//! Performs internal common dtls endpoint setup.
//! @param socket the dtls socket being used for data transmission
//! @param address the address of the remote endpoint connecting to us
//! @return a connect result specifying whether the connection is still pending, failed, or complete
ConnectResult ConstructEndpointInternal(const DtlsSocket& socket, const IpAddress& address);
//! Attempts to complete the dtls handshake and establish an encrypted connection.
//! @param outHandshakeData buffer to store any required outgoing dtls handshake data
//! @return a connect result specifying whether the connection is still pending, failed, or complete
ConnectResult PerformHandshakeInternal(UdpPacketEncodingBuffer& outHandshakeData);
HandshakeState m_state;
IpAddress m_address;
SSL* m_sslSocket;
BIO* m_readBio;
BIO* m_writeBio;
};
const char* GetEnumString(DtlsEndpoint::HandshakeState value);
}
@@ -0,0 +1,99 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/DtlsSocket.h>
#include <AzCore/Console/ILogger.h>
#if AZ_TRAIT_USE_OPENSSL
# include <openssl/ssl.h>
# include <openssl/err.h>
#endif
namespace AzNetworking
{
DtlsSocket::~DtlsSocket()
{
FreeSslContext(m_sslContext);
}
bool DtlsSocket::IsEncrypted() const
{
return true;
}
DtlsEndpoint::ConnectResult DtlsSocket::ConnectDtlsEndpoint(DtlsEndpoint& dtlsEndpoint, const IpAddress& address, UdpPacketEncodingBuffer& outDtlsData) const
{
return dtlsEndpoint.Connect(*this, address, outDtlsData);
}
DtlsEndpoint::ConnectResult DtlsSocket::AcceptDtlsEndpoint(DtlsEndpoint& dtlsEndpoint, const IpAddress& address, const UdpPacketEncodingBuffer& dtlsData) const
{
return dtlsEndpoint.Accept(*this, address, dtlsData);
}
bool DtlsSocket::Open(uint16_t port, CanAcceptConnections canAccept, TrustZone trustZone)
{
Close();
const SslContextType contextType = (canAccept == UdpSocket::CanAcceptConnections::True) ? SslContextType::DtlsGeneric : SslContextType::DtlsClient;
m_sslContext = CreateSslContext(contextType, trustZone);
if (m_sslContext == nullptr)
{
AZLOG_ERROR("SSL context creation call failed");
PrintSslErrorStack();
Close();
return false;
}
if (!UdpSocket::Open(port, canAccept, trustZone))
{
AZLOG_ERROR("UDP socket creation failed");
PrintSslErrorStack();
Close();
return false;
}
return true;
}
void DtlsSocket::Close()
{
FreeSslContext(m_sslContext);
UdpSocket::Close();
}
int32_t DtlsSocket::SendInternal(const IpAddress& address, const uint8_t* data, uint32_t size, bool encrypt, DtlsEndpoint& dtlsEndpoint) const
{
if (!encrypt)
{
// If the packet has requested to remain unencrypted then just send directly
return UdpSocket::SendInternal(address, data, size, encrypt, dtlsEndpoint);
}
if (dtlsEndpoint.m_sslSocket == nullptr)
{
AZLOG_ERROR("Trying to send on an open socketfd, but with a nullptr ssl socket wrapper!");
return SocketOpResultErrorNoSsl;
}
#if AZ_TRAIT_USE_OPENSSL
uint8_t encrpytedSendBuffer[MaxUdpTransmissionUnit];
// Write out the packet we were requested to send
const int32_t sentBytesRaw = SSL_write(dtlsEndpoint.m_sslSocket, data, size);
const int32_t sentBytesEnc = BIO_read(dtlsEndpoint.m_writeBio, encrpytedSendBuffer, sizeof(encrpytedSendBuffer));
return UdpSocket::SendInternal(address, encrpytedSendBuffer, sentBytesEnc, encrypt, dtlsEndpoint);
#else
return 0;
#endif
}
}
@@ -0,0 +1,69 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/UdpTransport/UdpSocket.h>
#include <AzNetworking/ConnectionLayer/IConnection.h>
#include <AzNetworking/Utilities/EncryptionCommon.h>
namespace AzNetworking
{
class DtlsEndpoint;
//! @class DtlsSocket
//! @brief wrapper class for managing encrypted Udp sockets.
class DtlsSocket final
: public UdpSocket
{
friend class DtlsEndpoint;
public:
DtlsSocket() = default;
~DtlsSocket();
//! Returns true if this is an encrypted socket, false if not.
//! @return boolean true if this is an encrypted socket, false if not
bool IsEncrypted() const override;
//! Creates an encryption socket wrapper.
//! @param dtlsEndpoint the encryption wrapper instance to create a connection over
//! @param address the IP address of the endpoint to connect to
//! @param outDtlsData data buffer to store the dtls handshake packet
//! @return a connect result specifying whether the connection is still pending, failed, or complete
DtlsEndpoint::ConnectResult ConnectDtlsEndpoint(DtlsEndpoint& dtlsEndpoint, const IpAddress& address, UdpPacketEncodingBuffer& outDtlsData) const override;
//! Accepts an encryption socket wrapper.
//! @param dtlsEndpoint the encryption wrapper instance to create a connection over
//! @param address the IP address of the endpoint to connect to
//! @param dtlsData data buffer containing the dtls handshake packet
//! @return a connect result specifying whether the connection is still pending, failed, or complete
DtlsEndpoint::ConnectResult AcceptDtlsEndpoint(DtlsEndpoint& dtlsEndpoint, const IpAddress& address, const UdpPacketEncodingBuffer& dtlsData) const override;
//! Opens the UDP socket on the given port.
//! @param port the port number to open the UDP socket on, 0 will bind to any available port
//! @param canAccept if true, the socket will be opened in a way that allows accepting incoming connections
//! @param trustZone for encrypted connections, the level of trust we associate with this connection (internal or external)
//! @return boolean true on success
bool Open(uint16_t port, UdpSocket::CanAcceptConnections canAccept, TrustZone trustZone) override;
//! Closes an open socket.
void Close() override;
private:
int32_t SendInternal(const IpAddress& address, const uint8_t* data, uint32_t size, bool encrypt, DtlsEndpoint& dtlsEndpoint) const override;
SSL_CTX* m_sslContext = nullptr;
};
}
@@ -0,0 +1,288 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/UdpConnection.h>
#include <AzNetworking/UdpTransport/UdpPacketTracker.h>
#include <AzNetworking/UdpTransport/UdpNetworkInterface.h>
#include <AzNetworking/Serialization/NetworkInputSerializer.h>
#include <AzNetworking/Serialization/NetworkOutputSerializer.h>
#include <AzNetworking/Serialization/TrackChangedSerializer.h>
#include <AzNetworking/AutoGen/CorePackets.AutoPackets.h>
#include <AzNetworking/Utilities/NetworkCommon.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Console/ILogger.h>
namespace AzNetworking
{
AZ_CVAR(uint32_t, net_UdpMaxUnackedPacketCount, 10, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Maximum packets to receive before forcing a heartbeat packet for acking");
// Track every 8th packet to determine Rtt
// Only reason we're doing every 8th packet instead of every packet is to reduce per-packet overhead
static const uint32_t PacketRttMask = 0x07;
static_assert(AZ::IsPowerOfTwo(PacketRttMask + 1), "Sequence mask should be of the form 2^N - 1");
static bool IncludePacketInRtt(PacketId packetId)
{
return ((static_cast<uint32_t>(packetId) & PacketRttMask) == 0);
}
const char* GetEnumString(PacketTimeoutResult value)
{
switch (value)
{
case PacketTimeoutResult::Acked:
return "PacketTimeoutResult::Acked";
case PacketTimeoutResult::Lost:
return "PacketTimeoutResult::Lost";
case PacketTimeoutResult::Pending:
return "PacketTimeoutResult::Pending";
}
return "INVALID";
}
UdpConnection::UdpConnection(ConnectionId connectionId, const IpAddress& remoteAddress, UdpNetworkInterface& networkInterface, ConnectionRole connectionRole)
: IConnection(connectionId, remoteAddress)
, m_networkInterface(networkInterface)
, m_lastSentPacketMs(AZ::GetElapsedTimeMs())
, m_connectionRole(connectionRole)
{
;
}
UdpConnection::~UdpConnection()
{
if (m_state == ConnectionState::Connected)
{
m_networkInterface.GetConnectionListener().OnDisconnect(this, DisconnectReason::ConnectionDeleted, TerminationEndpoint::Local);
}
}
DtlsEndpoint::ConnectResult UdpConnection::CompleteHandshake()
{
const DtlsEndpoint::ConnectResult result = m_dtlsEndpoint.CompleteHandshake(*(m_networkInterface.m_socket));
if (result == DtlsEndpoint::ConnectResult::Failed)
{
Disconnect(DisconnectReason::NetworkError, TerminationEndpoint::Local);
}
return result;
}
void UdpConnection::UpdateHeartbeat([[maybe_unused]] AZ::TimeMs currentTimeMs)
{
if (m_unackedPacketCount >= net_UdpMaxUnackedPacketCount)
{
AZLOG(NET_Acks, "Unacked packet count exceeded, sending client heartbeat (curr %u : max %u)", m_unackedPacketCount, static_cast<uint32_t>(net_UdpMaxUnackedPacketCount));
// This simply times out unreliable chunks that haven't completed within our timeout delay
m_fragmentQueue.Update();
SendUnreliablePacket(CorePackets::HeartbeatPacket());
}
}
bool UdpConnection::SendReliablePacket(const IPacket& packet)
{
const SequenceId reliableSequenceId = m_reliableQueue.GetNextSequenceId();
return (m_networkInterface.SendPacket(*this, packet, reliableSequenceId) != InvalidPacketId);
}
PacketId UdpConnection::SendUnreliablePacket(const IPacket& packet)
{
return m_networkInterface.SendPacket(*this, packet, InvalidSequenceId);
}
bool UdpConnection::WasPacketAcked(PacketId packetId) const
{
return m_packetTracker.GetPacketAckStatus(packetId) == PacketAckState::Acked;
}
ConnectionState UdpConnection::GetConnectionState() const
{
return m_state;
}
ConnectionRole UdpConnection::GetConnectionRole() const
{
return m_connectionRole;
}
bool UdpConnection::Disconnect(DisconnectReason reason, TerminationEndpoint endpoint)
{
if (m_state == ConnectionState::Disconnecting)
{
AZStd::string reasonString = ToString(reason);
AZLOG_ERROR("Disconnecting an already disconnecting connection due to %s", reasonString.c_str());
return false;
}
m_state = ConnectionState::Disconnecting;
if (endpoint == TerminationEndpoint::Local
&& reason != DisconnectReason::NetworkError
&& reason != DisconnectReason::DtlsHandshakeError
&& reason != DisconnectReason::Unknown
&& reason != DisconnectReason::RemoteHostClosedConnection
&& reason != DisconnectReason::TransportError
&& reason != DisconnectReason::SslFailure)
{
// If disconnect initiated from Local, inform the remote endpoint
CorePackets::TerminateConnectionPacket terminationPacket(reason);
SendUnreliablePacket(terminationPacket);
}
m_networkInterface.RequestDisconnect(this, reason, endpoint);
return true;
}
void UdpConnection::SetConnectionMtu(uint32_t connectionMtu)
{
m_connectionMtu = connectionMtu;
}
uint32_t UdpConnection::GetConnectionMtu() const
{
return m_connectionMtu;
}
void UdpConnection::ProcessAcked(PacketId packetId, AZ::TimeMs currentTimeMs)
{
GetMetrics().m_packetsAcked++;
m_reliableQueue.OnPacketAcked(m_networkInterface, *this, packetId);
// Compute Rtt adjustments
if (IncludePacketInRtt(packetId))
{
GetMetrics().m_connectionRtt.LogPacketAcked(packetId, currentTimeMs);
}
}
void UdpConnection::ProcessSent(PacketId packetId, [[maybe_unused]] const IPacket& packet,
uint32_t packetSize, [[maybe_unused]] ReliabilityType reliability)
{
const AZ::TimeMs currentTimeMs = AZ::GetElapsedTimeMs();
if (IncludePacketInRtt(packetId))
{
GetMetrics().m_connectionRtt.LogPacketSent(packetId, currentTimeMs);
}
GetMetrics().m_packetsSent++;
GetMetrics().m_sendDatarate.LogPacket(packetSize, currentTimeMs);
m_lastSentPacketMs = currentTimeMs;
m_unackedPacketCount = 0;
}
PacketTimeoutResult UdpConnection::ProcessTimeout(PacketId packetId, ReliabilityType reliability)
{
if (IncludePacketInRtt(packetId))
{
GetMetrics().m_connectionRtt.LogPacketTimeout(packetId);
}
const PacketAckState ackState = m_packetTracker.GetPacketAckStatus(packetId);
switch (ackState)
{
case PacketAckState::Acked:
return PacketTimeoutResult::Acked;
case PacketAckState::Nacked:
GetMetrics().m_packetsLost++;
if (reliability == ReliabilityType::Reliable)
{
m_reliableQueue.OnPacketLost(m_networkInterface, *this, packetId);
}
return PacketTimeoutResult::Lost;
case PacketAckState::Unknown_TooNew:
return PacketTimeoutResult::Pending;
case PacketAckState::Unknown_TooOld:
// TODO: Disconnect?
AZLOG_ERROR("PacketId %u timeout fell outside the ack history window", static_cast<uint32_t>(packetId));
break;
default:
AZLOG_ERROR("PacketId %u ack state was unhandled (%s)", static_cast<uint32_t>(packetId), GetEnumString(ackState));
break;
}
return PacketTimeoutResult::Lost;
}
bool UdpConnection::ProcessReceived(UdpPacketHeader& header, [[maybe_unused]] const NetworkOutputSerializer& serializer,
uint32_t packetSize, AZ::TimeMs currentTimeMs)
{
if (!m_packetTracker.ProcessReceived(this, header))
{
return false;
}
GetMetrics().m_packetsRecv++;
GetMetrics().m_recvDatarate.LogPacket(packetSize, currentTimeMs);
if (header.GetIsReliable() && !m_reliableQueue.OnPacketReceived(header))
{
return false;
}
m_unackedPacketCount++;
UpdateHeartbeat(currentTimeMs);
return true;
}
bool UdpConnection::HandleCorePacket(IConnectionListener& connectionListener, UdpPacketHeader& header, ISerializer& serializer)
{
switch (static_cast<CorePackets::PacketType>(header.GetPacketType()))
{
case CorePackets::PacketType::InitiateConnectionPacket:
{
AZLOG(NET_CorePackets, "Received core packet %s", "InitiateConnection");
return true;
}
break;
case CorePackets::PacketType::TerminateConnectionPacket:
{
AZLOG(NET_CorePackets, "Received core packet %s", "TerminateConnection");
CorePackets::TerminateConnectionPacket packet;
if (!serializer.Serialize(packet, "Packet"))
{
return false;
}
Disconnect(packet.GetDisconnectReason(), TerminationEndpoint::Remote);
return true;
}
break;
case CorePackets::PacketType::HeartbeatPacket:
{
AZLOG(NET_CorePackets, "Received core packet %s", "Heartbeat");
CorePackets::HeartbeatPacket packet;
if (!serializer.Serialize(packet, "Packet"))
{
return false;
}
// Do nothing, we've already processed our ack packets
return true;
}
break;
case CorePackets::PacketType::FragmentedPacket:
AZLOG(NET_CorePackets, "Received core packet %s", "Fragment");
return m_fragmentQueue.ProcessReceivedChunk(this, connectionListener, header, serializer);
default:
AZ_Assert(false, "Unhandled core packet type!");
}
return false;
}
}
@@ -0,0 +1,165 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/ConnectionLayer/IConnection.h>
#include <AzNetworking/ConnectionLayer/IConnectionListener.h>
#include <AzNetworking/UdpTransport/DtlsEndpoint.h>
#include <AzNetworking/UdpTransport/UdpPacketTracker.h>
#include <AzNetworking/UdpTransport/UdpReliableQueue.h>
#include <AzNetworking/UdpTransport/UdpFragmentQueue.h>
#include <AzCore/Console/ILogger.h>
namespace AzNetworking
{
// Forwards
class UdpPacketHeader;
enum class PacketTimeoutResult
{
Acked,
Lost,
Pending
};
const char* GetEnumString(PacketTimeoutResult value);
//! @class UdpConnection
//! @brief Connection class for udp endpoints.
class UdpConnection
: public IConnection
{
friend class UdpNetworkInterface;
public:
//! Constructor
//! @param connectionId the connection identifier to use for this connection
//! @param remoteAddress the remote address this connection
//! @param networkInterface reference of the network interface that owns this connection instance
//! @param connectionRole whether this connection was the connector or acceptor
UdpConnection(ConnectionId connectionId, const IpAddress& remoteAddress, UdpNetworkInterface& networkInterface, ConnectionRole connectionRole);
~UdpConnection() override;
//! Helper to complete dtls handshake logic on a newly established connection
//! @return the current result code for the dtls handshake operation, failed, pending, or complete
DtlsEndpoint::ConnectResult CompleteHandshake();
//! Updates the connection heartbeat if active.
//! @param currentTimeMs current wall clock time in milliseconds
void UpdateHeartbeat(AZ::TimeMs currentTimeMs);
//! IConnection interface.
// @{
bool SendReliablePacket(const IPacket& packet) override;
PacketId SendUnreliablePacket(const IPacket& packet) override;
bool WasPacketAcked(PacketId packetId) const override;
ConnectionState GetConnectionState() const override;
ConnectionRole GetConnectionRole() const override;
bool Disconnect(DisconnectReason reason, TerminationEndpoint endpoint) override;
void SetConnectionMtu(uint32_t connectionMtu) override;
uint32_t GetConnectionMtu() const override;
void SetConnectionQuality(const ConnectionQuality& connectionQuality) override;
// @}
//! Gets connection quality values for testing poor connection conditions.
//! @return connection quality values for this IConnection instance
const ConnectionQuality& GetConnectionQuality() const;
//! Returns a suitable encryption endpoint for this connection type.
//! @return reference to the connections encryption endpoint
DtlsEndpoint& GetDtlsEndpoint();
//! Retrieves packet delivery tracker instance for the specified connection.
//! @return reference to the requested packet tracker instance
const UdpPacketTracker& GetPacketTracker() const;
//! Retrieves packet delivery tracker instance for the specified connection.
//! @return reference to the requested packet tracker instance
UdpPacketTracker& GetPacketTracker();
//! Returns the number of unacked reliable messages still pending in the reliable queue.
//! @return the number of unacked reliable messages still pending in the reliable queue
uint32_t GetReliableQueueSize() const;
//! Acks a packetId.
//! @param packetId the PacketId of the packet being acked
//! @param currentTimeMs current wall clock time in milliseconds
void ProcessAcked(PacketId packetId, AZ::TimeMs currentTimeMs);
//! Sets the timeout identifier for this connection instance.
//! @param timeoutId the timeoutId to use for this connection instance
void SetTimeoutId(TimeoutId timeoutId);
//! Retrieves the timeout identifier for this connection instance.
//! @return the timeout identifier for this connection instance
TimeoutId GetTimeoutId() const;
protected:
//! Prepare a reliable packet for transmission.
//! @param packetId identifier of the packet being sent
//! @param packet reference to the packet being transmitted
//! @return boolean true on success, false on failure
bool PrepareReliablePacketForSend(PacketId packetId, SequenceId reliableSequenceId, const IPacket& packet);
//! Process a packet for sending.
//! @param packetId identifier of the packet being sent
//! @param packet reference to the packet being transmitted
//! @param packetSize packet size in bytes
//! @param reliability whether or not to guarantee delivery
void ProcessSent(PacketId packetId, const IPacket& packet, uint32_t packetSize, ReliabilityType reliability);
//! Process a timed out packet header.
//! @param packetId identifier of the packet that timed out
//! @param reliability whether or not the packet that timed out was marked reliable
//! @return PacketTimeoutResult::Acked if the packet was confirmed to be received prior to timeout, PacketTimeoutResult::Lost if not
PacketTimeoutResult ProcessTimeout(PacketId packetId, ReliabilityType reliability);
//! Process a received packet header.
//! @param header the packet header received to process
//! @param serializer the output serializer containing the transmitted packet data
//! @param packetSize the size of the received packet in bytes
//! @param currentTimeMs current wall clock time in milliseconds
//! @return boolean true on successful handling of the received header
bool ProcessReceived(UdpPacketHeader& header, const NetworkOutputSerializer& serializer, uint32_t packetSize, AZ::TimeMs currentTimeMs);
//! Handle a core network packet.
//! @param listener a connection listener to receive connection related events
//! @param header the packet header received to process
//! @param serializer the output serializer containing the transmitted packet data
//! @return boolean true on successful handling of the received header
bool HandleCorePacket(IConnectionListener& listener, UdpPacketHeader& header, ISerializer& serializer);
AZ_DISABLE_COPY_MOVE(UdpConnection);
UdpNetworkInterface& m_networkInterface;
UdpPacketTracker m_packetTracker;
UdpReliableQueue m_reliableQueue;
UdpFragmentQueue m_fragmentQueue;
ConnectionState m_state = ConnectionState::Disconnected;
ConnectionRole m_connectionRole = ConnectionRole::Connector;
ConnectionQuality m_connectionQuality;
DtlsEndpoint m_dtlsEndpoint;
AZ::TimeMs m_lastSentPacketMs;
uint32_t m_unackedPacketCount = 0;
uint32_t m_connectionMtu = MaxUdpTransmissionUnit;
TimeoutId m_timeoutId;
uint32_t m_timeoutCounter = 0;
};
}
#include <AzNetworking/UdpTransport/UdpConnection.inl>
@@ -0,0 +1,61 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
namespace AzNetworking
{
inline void UdpConnection::SetConnectionQuality(const ConnectionQuality& connectionQuality)
{
m_connectionQuality = connectionQuality;
}
inline const ConnectionQuality& UdpConnection::GetConnectionQuality() const
{
return m_connectionQuality;
}
inline DtlsEndpoint& UdpConnection::GetDtlsEndpoint()
{
return m_dtlsEndpoint;
}
inline const UdpPacketTracker& UdpConnection::GetPacketTracker() const
{
return m_packetTracker;
}
inline UdpPacketTracker& UdpConnection::GetPacketTracker()
{
return m_packetTracker;
}
inline uint32_t UdpConnection::GetReliableQueueSize() const
{
return m_reliableQueue.GetQueueSize();
}
inline void UdpConnection::SetTimeoutId(TimeoutId timeoutId)
{
m_timeoutId = timeoutId;
}
inline TimeoutId UdpConnection::GetTimeoutId() const
{
return m_timeoutId;
}
inline bool UdpConnection::PrepareReliablePacketForSend(PacketId packetId, SequenceId reliableSequenceId, const IPacket& packet)
{
return m_reliableQueue.PrepareForSend(packetId, reliableSequenceId, packet);
}
}
@@ -0,0 +1,123 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/UdpConnectionSet.h>
#include <AzNetworking/UdpTransport/UdpConnection.h>
namespace AzNetworking
{
bool UdpConnectionSet::AddConnection(AZStd::unique_ptr<UdpConnection> connection)
{
AZ_Assert(connection, "Adding a nullptr UdpConnection instance to the connection set");
if (!connection)
{
return false;
}
AZLOG(UdpConnectionSet, "Adding new Udp connection (%u : %s)",
aznumeric_cast<uint32_t>(connection->GetConnectionId()),
connection->GetRemoteAddress().GetString().c_str()
);
// Check for errors here, don't want to clobber an existing connection...
AZ_Assert(GetConnection(connection->GetConnectionId()) == nullptr, "ConnectionId already exists in connection set");
AZ_Assert(GetConnection(connection->GetRemoteAddress()) == nullptr, "Remote address already exists in connection set");
m_remoteAddressMap[connection->GetRemoteAddress()] = connection.get();
m_connectionIdMap[connection->GetConnectionId()] = AZStd::move(connection);
return true;
}
bool UdpConnectionSet::DeleteConnection(const IpAddress& address)
{
AZLOG(UdpConnectionSet, "Deleting Udp connection by remote address (%s)", address.GetString().c_str());
UdpConnection* connection = GetConnection(address);
if (connection == nullptr)
{
return false;
}
AZLOG(UdpConnectionSet, "Deleting Udp connection (%u : %s)",
aznumeric_cast<uint32_t>(connection->GetConnectionId()),
connection->GetRemoteAddress().GetString().c_str()
);
AZ_Assert(connection->GetRemoteAddress() == address, "Connection list is corrupt, mismatched remote endpoint addresses detected");
m_remoteAddressMap.erase(connection->GetRemoteAddress());
m_connectionIdMap.erase(connection->GetConnectionId());
return true;
}
void UdpConnectionSet::VisitConnections(const ConnectionVisitor& visitor)
{
for (auto& connection : m_connectionIdMap)
{
visitor(*connection.second);
}
}
bool UdpConnectionSet::DeleteConnection(ConnectionId connectionId)
{
AZLOG(UdpConnectionSet, "Deleting Udp connection by connectionId (%u)", aznumeric_cast<uint32_t>(connectionId));
UdpConnection* connection = static_cast<UdpConnection*>(GetConnection(connectionId));
if (connection == nullptr)
{
return false;
}
AZLOG(UdpConnectionSet, "Deleting Udp connection (%u : %s)",
aznumeric_cast<uint32_t>(connectionId),
connection->GetRemoteAddress().GetString().c_str()
);
AZ_Assert(connection->GetConnectionId() == connectionId, "Connection list is corrupt, mismatched connection identifiers detected");
m_remoteAddressMap.erase(connection->GetRemoteAddress());
m_connectionIdMap.erase(connectionId);
return true;
}
IConnection* UdpConnectionSet::GetConnection(ConnectionId connectionId) const
{
ConnectionIdMap::const_iterator lookup = m_connectionIdMap.find(connectionId);
if (lookup != m_connectionIdMap.end())
{
return lookup->second.get();
}
return nullptr;
}
ConnectionId UdpConnectionSet::GetNextConnectionId()
{
// In the case of wrap-around, don't return a connectionId that's in-use or is the invalid connection Id
do
{
++m_nextConnectionId;
if (m_nextConnectionId == InvalidConnectionId)
{
m_nextConnectionId = ConnectionId(0);
}
} while (m_connectionIdMap.count(m_nextConnectionId) > 0);
return m_nextConnectionId;
}
uint32_t UdpConnectionSet::GetConnectionCount() const
{
return aznumeric_cast<uint32_t>(m_connectionIdMap.size());
}
UdpConnection* UdpConnectionSet::GetConnection(const IpAddress& address) const
{
RemoteAddressMap::const_iterator lookup = m_remoteAddressMap.find(address);
if (lookup != m_remoteAddressMap.end())
{
return lookup->second;
}
return nullptr;
}
}
@@ -0,0 +1,66 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/Utilities/IpAddress.h>
#include <AzNetworking/ConnectionLayer/IConnectionSet.h>
#include <AzCore/std/containers/unordered_map.h>
namespace AzNetworking
{
class UdpConnection;
//! @class UdpConnectionSet
//! @brief Tracks current UDP endpoints and allows fast lookups by connection identifier and remote address.
class UdpConnectionSet final
: public IConnectionSet
{
public:
using ConnectionIdMap = AZStd::unordered_map<ConnectionId, AZStd::unique_ptr<UdpConnection>>;
using RemoteAddressMap = AZStd::unordered_map<IpAddress, UdpConnection*>;
UdpConnectionSet() = default;
~UdpConnectionSet() override = default;
//! Adds a new connection to this connection list instance.
//! @param connection pointer to the connection instance to add
//! @return boolean true on success
bool AddConnection(AZStd::unique_ptr<UdpConnection> connection);
//! Deletes a connection from this connection list instance by endpoint remote address.
//! @param address address of the remote endpoint to delete
//! @return boolean true on success
bool DeleteConnection(const IpAddress& address);
//! IConnectionSet interface.
//! @{
void VisitConnections(const ConnectionVisitor& visitor) override;
bool DeleteConnection(ConnectionId connectionId) override;
IConnection* GetConnection(ConnectionId connectionId) const override;
ConnectionId GetNextConnectionId() override;
uint32_t GetConnectionCount() const override;
//! @}
//! Retrieves a connection from this connection list instance by endpoint remote address
//! @param address address of the remote endpoint of the connection to retrieve
//! @return pointer to the requested connection instance on success, nullptr on failure
UdpConnection* GetConnection(const IpAddress& address) const;
private:
ConnectionId m_nextConnectionId = InvalidConnectionId;
ConnectionIdMap m_connectionIdMap;
RemoteAddressMap m_remoteAddressMap;
};
}
@@ -0,0 +1,168 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/UdpFragmentQueue.h>
#include <AzNetworking/UdpTransport/UdpConnection.h>
#include <AzNetworking/UdpTransport/UdpPacketHeader.h>
#include <AzNetworking/Serialization/NetworkOutputSerializer.h>
#include <AzNetworking/Utilities/NetworkCommon.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Console/ILogger.h>
namespace AzNetworking
{
AZ_CVAR(AZ::TimeMs, net_UdpFragmentTimeoutMs, AZ::TimeMs{ 5000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Milliseconds to retain chunks of incomplete unreliable fragmented packets before timing them out");
void UdpFragmentQueue::Update()
{
m_timeoutQueue.UpdateTimeouts(*this);
}
void UdpFragmentQueue::Reset()
{
m_timeoutQueue.Reset();
m_sequenceGenerator.Reset();
m_packetFragments.clear();
m_latestReceivedFragmentSequence = InvalidSequenceId;
m_deliveredFragments.Reset();
}
SequenceId UdpFragmentQueue::GetNextFragmentedSequenceId()
{
return m_sequenceGenerator.GetNextSequenceId();
}
bool UdpFragmentQueue::ProcessReceivedChunk(UdpConnection* connection, IConnectionListener& connectionListener, UdpPacketHeader& header, ISerializer& serializer)
{
AZStd::unique_ptr<CorePackets::FragmentedPacket> packet = AZStd::make_unique<CorePackets::FragmentedPacket>();
if (!serializer.Serialize(*packet, "Packet"))
{
AZLOG(NET_FragmentQueue, "Fragment failed serialization");
return false;
}
const bool isReliable = header.GetIsReliable();
const SequenceId fragmentSequence = packet->GetFragmentSequence();
if (SequenceMoreRecent(fragmentSequence, m_latestReceivedFragmentSequence))
{
const SequenceId sequenceDelta = SequenceId(fragmentSequence - m_latestReceivedFragmentSequence);
m_latestReceivedFragmentSequence = fragmentSequence;
m_deliveredFragments.PushBackBits(static_cast<uint32_t>(sequenceDelta));
}
const SequenceId sequenceDelta = SequenceId(m_latestReceivedFragmentSequence - fragmentSequence);
if (static_cast<uint32_t>(sequenceDelta) >= m_deliveredFragments.GetValidBitCount())
{
// Too old to process
AZLOG(NET_FragmentQueue, "Fragment sequence ID is outside our tracked window");
return false;
}
if (m_deliveredFragments.GetBit(static_cast<uint32_t>(sequenceDelta)))
{
// Received packet is a duplicate of one already forwarded to gameplay
AZLOG(NET_FragmentQueue, "Received duplicate of fragmented packet %u, discarding", static_cast<uint32_t>(fragmentSequence));
return true;
}
const uint32_t chunkCount = packet->GetChunkCount();
const uint32_t chunkIndex = packet->GetChunkIndex();
// If this is the first time we've heard about this sequence, resize the vector appropriately
const bool isNewPacketFragment = m_packetFragments.find(fragmentSequence) == m_packetFragments.end();
PacketFragments& packetFragments = m_packetFragments[fragmentSequence];
if (isNewPacketFragment)
{
packetFragments.resize(chunkCount);
}
if ((chunkCount != packetFragments.size()) || (chunkIndex >= chunkCount))
{
// Either we disagree on the number of chunks, or chunkIndex is bigger than the expected size, bail and disconnect
AZLOG(NET_FragmentQueue, "Malformed chunk metadata in fragmented packet, chunkIndex %u, chunkCount %u, reservedSize %u", chunkIndex, chunkCount, static_cast<uint32_t>(packetFragments.size()));
return false;
}
packetFragments[chunkIndex] = AZStd::move(packet);
uint32_t totalPacketSize = 0;
for (uint32_t index = 0; index < packetFragments.size(); ++index)
{
if (packetFragments[index] == nullptr)
{
if (!isReliable)
{
m_timeoutQueue.RegisterItem(static_cast<uint64_t>(fragmentSequence), net_UdpFragmentTimeoutMs);
}
// We haven't received all chunks required to complete this packet yet
return true;
}
totalPacketSize += packetFragments[index]->GetChunkBuffer().GetSize();
}
// We now mark this sequence as delivered, so if by some chance all the individual chunks get redelivered again we don't double deliver the reconstructed packet
m_deliveredFragments.SetBit(static_cast<uint32_t>(sequenceDelta), true);
// All chunks have been received, reconstruct the original packet and deliver to the connection listener
UdpPacketEncodingBuffer buffer;
if (!buffer.Resize(totalPacketSize))
{
AZLOG_ERROR("Fragmented packet is too large to fit in UdpPacketEncodingBuffer");
return false;
}
uint8_t* bufferPointer = buffer.GetBuffer();
for (uint32_t index = 0; index < packetFragments.size(); ++index)
{
const uint32_t chunkSize = packetFragments[index]->GetChunkBuffer().GetSize();
memcpy(bufferPointer, packetFragments[index]->GetChunkBuffer().GetBuffer(), chunkSize);
bufferPointer += chunkSize;
}
// We can erase all the chunks now, packet is completed
m_packetFragments.erase(fragmentSequence);
NetworkOutputSerializer networkSerializer(buffer.GetBuffer(), buffer.GetSize());
{
ISerializer& serializer = networkSerializer; // To get the default typeinfo parameters in ISerializer
// First, serialize out the header
if (!header.SerializePacketFlags(networkSerializer))
{
AZLOG(NET_FragmentQueue, "Reconstructed fragmented packet failed packet flags serialization");
return false;
}
if (!serializer.Serialize(header, "Header"))
{
AZLOG(NET_FragmentQueue, "Reconstructed fragmented packet failed header serialization");
return false;
}
}
connection->GetPacketTracker().ProcessReceived(connection, header);
return connectionListener.OnPacketReceived(connection, header, networkSerializer);
}
TimeoutResult UdpFragmentQueue::HandleTimeout(TimeoutQueue::TimeoutItem& item)
{
const SequenceId fragmentSequence = static_cast<SequenceId>(item.m_userData & 0xFF);
AZLOG(NET_FragmentQueue, "Timing out unreliable fragmented packet %u", static_cast<uint32_t>(fragmentSequence));
m_packetFragments.erase(fragmentSequence);
return TimeoutResult::Delete;
}
}
@@ -0,0 +1,72 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/PacketLayer/IPacket.h>
#include <AzNetworking/AutoGen/CorePackets.AutoPackets.h>
#include <AzNetworking/ConnectionLayer/SequenceGenerator.h>
#include <AzNetworking/DataStructures/RingBufferBitset.h>
#include <AzNetworking/DataStructures/TimeoutQueue.h>
#include <AzNetworking/UdpTransport/UdpSocket.h>
#include <AzCore/std/containers/unordered_map.h>
namespace AzNetworking
{
class IConnectionListener;
class UdpConnection;
class UdpPacketHeader;
//! @class UdpFragmentQueue
//! @brief Class for reconstructing packet chunks into the original unsegmented packet.
class UdpFragmentQueue
: public ITimeoutHandler
{
public:
//! Updates the UdpFragmentQueue timeout queue.
void Update();
//! Resets all internal state.
void Reset();
//! Returns the next (outgoing) fragmented sequenceId for this FragmentQueue instance.
SequenceId GetNextFragmentedSequenceId();
//! Processes a received chunk and delivers the final reconstructed packet if possible.
//! @param connection pointer to the connection this packet chunk was received on
//! @param connectionListener the connection listener for delivery of completed packets
//! @param header the chunk packet header
//! @param serializer the serializer containing the chunk body
//! @return boolean true if the chunk was processed, false if an error was encountered
bool ProcessReceivedChunk(UdpConnection* connection, IConnectionListener& connectionListener, UdpPacketHeader& header, ISerializer& serializer);
private:
//! Handler callback for timed out items.
//! @param item containing registered timeout details
//! @return ETimeoutResult for whether to re-register or discard the timeout params
virtual TimeoutResult HandleTimeout(TimeoutQueue::TimeoutItem& item) override;
TimeoutQueue m_timeoutQueue;
SequenceGenerator m_sequenceGenerator;
using PacketFragments = AZStd::vector<AZStd::unique_ptr<CorePackets::FragmentedPacket>>;
AZStd::unordered_map<SequenceId, PacketFragments> m_packetFragments;
static constexpr uint32_t PacketWindowAckCount = 16384; // The total number of packet id's to track
using PacketAckContainer = RingbufferBitset<PacketWindowAckCount>;
SequenceId m_latestReceivedFragmentSequence = InvalidSequenceId;
PacketAckContainer m_deliveredFragments;
};
}
@@ -0,0 +1,731 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/UdpNetworkInterface.h>
#include <AzNetworking/UdpTransport/UdpConnection.h>
#include <AzNetworking/UdpTransport/DtlsSocket.h>
#include <AzNetworking/UdpTransport/UdpSocket.h>
#include <AzNetworking/Serialization/NetworkInputSerializer.h>
#include <AzNetworking/Serialization/NetworkOutputSerializer.h>
#include <AzNetworking/Framework/ICompressor.h>
#include <AzNetworking/Utilities/CompressionCommon.h>
#include <AzNetworking/Utilities/NetworkCommon.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Console/ILogger.h>
namespace AzNetworking
{
#if AZ_TRAIT_USE_OPENSSL
AZ_CVAR(bool, net_UdpUseEncryption, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Enable encryption on Udp based connections");
#else
static const bool net_UdpUseEncryption = false;
#endif
AZ_CVAR(bool, net_UdpTimeoutConnections, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Boolean value on whether we should timeout Udp connections");
AZ_CVAR(AZ::TimeMs, net_UdpPacketTimeSliceMs, AZ::TimeMs{ 8 }, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The number of milliseconds to allow for packet processing");
AZ_CVAR(AZ::TimeMs, net_UdpHearthbeatTimeMs, AZ::TimeMs{ 2 * 1000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Udp connection heartbeat frequency");
AZ_CVAR(AZ::TimeMs, net_UdpTimeoutTimeMs, AZ::TimeMs{ 10 * 1000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Time in milliseconds before we timeout an idle Udp connection");
AZ_CVAR(AZ::TimeMs, net_MinPacketTimeoutMs, AZ::TimeMs{ 200 }, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Minimum time to wait before timing out an unacked packet");
AZ_CVAR(int32_t, net_MaxTimeoutsPerFrame, 1000, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Maximum number of packet timeouts to allow to process in a single frame");
AZ_CVAR(float, net_RttFudgeScalar, 2.0f, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Scalar value to multiply computed Rtt by to determine an optimal packet timeout threshold");
AZ_CVAR(uint32_t, net_FragmentedHeaderOverhead, 32, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "A fudge overhead value to take out of fragmented packet payloads");
AZ_CVAR(AZ::CVarFixedString, net_UdpCompressor, "MultiplayerCompressor", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "UDP compressor to use."); // WARN: similar to encryption this needs to be set once and only once before creating the network interface
static uint64_t ConstructTimeoutId(ConnectionId connectionId, PacketId packetId, ReliabilityType reliability)
{
const uint64_t intConnectionId = aznumeric_cast<uint64_t>(connectionId);
const uint64_t intPacketId = aznumeric_cast<uint64_t>(packetId);
const uint64_t intReliability = (reliability == ReliabilityType::Reliable) ? 1 : 0;
const uint64_t baseTimeoutId = ((intConnectionId << 32) | intPacketId) & 0x7FFFFFFFFFFFFFFF;
return (intReliability << 63) | baseTimeoutId;
}
static void DecodeTimeoutId(uint64_t timeoutId, ConnectionId& outConnectionId, PacketId& outPacketId, ReliabilityType& outReliability)
{
outConnectionId = ConnectionId(aznumeric_cast<uint32_t>(timeoutId >> 32) & 0x7FFFFFFF);
outPacketId = PacketId(aznumeric_cast<uint32_t>(timeoutId >> 0) & 0xFFFFFFFF);
outReliability = ((timeoutId & 0x8000000000000000) > 0) ? ReliabilityType::Reliable : ReliabilityType::Unreliable;
}
UdpNetworkInterface::UdpNetworkInterface(AZ::Name name, IConnectionListener& connectionListener, TrustZone trustZone, UdpReaderThread& readerThread)
: m_name(name)
, m_trustZone(trustZone)
, m_connectionListener(connectionListener)
, m_socket(net_UdpUseEncryption ? new DtlsSocket() : new UdpSocket())
, m_readerThread(readerThread)
{
const AZ::CVarFixedString compressor = static_cast<AZ::CVarFixedString>(net_UdpCompressor);
const char* compressorName = compressor.c_str();
m_compressor = CreateCompressor(compressorName);
}
UdpNetworkInterface::~UdpNetworkInterface()
{
m_readerThread.UnregisterSocket(m_socket.get());
}
AZ::Name UdpNetworkInterface::GetName() const
{
return m_name;
}
ProtocolType UdpNetworkInterface::GetType() const
{
return ProtocolType::Udp;
}
TrustZone UdpNetworkInterface::GetTrustZone() const
{
return m_trustZone;
}
uint16_t UdpNetworkInterface::GetPort() const
{
return m_port;
}
IConnectionSet& UdpNetworkInterface::GetConnectionSet()
{
return m_connectionSet;
}
IConnectionListener& UdpNetworkInterface::GetConnectionListener()
{
return m_connectionListener;
}
bool UdpNetworkInterface::Listen(uint16_t port)
{
if (m_socket->IsOpen())
{
AZ_Assert(false, "Listen cannot be invoked on an already opened network interface");
return false;
}
m_port = port;
m_allowIncomingConnections = true;
m_socket->Open(m_port, UdpSocket::CanAcceptConnections::True, m_trustZone);
m_readerThread.RegisterSocket(m_socket.get());
return true;
}
ConnectionId UdpNetworkInterface::Connect(const IpAddress& remoteAddress)
{
if (!m_socket->IsOpen())
{
m_socket->Open(m_port, UdpSocket::CanAcceptConnections::True, m_trustZone);
m_readerThread.RegisterSocket(m_socket.get());
}
const ConnectionId connectionId = m_connectionSet.GetNextConnectionId();
const TimeoutId timeoutId = m_connectionTimeoutQueue.RegisterItem(aznumeric_cast<uint64_t>(connectionId), net_UdpHearthbeatTimeMs);
AZStd::unique_ptr<UdpConnection> connection = AZStd::make_unique<UdpConnection>(connectionId, remoteAddress, *this, ConnectionRole::Connector);
UdpPacketEncodingBuffer dtlsData;
m_socket->ConnectDtlsEndpoint(connection->GetDtlsEndpoint(), remoteAddress, dtlsData);
// We're initiating this connection, so go to a connecting state until we receive some kind of response so that we know it's alive and valid
connection->m_state = ConnectionState::Connecting;
connection->SetConnectionMtu(MaxUdpTransmissionUnit);
connection->SetTimeoutId(timeoutId);
connection->SendReliablePacket(CorePackets::InitiateConnectionPacket());
m_connectionListener.OnConnect(connection.get());
m_connectionSet.AddConnection(AZStd::move(connection));
return connectionId;
}
void UdpNetworkInterface::Update([[maybe_unused]] AZ::TimeMs deltaTimeMs)
{
if (!m_socket->IsOpen())
{
return;
}
#ifdef ENABLE_LATENCY_DEBUG
m_socket->ProcessDeferredPackets();
#endif
const AZ::TimeMs startTimeMs = AZ::GetElapsedTimeMs();
const UdpReaderThread::ReceivedPackets* packets = m_readerThread.GetReceivedPackets(m_socket.get());
if (packets == nullptr)
{
AZ_Assert(false, "nullptr was retrieved for the received packet buffer, check that the socket has been registered with the reader thread");
return;
}
for (uint32_t i = 0; i < packets->size(); ++i)
{
const UdpReaderThread::ReceivedPacket& packet = (*packets)[i];
const AZ::TimeMs currentTimeMs = AZ::GetElapsedTimeMs();
// Don't exceed our timeslice, even if unprocessed data remains
if ((currentTimeMs - startTimeMs) > net_UdpPacketTimeSliceMs)
{
AZLOG_WARN("Processing time exceeded, discarding %d/%d received packets", aznumeric_cast<int32_t>(packets->size() - i), aznumeric_cast<int32_t>(packets->size()));
GetMetrics().m_discardedPackets += packets->size() - i;
break;
}
UdpConnection* connection = m_connectionSet.GetConnection(packet.m_address);
if (connection == nullptr)
{
AcceptConnection(packet);
continue;
}
const DisconnectReason disconnectReason = GetDisconnectReasonForSocketResult(packet.m_receivedBytes);
if (disconnectReason != DisconnectReason::MAX)
{
connection->Disconnect(disconnectReason, TerminationEndpoint::Local);
continue;
}
int32_t decodedPacketSize = 0;
m_decryptBuffer.Resize(m_decryptBuffer.GetCapacity());
const uint8_t* decodedPacketData = connection->GetDtlsEndpoint().DecodePacket(*m_socket, packet.m_buffer, packet.m_receivedBytes, m_decryptBuffer.GetBuffer(), decodedPacketSize);
m_decryptBuffer.Resize(decodedPacketSize);
if (decodedPacketSize == 0)
{
// OpenSSL may have consumed packets during handshake negotiation
continue;
}
else if (decodedPacketSize < 0)
{
// Something bad happened on the SSL read and we're now invalid, we should disconnect
connection->Disconnect(DisconnectReason::SslFailure, TerminationEndpoint::Local);
continue;
}
connection->GetMetrics().m_recvDatarate.LogPacket(packet.m_receivedBytes + UdpPacketHeaderSize, currentTimeMs);
connection->GetMetrics().m_packetsRecv++;
// Decode the packet flag bitset first since it's always uncompressed
UdpPacketHeader header;
{
NetworkOutputSerializer flagSerializer(decodedPacketData, decodedPacketSize);
if (!header.SerializePacketFlags(flagSerializer))
{
continue;
}
// Adjust decoded tracking to represent the payload now that we've grabbed the flags
decodedPacketData = flagSerializer.GetUnreadData();
decodedPacketSize = flagSerializer.GetUnreadSize();
GetMetrics().m_recvBytesUncompressed += flagSerializer.GetReadSize();
}
if (m_compressor && header.IsPacketFlagSet(PacketFlag::Compressed))
{
// Only the payload is compressed
if (!DecompressPacket(decodedPacketData, decodedPacketSize, m_decompressBuffer))
{
AZLOG_WARN("Failed to decompress packet!");
continue;
}
decodedPacketData = m_decompressBuffer.GetBuffer();
decodedPacketSize = m_decompressBuffer.GetSize();
GetMetrics().m_recvBytesUncompressed += decodedPacketSize;
}
TimeoutQueue::TimeoutItem* timeoutItem = m_connectionTimeoutQueue.RetrieveItem(connection->GetTimeoutId());
if (timeoutItem == nullptr)
{
connection->Disconnect(DisconnectReason::Unknown, TerminationEndpoint::Local);
continue;
}
else
{
// Deserialize the packet header
NetworkOutputSerializer packetSerializer(decodedPacketData, decodedPacketSize);
ISerializer& serializer = packetSerializer; // To get the default typeinfo parameters in ISerializer
if (!serializer.Serialize(header, "Header"))
{
continue;
}
// Note that the serializer passed in here is unused for UDP
if (!connection->ProcessReceived(header, packetSerializer, packet.m_receivedBytes + UdpPacketHeaderSize, currentTimeMs))
{
continue;
}
timeoutItem->UpdateTimeoutTime(startTimeMs);
bool handledPacket = false;
if (header.GetPacketType() < aznumeric_cast<PacketType>(CorePackets::PacketType::MAX))
{
handledPacket = connection->HandleCorePacket(m_connectionListener, header, packetSerializer);
}
else
{
handledPacket = m_connectionListener.OnPacketReceived(connection, header, packetSerializer);
}
if (handledPacket)
{
connection->UpdateHeartbeat(currentTimeMs);
if (connection->GetConnectionState() == ConnectionState::Connecting)
{
connection->m_state = ConnectionState::Connected;
}
}
else if (connection->GetConnectionState() != ConnectionState::Disconnecting)
{
connection->Disconnect(DisconnectReason::StreamError, TerminationEndpoint::Local);
}
}
}
const AZ::TimeMs receiveTimeMs = AZ::GetElapsedTimeMs() - startTimeMs;
// Time out any stale client connections
{
ConnectionTimeoutFunctor functor(*this);
m_connectionTimeoutQueue.UpdateTimeouts(functor);
}
// Time out any packets that haven't been acked within our timeout window
{
PacketTimeoutFunctor functor(*this);
m_packetTimeoutQueue.UpdateTimeouts(functor, static_cast<int32_t>(net_MaxTimeoutsPerFrame));
}
// Delete any connections we've disconnected
for (RemovedConnection& removedConnection : m_removedConnections)
{
m_connectionListener.OnDisconnect(removedConnection.m_connection, removedConnection.m_reason, removedConnection.m_endpoint);
m_connectionSet.DeleteConnection(removedConnection.m_connection->GetConnectionId()); // Will delete the connection
}
m_removedConnections.clear();
// Update metrics
GetMetrics().m_sendPackets = m_socket->GetSentPackets();
GetMetrics().m_sendBytes = m_socket->GetSentBytes();
GetMetrics().m_recvTimeMs += receiveTimeMs;
GetMetrics().m_recvPackets = m_socket->GetRecvPackets();
GetMetrics().m_recvBytes = m_socket->GetRecvBytes();
GetMetrics().m_connectionCount = m_connectionSet.GetConnectionCount();
GetMetrics().m_updateTimeMs += AZ::GetElapsedTimeMs() - startTimeMs;
}
bool UdpNetworkInterface::SendReliablePacket(ConnectionId connectionId, const IPacket& packet)
{
IConnection* connection = m_connectionSet.GetConnection(connectionId);
if (connection == nullptr)
{
return false;
}
return connection->SendReliablePacket(packet);
}
PacketId UdpNetworkInterface::SendUnreliablePacket(ConnectionId connectionId, const IPacket& packet)
{
IConnection* connection = m_connectionSet.GetConnection(connectionId);
if (connection == nullptr)
{
return InvalidPacketId;
}
return connection->SendUnreliablePacket(packet);
}
bool UdpNetworkInterface::WasPacketAcked(ConnectionId connectionId, PacketId packetId)
{
IConnection* connection = m_connectionSet.GetConnection(connectionId);
if (connection == nullptr)
{
return false;
}
return connection->WasPacketAcked(packetId);
}
bool UdpNetworkInterface::Disconnect(ConnectionId connectionId, DisconnectReason reason)
{
IConnection* connection = m_connectionSet.GetConnection(connectionId);
if (connection == nullptr)
{
return false;
}
return connection->Disconnect(reason, TerminationEndpoint::Local);
}
bool UdpNetworkInterface::IsEncrypted() const
{
return m_socket->IsEncrypted();
}
bool UdpNetworkInterface::IsOpen() const
{
return m_socket->IsOpen();
}
void UdpNetworkInterface::RegisterWithTimeoutQueue(ConnectionId connectionId, PacketId packetId, ReliabilityType reliability, const ConnectionMetrics& metrics)
{
const float avgRtt = metrics.m_connectionRtt.GetRoundTripTimeSeconds(); // Time is in seconds, timeout times are in milliseconds
const AZ::TimeMs expectedTimeoutMs = aznumeric_cast<AZ::TimeMs>(aznumeric_cast<int64_t>(avgRtt * 1000.0f * net_RttFudgeScalar));
const AZ::TimeMs packetTimeoutMs = AZStd::max<AZ::TimeMs>(expectedTimeoutMs, net_MinPacketTimeoutMs); // Consider packets lost after twice the current connection Rtt
AZLOG(NET_Debug, "Registering packetId %u with timeout %u", aznumeric_cast<uint32_t>(packetId), aznumeric_cast<uint32_t>(packetTimeoutMs));
m_packetTimeoutQueue.RegisterItem(ConstructTimeoutId(connectionId, packetId, reliability), packetTimeoutMs);
}
bool UdpNetworkInterface::DecompressPacket(const uint8_t* packetBuffer, size_t packetSize, UdpPacketEncodingBuffer& packetBufferOut) const
{
if (!m_compressor) // should probably have some compression handshake than relying on existence of compressor
{
AZLOG_ERROR("Decompress called without a compressor.");
return false;
}
AZStd::size_t uncompSize = 0;
AZStd::size_t bytesConsumed = 0;
packetBufferOut.Resize(packetBufferOut.GetCapacity());
const CompressorError compErr = m_compressor->Decompress(packetBuffer, packetSize, packetBufferOut.GetBuffer(), packetBufferOut.GetCapacity(), bytesConsumed, uncompSize);
packetBufferOut.Resize(aznumeric_cast<uint32_t>(uncompSize)); // Decompress will fail if larger than buffer size, so this cast is safe
if (compErr != CompressorError::Ok)
{
AZLOG_ERROR("Decompress failed with error %d this will lead to data read errors!", compErr);
return false;
}
if (packetSize != bytesConsumed)
{
AZLOG_ERROR("Decompress must consume entire buffer [%zu != %zu]!", bytesConsumed, packetSize);
return false;
}
return true;
}
PacketId UdpNetworkInterface::SendPacket(UdpConnection& connection, const IPacket& packet, SequenceId reliableSequence)
{
AZLOG(NET_DebugPacketSend, "Sending packet type %u to remote address %s", aznumeric_cast<uint32_t>(packet.GetPacketType()), connection.GetRemoteAddress().GetString().c_str());
// The ordering inside this function is incredibly important and fragile
const IpAddress& address = connection.GetRemoteAddress();
// We don't want to compress the initial InitiateConnectionPacket
// We can use this to transmit compression and encryption details in the future
const bool shouldCompress = packet.GetPacketType() != aznumeric_cast<PacketType>(CorePackets::PacketType::InitiateConnectionPacket);
if (address.GetAddress(ByteOrder::Host) == 0)
{
return InvalidPacketId;
}
const ReliabilityType reliabilityType = (reliableSequence == InvalidSequenceId) ? ReliabilityType::Unreliable : ReliabilityType::Reliable;
// Check if we need to fragment this packet first
// We don't ack aggregate packets that get fragmented, so we want to get this chunk out of the way before
// we start throwing PacketId's and SequenceId's into our other tracking data structures below
UdpPacketHeader header(connection.GetPacketTracker(), packet.GetPacketType(), reliableSequence);
const PacketId localPacketId = header.GetPacketId();
UdpPacketEncodingBuffer buffer;
{
buffer.Resize(buffer.GetCapacity());
NetworkInputSerializer networkSerializer(buffer.GetBuffer(), buffer.GetCapacity());
ISerializer& serializer = networkSerializer; // To get the default typeinfo parameters in ISerializer
if (!header.SerializePacketFlags(serializer))
{
AZLOG_ERROR("PacketId %u failed flag serialization and will not be sent", aznumeric_cast<uint32_t>(localPacketId));
return InvalidPacketId;
}
if (!serializer.Serialize(header, "Header"))
{
AZLOG_ERROR("PacketId %u failed header serialization and will not be sent", aznumeric_cast<uint32_t>(localPacketId));
return InvalidPacketId;
}
if (!serializer.Serialize(const_cast<IPacket&>(packet), "Payload"))
{
AZLOG_ERROR("PacketId %u failed payload serialization and will not be sent", aznumeric_cast<uint32_t>(localPacketId));
return InvalidPacketId;
}
buffer.Resize(serializer.GetSize());
}
uint32_t packetSize = buffer.GetSize();
uint8_t* packetData = buffer.GetBuffer();
// If the packet doesn't fit within our MTU, break it up
if (packetSize > connection.GetConnectionMtu())
{
// Each fragmented packet we send adds an extra fragmented packet header, need to deduct that from our chunk size, otherwise we infinitely loop
const uint32_t chunkSize = connection.GetConnectionMtu() - net_FragmentedHeaderOverhead;
const uint32_t numChunks = (packetSize + chunkSize - 1) / chunkSize; // We want to round up on the remainder
const uint8_t* chunkStart = packetData;
const SequenceId fragmentedSequence = connection.m_fragmentQueue.GetNextFragmentedSequenceId();
uint32_t bytesRemaining = packetSize;
ChunkBuffer chunkBuffer;
for (uint32_t chunkIndex = 0; chunkIndex < numChunks; ++chunkIndex)
{
const uint32_t nextChunkSize = AZStd::min(bytesRemaining, chunkSize);
chunkBuffer.CopyValues(chunkStart, nextChunkSize);
CorePackets::FragmentedPacket fragmentedPacket(ToSequenceId(localPacketId), fragmentedSequence, aznumeric_cast<uint8_t>(chunkIndex), aznumeric_cast<uint8_t>(numChunks), chunkBuffer);
const SequenceId chunkReliableId = (reliabilityType == ReliabilityType::Reliable) ? connection.m_reliableQueue.GetNextSequenceId() : InvalidSequenceId;
SendPacket(connection, fragmentedPacket, chunkReliableId);
bytesRemaining -= nextChunkSize;
chunkStart += nextChunkSize;
}
AZ_Assert(bytesRemaining == 0, "Non-zero bytes remaining (%u) after chunking a packet into fragments", bytesRemaining);
return localPacketId;
}
UdpPacketEncodingBuffer writeBuffer;
if (m_compressor && shouldCompress)
{
NetworkInputSerializer flagSerializer(writeBuffer.GetBuffer(), writeBuffer.GetCapacity());
ISerializer& serializer = flagSerializer; // To get the default typeinfo parameters in ISerializer
header.SetPacketFlag(PacketFlag::Compressed, true);
if (!header.SerializePacketFlags(serializer))
{
AZLOG_ERROR("PacketId %u failed flag serialization for compression and will not be sent", aznumeric_cast<uint32_t>(localPacketId));
return InvalidPacketId;
}
uint32_t flagSize = flagSerializer.GetSize();
AZ_Assert(flagSize == 1, "Flag bitfield should serialize to one byte");
// Compress the packet, make sure to offset by the size of the flag which is now serialized
const uint32_t payloadSize = buffer.GetSize() - flagSize;
uint8_t* payload = buffer.GetBuffer() + flagSize;
const AZStd::size_t maxSizeNeeded = m_compressor->GetMaxCompressedBufferSize(payloadSize);
AZStd::size_t compressionMemBytesUsed = 0;
CompressorError compErr = m_compressor->Compress(payload, payloadSize, writeBuffer.GetBuffer() + flagSize, maxSizeNeeded, compressionMemBytesUsed);
if (compErr != CompressorError::Ok)
{
AZLOG_ERROR("Failed to compress packet with error %d", aznumeric_cast<int32_t>(compErr));
return InvalidPacketId;
}
// Only use compression if there's actual gain
if (compressionMemBytesUsed < payloadSize)
{
writeBuffer.Resize(aznumeric_cast<int32_t>(flagSize + compressionMemBytesUsed));
packetSize = writeBuffer.GetSize();
packetData = writeBuffer.GetBuffer();
// Track byte delta caused by compression
GetMetrics().m_sendBytesCompressedDelta += (packetSize - compressionMemBytesUsed);
}
}
AZLOG(NET_Debug, "Sending local sequence id %d, remote sequence id %d, %s, reliable id: %d, ack vector %x",
aznumeric_cast<int32_t>(header.GetLocalSequenceId()),
aznumeric_cast<int32_t>(header.GetRemoteSequenceId()),
header.GetIsReliable() ? "reliable" : "unreliable",
aznumeric_cast<int32_t>(header.GetReliableSequenceId()),
aznumeric_cast<uint32_t>(header.GetSequenceWindow())
);
// If it's a reliable packet, make sure our reliable queue knows about it now because we might need to drop it if our connection is not set up
if (reliabilityType == ReliabilityType::Reliable)
{
if (!connection.PrepareReliablePacketForSend(localPacketId, reliableSequence, packet))
{
connection.Disconnect(DisconnectReason::ReliableQueueFull, TerminationEndpoint::Local);
}
}
// Okay, if we're still connecting, and the packet we're trying to send is not a retransmitted initiate connection packet, return an error, don't send yet
if (connection.GetDtlsEndpoint().IsConnecting() && (packet.GetPacketType() != aznumeric_cast<PacketType>(CorePackets::PacketType::InitiateConnectionPacket)))
{
const DtlsEndpoint::ConnectResult result = connection.CompleteHandshake();
if (result != DtlsEndpoint::ConnectResult::Complete) // DTLS handshake still in progress
{
// IMPORTANT that we register with the timeout queue here, otherwise we don't have the timer to pop for reliable packets
RegisterWithTimeoutQueue(connection.GetConnectionId(), localPacketId, reliabilityType, connection.GetMetrics());
AZLOG(NET_DebugDtls, "Connection is still in handshake negotiation, blocking packet send for packet type %d", (int)packet.GetPacketType());
return localPacketId;
}
}
AZLOG(NET_DebugDtls, "Connection is sending packet type %d", aznumeric_cast<int32_t>(packet.GetPacketType()));
const bool shouldEncrypt = packet.GetPacketType() == aznumeric_cast<PacketType>(CorePackets::PacketType::InitiateConnectionPacket);
if (m_socket->Send(address, packetData, packetSize, shouldEncrypt, connection.GetDtlsEndpoint(), connection.GetConnectionQuality()))
{
RegisterWithTimeoutQueue(connection.GetConnectionId(), localPacketId, reliabilityType, connection.GetMetrics());
connection.ProcessSent(localPacketId, packet, packetSize + UdpPacketHeaderSize, reliabilityType);
GetMetrics().m_sendBytesUncompressed += buffer.GetSize() + UdpPacketHeaderSize + (shouldEncrypt ? DtlsPacketHeaderSize : 0);
return localPacketId;
}
else
{
AZLOG_ERROR("PacketId %u failed to send on the socket", aznumeric_cast<uint32_t>(localPacketId));
}
return InvalidPacketId;
}
void UdpNetworkInterface::AcceptConnection(const UdpReaderThread::ReceivedPacket& connectPacket)
{
if (!m_allowIncomingConnections)
{
// This network interface is not set to allow incoming connections
return;
}
CorePackets::InitiateConnectionPacket packet;
{
NetworkOutputSerializer networkSerializer(connectPacket.m_buffer, connectPacket.m_receivedBytes);
// First, serialize out the header
UdpPacketHeader header;
if (!header.SerializePacketFlags(networkSerializer))
{
return;
}
if (!static_cast<ISerializer&>(networkSerializer).Serialize(header, "Header"))
{
return;
}
// Validate that this is really an InitiateConnection packet
if (header.GetPacketType() != aznumeric_cast<PacketType>(CorePackets::PacketType::InitiateConnectionPacket))
{
return;
}
// Next serialize the InitiateConnectionPacket itself
{
NetworkOutputSerializer tempPacketSerializer(networkSerializer.GetUnreadData(), networkSerializer.GetUnreadSize());
if (!static_cast<ISerializer&>(tempPacketSerializer).Serialize(packet, "Packet"))
{
return;
}
}
// Retrieve the connection type, and run application layer connection filtering (state checks, CIDR address filtering, etc..)
const ConnectResult connectResult = m_connectionListener.ValidateConnect(connectPacket.m_address, header, networkSerializer);
switch (connectResult)
{
case ConnectResult::Rejected:
return; // Failed validation, simply discard the connect message
case ConnectResult::Accepted:
break; // This is not actually an expected return from this code path, assume it's a client connection
}
}
// We've passed all our security checks, so now we're free to allocate memory and track the new connection
// How long should we sit in the timeout queue before heartbeating or disconnecting
const ConnectionId connectionId = m_connectionSet.GetNextConnectionId();
const TimeoutId timeoutId = m_connectionTimeoutQueue.RegisterItem(aznumeric_cast<uint64_t>(connectionId), net_UdpTimeoutTimeMs);
AZLOG(Debug_UdpConnect, "Accepted new Udp Connection");
AZStd::unique_ptr<UdpConnection> connection = AZStd::make_unique<UdpConnection>(connectionId, connectPacket.m_address, *this, ConnectionRole::Acceptor);
UdpPacketEncodingBuffer dtlsData;
m_socket->AcceptDtlsEndpoint(connection->GetDtlsEndpoint(), connectPacket.m_address, dtlsData);
// We're accepting this connection, so we can immediately transition to a connected state
connection->m_state = ConnectionState::Connected;
connection->SetTimeoutId(timeoutId);
m_connectionListener.OnConnect(connection.get());
m_connectionSet.AddConnection(AZStd::move(connection));
}
void UdpNetworkInterface::RequestDisconnect(UdpConnection* connection, DisconnectReason reason, TerminationEndpoint endpoint)
{
if (connection == nullptr)
{
return;
}
connection->m_state = ConnectionState::Disconnecting;
m_removedConnections.emplace_back(RemovedConnection{ connection, reason, endpoint });
}
UdpNetworkInterface::ConnectionTimeoutFunctor::ConnectionTimeoutFunctor(UdpNetworkInterface& networkInterface)
: m_networkInterface(networkInterface)
{
;
}
TimeoutResult UdpNetworkInterface::ConnectionTimeoutFunctor::HandleTimeout(TimeoutQueue::TimeoutItem& item)
{
const ConnectionId connectionId = ConnectionId(aznumeric_cast<uint32_t>(item.m_userData));
UdpConnection* udpConnection = static_cast<UdpConnection*>(m_networkInterface.m_connectionSet.GetConnection(connectionId));
if (udpConnection == nullptr)
{
// We've already deleted this connection
return TimeoutResult::Delete;
}
if (udpConnection->GetConnectionState() == ConnectionState::Connecting)
{
if (udpConnection->GetDtlsEndpoint().IsConnecting())
{
udpConnection->CompleteHandshake();
return TimeoutResult::Refresh;
}
}
if (udpConnection->GetConnectionRole() == ConnectionRole::Connector)
{
udpConnection->SendUnreliablePacket(CorePackets::HeartbeatPacket());
}
else if (net_UdpTimeoutConnections)
{
udpConnection->Disconnect(DisconnectReason::Timeout, TerminationEndpoint::Local);
return TimeoutResult::Delete;
}
return TimeoutResult::Refresh;
}
UdpNetworkInterface::PacketTimeoutFunctor::PacketTimeoutFunctor(UdpNetworkInterface& networkInterface)
: m_networkInterface(networkInterface)
{
;
}
TimeoutResult UdpNetworkInterface::PacketTimeoutFunctor::HandleTimeout(TimeoutQueue::TimeoutItem& item)
{
ConnectionId connectionId;
PacketId packetId;
ReliabilityType reliability;
DecodeTimeoutId(item.m_userData, connectionId, packetId, reliability);
UdpConnection* connection = static_cast<UdpConnection*>(m_networkInterface.m_connectionSet.GetConnection(connectionId));
if (connection == nullptr)
{
AZLOG(NET_Debug, "Failed to look up connection for timed out packetId %u", aznumeric_cast<uint32_t>(packetId));
return TimeoutResult::Delete;
}
const PacketTimeoutResult result = connection->ProcessTimeout(packetId, reliability);
AZLOG(NET_Debug, "Timeout triggered for packetId %u with result %s", aznumeric_cast<uint32_t>(packetId), GetEnumString(result));
switch (result)
{
case PacketTimeoutResult::Acked:
// Packet was already acked, just discard this timeout entry
return TimeoutResult::Delete;
case PacketTimeoutResult::Pending:
// Packet timed out before we received any info about it's sequence from the remote endpoint
// The connection latency may have increased, and our Rtt metrics may still be adjusting..
// Just throw it back into the timeout queue
return TimeoutResult::Refresh;
case PacketTimeoutResult::Lost:
// Packet timed out and was not acked, so we consider it lost
m_networkInterface.m_connectionListener.OnPacketLost(connection, packetId);
break;
}
return TimeoutResult::Delete;
}
}
@@ -0,0 +1,155 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/UdpTransport/UdpPacketHeader.h>
#include <AzNetworking/UdpTransport/UdpConnectionSet.h>
#include <AzNetworking/UdpTransport/UdpReaderThread.h>
#include <AzNetworking/ConnectionLayer/IConnection.h>
#include <AzNetworking/ConnectionLayer/ConnectionEnums.h>
#include <AzNetworking/Framework/INetworkInterface.h>
#include <AzNetworking/DataStructures/TimeoutQueue.h>
#include <AzCore/Threading/ThreadSafeDeque.h>
#include <AzCore/std/containers/vector.h>
namespace AzNetworking
{
class IConnectionListener;
class ICompressor;
// 20 byte IPv4 header + 8 byte UDP header
static const uint32_t UdpPacketHeaderSize = 20 + 8;
static const uint32_t DtlsPacketHeaderSize = 13; // DTLS1_RT_HEADER_LENGTH
//! @class UdpNetworkInterface
//! @brief This class implements a UDP network interface.
class UdpNetworkInterface final
: public INetworkInterface
{
public:
//! Constructor.
//! @param name the name of this network interface instance.
//! @param connectionListener reference to the connection listener responsible for handling all connection events
//! @param trustZone the trust level assigned to this network interface, server to server or client to server
//! @param readerThread pointer to the reader thread to be bound to this network interface
UdpNetworkInterface(AZ::Name name, IConnectionListener& connectionListener, TrustZone trustZone, UdpReaderThread& readerThread);
~UdpNetworkInterface() override;
//! INetworkInterface interface.
//! @{
AZ::Name GetName() const override;
ProtocolType GetType() const override;
TrustZone GetTrustZone() const override;
uint16_t GetPort() const override;
IConnectionSet& GetConnectionSet() override;
IConnectionListener& GetConnectionListener() override;
bool Listen(uint16_t port) override;
ConnectionId Connect(const IpAddress& remoteAddress) override;
void Update(AZ::TimeMs deltaTimeMs) override;
bool SendReliablePacket(ConnectionId connectionId, const IPacket& packet) override;
PacketId SendUnreliablePacket(ConnectionId connectionId, const IPacket& packet) override;
bool WasPacketAcked(ConnectionId connectionId, PacketId packetId) override;
bool Disconnect(ConnectionId connectionId, DisconnectReason reason) override;
//! @}
//! Returns true if this is an encrypted socket, false if not.
//! @return boolean true if this is an encrypted socket, false if not
bool IsEncrypted() const;
//! Returns true if this connection instance is in an open state, and is capable of actively sending and receiving packets.
//! @return boolean true if this connection instance is in an open state
bool IsOpen() const;
private:
//! Registers a packet with a timeout queue on the provided connection.
//! @param connectionId identifier of the connection to register
//! @param packetId packet id of the packet to register for the given connection
//! @param reliability whether or not to guarantee delivery
//! @param metrics reference to the connections metrics instance
void RegisterWithTimeoutQueue(ConnectionId connectionId, PacketId packetId, ReliabilityType reliability, const ConnectionMetrics& metrics);
//! Decompresses an incoming packet data buffer.
//! @param packetBuffer the compressed packet buffer to decode
//! @param packetSize the size of the compressed packet buffer
//! @param packetBufferOut the decoded data
//! @return boolean true on success, false on failure
bool DecompressPacket(const uint8_t* packetBuffer, size_t packetSize, UdpPacketEncodingBuffer& packetBufferOut) const;
//! Sends a packet to the remote connection.
//! @param connection the UdpConnection instance to send the packet on
//! @param packet serializable object to transmit
//! @param reliableSequence the reliable sequence number to use for this packet, providing InvalidSequenceId will cause the packet to be sent unreliably
//! @return packet id for the transmitted packet
PacketId SendPacket(UdpConnection& connection, const IPacket& packet, SequenceId reliableSequence);
//! Accepts an incoming udp connection.
//! @param connectPacket the initial connectPacket
void AcceptConnection(const UdpReaderThread::ReceivedPacket& connectPacket);
//! Internal helper to cleanly remove a connection from the network interface.
//! @param connection pointer to the connection to disconnect
//! @param reason reason for the disconnect
//! @param endpoint whether the disconnection was initiated locally or remotely
void RequestDisconnect(UdpConnection* connection, DisconnectReason reason, TerminationEndpoint endpoint);
AZ_DISABLE_COPY_MOVE(UdpNetworkInterface);
struct ConnectionTimeoutFunctor final
: public ITimeoutHandler
{
ConnectionTimeoutFunctor(UdpNetworkInterface& networkInterface);
TimeoutResult HandleTimeout(TimeoutQueue::TimeoutItem& item) override;
private:
AZ_DISABLE_COPY_MOVE(ConnectionTimeoutFunctor);
UdpNetworkInterface& m_networkInterface;
};
struct PacketTimeoutFunctor final
: public ITimeoutHandler
{
PacketTimeoutFunctor(UdpNetworkInterface& networkInterface);
TimeoutResult HandleTimeout(TimeoutQueue::TimeoutItem& item) override;
private:
AZ_DISABLE_COPY_MOVE(PacketTimeoutFunctor);
UdpNetworkInterface& m_networkInterface;
};
AZ::Name m_name;
TrustZone m_trustZone;
uint16_t m_port = 0;
bool m_allowIncomingConnections = false;
IConnectionListener& m_connectionListener;
UdpConnectionSet m_connectionSet;
TimeoutQueue m_connectionTimeoutQueue;
TimeoutQueue m_packetTimeoutQueue;
AZStd::unique_ptr<UdpSocket> m_socket;
AZStd::unique_ptr<ICompressor> m_compressor;
UdpReaderThread& m_readerThread;
struct RemovedConnection
{
UdpConnection* m_connection;
DisconnectReason m_reason;
TerminationEndpoint m_endpoint;
};
AZStd::vector<RemovedConnection> m_removedConnections;
UdpPacketEncodingBuffer m_decryptBuffer;
UdpPacketEncodingBuffer m_decompressBuffer;
friend class UdpReliableQueue;
friend class UdpConnection; // For access to private RequestDisconnect() method
};
}
@@ -0,0 +1,96 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/UdpPacketHeader.h>
#include <AzNetworking/UdpTransport/UdpPacketTracker.h>
namespace AzNetworking
{
UdpPacketHeader::UdpPacketHeader()
: m_packetType(PacketType{ 0 })
, m_localSequence(InvalidSequenceId)
, m_remoteSequence(InvalidSequenceId)
, m_reliableSequence(InvalidSequenceId)
, m_sequenceWindow(0)
, m_localRolloverCount(InvalidSequenceRolloverCount)
{
;
}
UdpPacketHeader::UdpPacketHeader(UdpPacketTracker& packetTracker, PacketType packetType, SequenceId reliableSequence)
: m_packetType(packetType)
, m_localSequence(InvalidSequenceId)
, m_remoteSequence(packetTracker.GetLastReceivedSequenceId())
, m_reliableSequence(reliableSequence)
, m_sequenceWindow(packetTracker.GetSequencedAckHistory(m_sequenceWindow)) // m_sequenceWindow is being passed in uninitialized, okay here since it's just a uint32_t
, m_localRolloverCount(InvalidSequenceRolloverCount)
{
const PacketId packetId = packetTracker.GetNextPacketId();
m_localSequence = ToSequenceId(packetId);
m_localRolloverCount = ToRolloverCount(packetId);
}
UdpPacketHeader::UdpPacketHeader(PacketType packetType, PacketId packetId)
: m_packetType(packetType)
, m_localSequence(InvalidSequenceId)
, m_remoteSequence(InvalidSequenceId)
, m_reliableSequence(InvalidSequenceId)
, m_sequenceWindow(0)
, m_localRolloverCount(InvalidSequenceRolloverCount)
{
m_localSequence = ToSequenceId(packetId);
m_localRolloverCount = ToRolloverCount(packetId);
}
UdpPacketHeader::UdpPacketHeader
(
PacketType packetType,
SequenceId localSequence,
SequenceId remoteSequence,
SequenceId reliableSequence,
BitsetChunk sequenceWindow,
SequenceRolloverCount localRolloverCount
)
: m_packetType(packetType)
, m_localSequence(localSequence)
, m_remoteSequence(remoteSequence)
, m_reliableSequence(reliableSequence)
, m_sequenceWindow(sequenceWindow)
, m_localRolloverCount(localRolloverCount)
{
;
}
bool UdpPacketHeader::Serialize(ISerializer& serializer)
{
bool isReliable = GetIsReliable();
serializer.Serialize(m_packetType, "PacketType");
serializer.Serialize(m_localSequence, "LocalSequence");
serializer.Serialize(m_remoteSequence, "RemoteSequence");
serializer.Serialize(m_sequenceWindow, "SequenceWindow");
serializer.Serialize(isReliable, "IsReliable");
// If the packet is flagged as reliable, serialize the reliable sequence id
if (isReliable)
{
serializer.Serialize(m_reliableSequence, "ReliableSequence");
}
return serializer.IsValid();
}
bool UdpPacketHeader::SerializePacketFlags(ISerializer& serializer)
{
return serializer.Serialize(m_packetFlags, "PacketFlags");
}
}
@@ -0,0 +1,132 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/PacketLayer/IPacket.h>
#include <AzNetworking/PacketLayer/IPacketHeader.h>
#include <AzNetworking/Serialization/ISerializer.h>
#include <AzNetworking/ConnectionLayer/ConnectionMetrics.h>
#include <AzNetworking/DataStructures/RingBufferBitset.h>
namespace AzNetworking
{
class UdpPacketTracker;
//! @class UdpPacketHeader
//! @brief Udp packet header class.
class UdpPacketHeader final
: public IPacketHeader
{
friend class UdpPacketIdWindow;
public:
AZ_RTTI(UdpPacketHeader, "{21A11FF3-6829-4A59-9906-C06EF7F39AC1}", IPacketHeader);
//! Default constructor, for when receiving a header from a remote connection.
UdpPacketHeader();
//! Constructor for generating a new header to send to a remote connection.
//! @param packetTracker packet delivery tracker instance for the connection in question
//! @param packetType type of packet
//! @param reliableSequence reliable sequence value, or InvalidSequenceId if the packet is unreliable
UdpPacketHeader(UdpPacketTracker& packetTracker, PacketType packetType, SequenceId reliableSequence);
//! Constructor for generating generic header with just a packet id, used for dispatching a bulk message.
//! @param packetType type of packet
//! @param packetId packet id we're duplicating
UdpPacketHeader(PacketType packetType, PacketId packetId);
//! Full constructor for unit tests.
//! @param packetType type of packet
//! @param localSequence local sequence number, this is the sequence for this packet instance
//! @param remoteSequence remote sequence number for ack replication, this is the latest sequence we've received from the remote endpoint
//! @param reliableSequence if this is a reliable packet, this is the reliable sequence number
//! @param sequenceWindow this is the ack vector for ack replication, corresponding to remoteSequence
//! @param localRolloverCount this is the reconstructed rollover count, used to convert localSequence to a full PacketId
UdpPacketHeader
(
PacketType packetType,
SequenceId localSequence,
SequenceId remoteSequence,
SequenceId reliableSequence,
BitsetChunk sequenceWindow,
SequenceRolloverCount localRolloverCount
);
~UdpPacketHeader() override = default;
//! IPacketHeader interface.
// @{
PacketType GetPacketType() const override;
PacketId GetPacketId() const override;
bool IsPacketFlagSet(PacketFlag flag) const override;
void SetPacketFlag(PacketFlag flag, bool value) override;
// @}
//! Sets the packet flag bitset for this packet.
//! @param flags The packet flag bitset
void SetPacketFlags(PacketFlagBitset flags);
//! Returns whether or not this header is for a reliably transmitted packet.
//! @return whether or not this header is for a reliably transmitted packet
bool GetIsReliable() const;
//! Retrieve the local sequence from this packet header.
//! @return packet header local sequence
SequenceId GetLocalSequenceId() const;
//! Retrieve the remote sequence being acked.
//! @return packet header remote sequence
SequenceId GetRemoteSequenceId() const;
//! Retrieve the reliable sequence if this was a reliable packet, InvalidSequenceId otherwise.
//! @return packet header reliable sequence if this was a reliable packet, InvalidSequenceId otherwise
SequenceId GetReliableSequenceId() const;
//! Retrieve the sequence window from this packet header.
//! @return the sequence window from this packet header
BitsetChunk GetSequenceWindow() const;
//! Retrieve the sequence rollover count from this packet header.
//! @return the sequence rollover count from this packet header
SequenceRolloverCount GetSequenceRolloverCount() const;
//! Base serialize method for all serializable structures or classes to implement.
//! @param serializer ISerializer instance to use for serialization
//! @return boolean true for success, false for serialization failure
bool Serialize(ISerializer& serializer);
//! Specialized serialize method for UDP Packet Flags
//! @param serializer ISerializer instance to use for serialization
//! @return boolean true for success, false for serialization failure
bool SerializePacketFlags(ISerializer& serializer);
private:
void SetLocalRolloverCount(SequenceRolloverCount rolloverCount);
PacketType m_packetType;
SequenceId m_localSequence;
SequenceId m_remoteSequence;
SequenceId m_reliableSequence;
BitsetChunk m_sequenceWindow;
SequenceRolloverCount m_localRolloverCount;
// UDP Packet flags are not serialized by the Serialize method and must be serialized separately
PacketFlagBitset m_packetFlags;
};
}
#include <AzNetworking/UdpTransport/UdpPacketHeader.inl>
@@ -0,0 +1,77 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
namespace AzNetworking
{
inline PacketType UdpPacketHeader::GetPacketType() const
{
return m_packetType;
}
inline PacketId UdpPacketHeader::GetPacketId() const
{
AZ_Assert(m_localRolloverCount != InvalidSequenceRolloverCount, "UdpPacketHeader: header was not initialized properly, PacketId is invalid");
return MakePacketId(m_localRolloverCount, m_localSequence);
}
inline bool UdpPacketHeader::IsPacketFlagSet(PacketFlag flag) const
{
return m_packetFlags.GetBit(aznumeric_cast<uint32_t>(flag));
}
inline void UdpPacketHeader::SetPacketFlag(PacketFlag flag, bool value)
{
m_packetFlags.SetBit(aznumeric_cast<uint32_t>(flag), value);
}
inline void UdpPacketHeader::SetPacketFlags(PacketFlagBitset flags)
{
m_packetFlags = flags;
}
inline bool UdpPacketHeader::GetIsReliable() const
{
return (m_reliableSequence != InvalidSequenceId);
}
inline SequenceId UdpPacketHeader::GetLocalSequenceId() const
{
return m_localSequence;
}
inline SequenceId UdpPacketHeader::GetRemoteSequenceId() const
{
return m_remoteSequence;
}
inline SequenceId UdpPacketHeader::GetReliableSequenceId() const
{
return m_reliableSequence;
}
inline BitsetChunk UdpPacketHeader::GetSequenceWindow() const
{
return m_sequenceWindow;
}
inline SequenceRolloverCount UdpPacketHeader::GetSequenceRolloverCount() const
{
return m_localRolloverCount;
}
inline void UdpPacketHeader::SetLocalRolloverCount(SequenceRolloverCount rolloverCount)
{
m_localRolloverCount = rolloverCount;
}
}
@@ -0,0 +1,216 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/UdpPacketIdWindow.h>
#include <AzNetworking/UdpTransport/UdpPacketHeader.h>
#include <AzNetworking/UdpTransport/UdpConnection.h>
#include <AzNetworking/DataStructures/FixedSizeBitset.h>
#include <AzCore/Console/ILogger.h>
namespace AzNetworking
{
const char* GetEnumString(PacketAckState value)
{
switch (value)
{
case PacketAckState::Acked:
return "PacketAckState::Acked";
case PacketAckState::Nacked:
return "PacketAckState::Nacked";
case PacketAckState::Unknown_TooNew:
return "PacketAckState::Unknown_TooNew";
case PacketAckState::Unknown_TooOld:
return "PacketAckState::Unknown_TooOld";
}
return "INVALID";
}
UdpPacketIdWindow::UdpPacketIdWindow()
: m_headSequenceId(SequenceId{ 0 })
, m_headPacketId(InvalidPacketId)
, m_sequenceRolloverCount(SequenceRolloverCount{ 0 })
{
;
}
void UdpPacketIdWindow::Reset()
{
m_headSequenceId = SequenceId{ 0 };
m_headPacketId = InvalidPacketId;
m_sequenceRolloverCount = SequenceRolloverCount{ 0 };
m_ackWindow.Reset();
}
PacketAckState UdpPacketIdWindow::GetPacketAckStatus(PacketId packetId) const
{
// If we haven't heard from the remote endpoint yet, treat packets as having fallen outside our window
if (m_headPacketId == InvalidPacketId)
{
return PacketAckState::Nacked;
}
// Check if the requested sequence id is newer than any acked packet received
if (packetId > m_headPacketId)
{
AZLOG(NET_Acks, "Requested ack status for %u, head %u, too new", aznumeric_cast<uint32_t>(packetId), aznumeric_cast<uint32_t>(m_headPacketId));
return PacketAckState::Unknown_TooNew;
}
const PacketId packetDelta = m_headPacketId - packetId;
// Sequence is so old, it's now out of bounds of our packet ack tracker
if (aznumeric_cast<uint32_t>(packetDelta) >= m_ackWindow.GetValidBitCount())
{
AZLOG(NET_Acks, "Requested ack status for %u, head %u, too old", aznumeric_cast<uint32_t>(packetId), aznumeric_cast<uint32_t>(m_headPacketId));
return PacketAckState::Unknown_TooOld;
}
const bool acked = m_ackWindow.GetBit(aznumeric_cast<uint32_t>(packetDelta));
return acked ? PacketAckState::Acked : PacketAckState::Nacked;
}
BitsetChunk& UdpPacketIdWindow::GetMostRecentAckState(BitsetChunk& outWindow) const
{
const uint64_t firstChunk = aznumeric_cast<uint64_t>(m_ackWindow.GetBitsetElement(0));
const uint64_t secondChunk = aznumeric_cast<uint64_t>(m_ackWindow.GetBitsetElement(1));
const uint32_t unusedHeadBits = m_ackWindow.GetUnusedHeadBits();
const uint32_t usedHeadBits = m_ackWindow.NumBitsetChunkedBits - unusedHeadBits;
outWindow = aznumeric_cast<BitsetChunk>((secondChunk << usedHeadBits) | firstChunk);
return outWindow;
}
bool UdpPacketIdWindow::UpdateForReceivedPacket(UdpPacketHeader& header)
{
const SequenceId receivedSequenceId = header.GetLocalSequenceId();
if (SequenceMoreRecent(receivedSequenceId, m_headSequenceId))
{
const SequenceId sequenceDelta = SequenceId(receivedSequenceId - m_headSequenceId);
m_ackWindow.PushBackBits(aznumeric_cast<uint32_t>(sequenceDelta));
m_ackWindow.SetBit(0, true);
// We've already determined that receivedSequenceId is 'newer' than m_headSequenceId
// So if receivedSequenceId is numerically less than m_headSequenceId, we have rolled over
if (receivedSequenceId < m_headSequenceId)
{
++m_sequenceRolloverCount;
}
// This is the newest packet, it's rollover count is always the most recent rollover count
header.SetLocalRolloverCount(m_sequenceRolloverCount);
m_headSequenceId = receivedSequenceId;
m_headPacketId = MakePacketId(m_sequenceRolloverCount, m_headSequenceId);
}
else
{
// This is an out of order packet, we've previously received a newer sequence value
const SequenceId sequenceDelta = m_headSequenceId - receivedSequenceId;
if (aznumeric_cast<uint32_t>(sequenceDelta) >= m_ackWindow.GetValidBitCount())
{
// Too old to process
AZLOG(NET_DebugUdp, "Discarding old packet, sequence is too old to process");
return false;
}
if (m_ackWindow.GetBit(aznumeric_cast<uint32_t>(sequenceDelta)))
{
// Received packet is a duplicate of one already processed
AZLOG(NET_DebugUdp, "Discarding packet due to duplicated sequence id");
return false;
}
m_ackWindow.SetBit(aznumeric_cast<uint32_t>(sequenceDelta), true);
// Received sequence is 'older' than head sequence
if (receivedSequenceId < m_headSequenceId)
{
// If the 'older' received sequence is numerically less than head sequence, the rollover count is unchanged
header.SetLocalRolloverCount(m_sequenceRolloverCount);
}
else
{
// If the 'older' received sequence is numerically greater than than head sequence, we've received a packet bound to the previous rollover count
header.SetLocalRolloverCount(m_sequenceRolloverCount - SequenceRolloverCount{ 1 });
}
}
return true;
}
void UdpPacketIdWindow::UpdateForRemoteAckStatus(UdpConnection* connection, UdpPacketHeader& header)
{
const SequenceId receivedSequenceId = header.GetRemoteSequenceId();
const BitsetChunk sequenceWindow = header.GetSequenceWindow();
// Reconstruct the ack state of the remote connection based on remote sequence number and ack bits
// Align the received and cached ack bit vectors
if (SequenceMoreRecent(receivedSequenceId, m_headSequenceId))
{
// We've already determined that receivedSequenceId is 'newer' than m_headSequenceId
// So if receivedSequenceId is numerically less than m_headSequenceId, we have rolled over
if (receivedSequenceId < m_headSequenceId)
{
++m_sequenceRolloverCount;
}
AZLOG
(
NET_DebugUdp,
"Updating ack data, old head sequence %u, new head sequence %u, ack vector %X",
aznumeric_cast<uint32_t>(m_headSequenceId),
aznumeric_cast<uint32_t>(receivedSequenceId),
sequenceWindow
);
const AZ::TimeMs currentTimeMs = AZ::GetElapsedTimeMs();
const SequenceId sequenceIdDelta = SequenceId(receivedSequenceId - m_headSequenceId);
const PacketId receivedPacketId = MakePacketId(m_sequenceRolloverCount, receivedSequenceId);
m_headSequenceId = receivedSequenceId;
m_headPacketId = receivedPacketId;
m_ackWindow.PushBackBits(aznumeric_cast<uint32_t>(sequenceIdDelta));
for (uint32_t bit = 0; bit < m_ackWindow.NumBitsetChunkedBits; ++bit)
{
if (!GetBitHelper(sequenceWindow, bit))
{
continue;
}
if (!m_ackWindow.GetBit(bit))
{
m_ackWindow.SetBit(bit, true);
AZLOG(NET_DebugUdp, "Acking packet ID %u", aznumeric_cast<uint32_t>(receivedPacketId) - bit);
if (connection != nullptr)
{
connection->ProcessAcked(receivedPacketId - aznumeric_cast<PacketId>(bit), currentTimeMs);
}
}
}
}
}
void UdpPacketIdWindow::PrintStatus() const
{
AZLOG_INFO
(
"%u - %08X:%08X:%08X:%08X",
aznumeric_cast<uint32_t>(m_headSequenceId),
aznumeric_cast<uint32_t>(m_ackWindow.GetBitsetElement(0)),
aznumeric_cast<uint32_t>(m_ackWindow.GetBitsetElement(1)),
aznumeric_cast<uint32_t>(m_ackWindow.GetBitsetElement(2)),
aznumeric_cast<uint32_t>(m_ackWindow.GetBitsetElement(3))
);
}
}
@@ -0,0 +1,93 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/DataStructures/RingBufferBitset.h>
#include <AzNetworking/Utilities/NetworkCommon.h>
#include <AzNetworking/UdpTransport/UdpPacketHeader.h>
namespace AzNetworking
{
class UdpConnection;
enum class PacketAckState
{
Acked,
Nacked,
Unknown_TooNew,
Unknown_TooOld
};
const char *GetEnumString(PacketAckState value);
//! @class UdpPacketIdWindow
//! @brief Wrapper class that handles management of ack status for a large range of packet id's.
class UdpPacketIdWindow
{
public:
static const uint32_t PacketWindowAckCount = 16384; // The total number of packet id's to track
using PacketAckContainer = RingbufferBitset<PacketWindowAckCount>;
UdpPacketIdWindow();
~UdpPacketIdWindow();
//! Resets the packet ack window, putting it back into a default initialized state.
void Reset();
//! Retrieves the latest ack status for a given PacketId.
//! @param packetId the identifier of the packet to lookup the latest ack status of
//! @return the current status for the given PacketId
PacketAckState GetPacketAckStatus(PacketId packetId) const;
//! Returns the latest known SequenceId managed by this UdpPacketIdWindow instance.
//! @return the latest known SequenceId managed by this UdpPacketIdWindow instance
SequenceId GetHeadSequenceId() const;
//! Returns the SequenceRolloverCount for the number of times the sequence ids have rolled over.
//! @return the number of times the sequence ids have rolled over
SequenceRolloverCount GetSequenceRolloverCount() const;
//! Returns the latest ack vector contained in this UdpPacketIdWindow.
//! @param outWindow the window to store the most recent ack state in
//! @return reference to the window where the ack vector was stored
BitsetChunk& GetMostRecentAckState(BitsetChunk& outWindow) const;
//! Returns the underlying packet ack container used for retaining packet ack status.
//! @return the underlying packet ack container used for retaining packet ack status
const PacketAckContainer& GetPacketAckContainer() const;
//! Updates the internal ack state for the newly received PacketId.
//! @param header the packet header received to process
//! @return boolean false if updating failed, and the packet should be discarded without further processing
bool UpdateForReceivedPacket(UdpPacketHeader& header);
//! Updates the internal ack state to replicate the received remote ack status.
//! @param connection the connection this packet was received on, used for ack callbacks
//! @param header the packet header received to process
void UpdateForRemoteAckStatus(UdpConnection* connection, UdpPacketHeader& header);
//! Prints the most recent ack window status to the Logger.
void PrintStatus() const;
private:
SequenceId m_headSequenceId;
PacketId m_headPacketId;
PacketAckContainer m_ackWindow;
SequenceRolloverCount m_sequenceRolloverCount;
};
}
#include <AzNetworking/UdpTransport/UdpPacketIdWindow.inl>
@@ -0,0 +1,36 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
namespace AzNetworking
{
inline UdpPacketIdWindow::~UdpPacketIdWindow()
{
Reset();
}
inline SequenceId UdpPacketIdWindow::GetHeadSequenceId() const
{
return m_headSequenceId;
}
inline SequenceRolloverCount UdpPacketIdWindow::GetSequenceRolloverCount() const
{
return m_sequenceRolloverCount;
}
inline const UdpPacketIdWindow::PacketAckContainer& UdpPacketIdWindow::GetPacketAckContainer() const
{
return m_ackWindow;
}
}
@@ -0,0 +1,49 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/UdpPacketTracker.h>
namespace AzNetworking
{
UdpPacketTracker::UdpPacketTracker()
: m_nextPacketId(InvalidPacketId)
{
;
}
UdpPacketTracker::~UdpPacketTracker()
{
;
}
void UdpPacketTracker::Reset()
{
m_nextPacketId = InvalidPacketId;
m_receivedWindow.Reset();
m_acknowledgedWindow.Reset();
}
bool UdpPacketTracker::ProcessReceived(UdpConnection* connection, UdpPacketHeader& header)
{
if (!m_receivedWindow.UpdateForReceivedPacket(header))
{
return false;
}
m_acknowledgedWindow.UpdateForRemoteAckStatus(connection, header);
return true;
}
PacketAckState UdpPacketTracker::GetPacketAckStatus(PacketId packetId) const
{
return m_acknowledgedWindow.GetPacketAckStatus(packetId);
}
}
@@ -0,0 +1,76 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/DataStructures/FixedSizeBitset.h>
#include <AzNetworking/DataStructures/TimeoutQueue.h>
#include <AzNetworking/ConnectionLayer/IConnection.h>
#include <AzNetworking/ConnectionLayer/SequenceGenerator.h>
#include <AzNetworking/UdpTransport/UdpPacketHeader.h>
#include <AzNetworking/UdpTransport/UdpPacketIdWindow.h>
namespace AzNetworking
{
//! @class UdpPacketTracker
//! @brief packet tracking mechanism for sending, acking, and detecting dropped packets.
class UdpPacketTracker
{
public:
UdpPacketTracker();
~UdpPacketTracker();
//! Resets all internal state for this packet tracker.
void Reset();
//! Returns the next packet id for this UdpPacketTracker instance.
//! @return the next sequence id for this UdpPacketTracker instance
PacketId GetNextPacketId();
//! Process a received packet header.
//! @param connection the connection this packet was received on, used for ack callbacks
//! @param header the packet header received to process
//! @return boolean true on successful handling of the received header
bool ProcessReceived(UdpConnection* connection, UdpPacketHeader& header);
//! Returns whether or not a particular packet was confirmed received by the remote connection.
//! @param sequenceId the sequence number of the packet to check the ack status of
//! @return boolean true if the requested sequence id was acked, false otherwise
PacketAckState GetPacketAckStatus(PacketId packetId) const;
//! Returns the last received remote sequence id.
//! @return the last received remote sequence id
SequenceId GetLastReceivedSequenceId() const;
//! Returns a bit sequence representing the last received packets from the remote connection.
//! @param outWindow window to store the output bit sequence in
//! @return reference to the output parameter
BitsetChunk& GetSequencedAckHistory(BitsetChunk& outWindow) const;
//! Const access to the packet trackers received window.
//! @return const reference to the packet trackers received window
const UdpPacketIdWindow& GetReceivedWindow() const;
//! Const access to the packet trackers acknowledged window.
//! @return const reference to the packet trackers acknowledged window
const UdpPacketIdWindow& GetAcknowledgedWindow() const;
private:
PacketId m_nextPacketId;
UdpPacketIdWindow m_receivedWindow; //< Packets received that were generated by the remote endpoint
UdpPacketIdWindow m_acknowledgedWindow; //< Packets we sent that have been acked by the remote endpoint
};
}
#include <AzNetworking/UdpTransport/UdpPacketTracker.inl>
@@ -0,0 +1,48 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
namespace AzNetworking
{
inline PacketId UdpPacketTracker::GetNextPacketId()
{
const PacketId nextPacketId = PacketId(++m_nextPacketId);
if (ToSequenceId(nextPacketId) == InvalidSequenceId)
{
return PacketId(++m_nextPacketId);
}
return nextPacketId;
}
inline SequenceId UdpPacketTracker::GetLastReceivedSequenceId() const
{
return m_receivedWindow.GetHeadSequenceId();
}
inline BitsetChunk& UdpPacketTracker::GetSequencedAckHistory(BitsetChunk& outWindow) const
{
return m_receivedWindow.GetMostRecentAckState(outWindow);
}
inline const UdpPacketIdWindow& UdpPacketTracker::GetReceivedWindow() const
{
return m_receivedWindow;
}
inline const UdpPacketIdWindow& UdpPacketTracker::GetAcknowledgedWindow() const
{
return m_acknowledgedWindow;
}
}
@@ -0,0 +1,218 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/UdpReaderThread.h>
#include <AzNetworking/UdpTransport/UdpSocket.h>
#include <AzNetworking/Utilities/NetworkCommon.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Console/ILogger.h>
namespace AzNetworking
{
static constexpr AZ::TimeMs ReaderThreadUpdateRateMs{ 10 };
AZ_CVAR(AZ::TimeMs, net_UdpMaxReadTimeMs, ReaderThreadUpdateRateMs, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The amount of time to allow the reader thread to read data off registered sockets");
UdpReaderThread::UdpReaderThread()
: TimedThread("UdpReaderThread", ReaderThreadUpdateRateMs)
{
;
}
UdpReaderThread::~UdpReaderThread()
{
Stop();
Join();
}
bool UdpReaderThread::RegisterSocket(UdpSocket* socket)
{
if (SocketExists(socket))
{
AZLOG_ERROR("Attempting to add a duplicate socket to the UdpReaderThread");
return false;
}
m_pendingAdds.push_back(socket);
if (!IsRunning())
{
Start();
}
return true;
}
void UdpReaderThread::UnregisterSocket(UdpSocket* socket)
{
// We need to null out the socket immediately in both the front and back
// buffers so that the reader thread doesn't try and use a deleted socket
AZStd::scoped_lock<AZStd::recursive_mutex> lock(m_mutex);
{
const int32_t frontIndex = 1 - m_backIndex;
ReaderBuffer& front = m_readerBuffers[frontIndex];
for (auto& socketEntry : front.m_entries)
{
if (socketEntry.m_socket == socket)
{
socketEntry.m_socket = nullptr;
}
}
}
{
ReaderBuffer& back = m_readerBuffers[m_backIndex];
for (auto& socketEntry : back.m_entries)
{
if (socketEntry.m_socket == socket)
{
socketEntry.m_socket = nullptr;
}
}
}
}
const UdpReaderThread::ReceivedPackets* UdpReaderThread::GetReceivedPackets(UdpSocket* socket) const
{
const int32_t frontIndex = 1 - m_backIndex;
const ReaderBuffer& front = m_readerBuffers[frontIndex];
for (const auto& socketEntry : front.m_entries)
{
if (socketEntry.m_socket == socket)
{
return &(socketEntry.m_receivedPackets);
}
}
return nullptr;
}
void UdpReaderThread::SwapBuffers()
{
const int32_t frontIndex = 1 - m_backIndex;
ReaderBuffer& front = m_readerBuffers[frontIndex];
// Clear all the packets we've already processed on our main thread
for (auto& socketEntry : front.m_entries)
{
socketEntry.m_receivedPackets.clear();
}
AZStd::scoped_lock<AZStd::recursive_mutex> lock(m_mutex);
{
// This scope is sync-safe between the main and reader threads
ReaderBuffer& back = m_readerBuffers[m_backIndex];
for (UdpSocket* socket : m_pendingAdds)
{
front.m_entries.emplace_back(SocketEntry{ socket, ReceivedPackets() });
back.m_entries.emplace_back(SocketEntry{ socket, ReceivedPackets() });
}
m_pendingAdds.clear();
AZStd::remove_if(front.m_entries.begin(), front.m_entries.end(), [](auto& socketEntry) { return socketEntry.m_socket == nullptr; });
AZStd::remove_if(back.m_entries.begin(), back.m_entries.end(), [](auto& socketEntry) { return socketEntry.m_socket == nullptr; });
m_backIndex = 1 - m_backIndex;
m_readerBuffers[m_backIndex].m_receiveBuffer.Resize(0);
}
}
uint32_t UdpReaderThread::GetSocketCount() const
{
const int32_t frontIndex = 1 - m_backIndex;
const ReaderBuffer& front = m_readerBuffers[frontIndex];
return aznumeric_cast<uint32_t>(front.m_entries.size());
}
AZ::TimeMs UdpReaderThread::GetUpdateTimeMs() const
{
return m_updateTimeMs;
}
bool UdpReaderThread::SocketExists(UdpSocket* socket) const
{
const int32_t frontIndex = 1 - m_backIndex;
const ReaderBuffer& front = m_readerBuffers[frontIndex];
for (auto& socketEntry : front.m_entries)
{
if (socketEntry.m_socket == socket)
{
return true;
}
}
return false;
}
void UdpReaderThread::OnStart()
{
;
}
void UdpReaderThread::OnStop()
{
;
}
void UdpReaderThread::OnUpdate(AZ::TimeMs updateRateMs)
{
AZ::TimeMs startTimeMs = AZ::GetElapsedTimeMs();
AZStd::scoped_lock<AZStd::recursive_mutex> lock(m_mutex);
ReaderBuffer& back = m_readerBuffers[m_backIndex];
ByteBuffer<MaxUdpReceiveBufferSize>& receiveBuffer = back.m_receiveBuffer;
for (auto& socketEntry : back.m_entries)
{
UdpSocket* socket = socketEntry.m_socket;
if (socket == nullptr)
{
continue;
}
ReceivedPackets& receivedPackets = socketEntry.m_receivedPackets;
for (;;)
{
AZ::TimeMs elapsedTimeMs = AZ::GetElapsedTimeMs() - startTimeMs;
if (elapsedTimeMs > updateRateMs)
{
AZLOG_INFO("ReceivePackets bled %d ms", aznumeric_cast<int32_t>(elapsedTimeMs - updateRateMs));
break;
}
IpAddress address;
const uint32_t bufferHead = receiveBuffer.GetSize();
if (bufferHead + MaxUdpTransmissionUnit >= receiveBuffer.GetCapacity())
{
AZLOG_INFO("Receive buffer full, leaving data on the socket. Size exceeded by %d",
aznumeric_cast<int32_t>(bufferHead + MaxUdpTransmissionUnit - receiveBuffer.GetCapacity()));
break;
}
uint8_t* dstData = receiveBuffer.GetBufferEnd();
receiveBuffer.Resize(bufferHead + MaxUdpTransmissionUnit);
const int32_t receivedBytes = socket->Receive(address, dstData, MaxUdpTransmissionUnit);
if (receivedBytes > 0 && !receivedPackets.full())
{
receivedPackets.push_back(ReceivedPacket(address, dstData, receivedBytes));
receiveBuffer.Resize(bufferHead + receivedBytes);
}
else
{
receiveBuffer.Resize(bufferHead);
break;
}
}
}
m_updateTimeMs += AZ::GetElapsedTimeMs() - startTimeMs;
}
UdpReaderThread::ReceivedPacket::ReceivedPacket(const IpAddress& address, const uint8_t* buffer, int32_t receivedBytes)
: m_address(address)
, m_buffer(buffer)
, m_receivedBytes(receivedBytes)
{
;
}
}
@@ -0,0 +1,103 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/Utilities/IpAddress.h>
#include <AzNetworking/DataStructures/ByteBuffer.h>
#include <AzNetworking/Utilities/TimedThread.h>
#include <AzNetworking/UdpTransport/DtlsEndpoint.h>
#include <AzCore/std/containers/unordered_map.h>
namespace AzNetworking
{
// Forwards
class UdpSocket;
//! @class UdpSocketReader
//! @brief reads lots of data off a UDP socket for deferred processing.
class UdpReaderThread
: public TimedThread
{
public:
static constexpr uint32_t MaxUdpReceivePacketCount = 1024;
static constexpr uint32_t MaxUdpReceiveBufferSize = MaxUdpReceivePacketCount * MaxUdpTransmissionUnit;
struct ReceivedPacket
{
ReceivedPacket() = default;
ReceivedPacket(const IpAddress& address, const uint8_t* buffer, int32_t receivedBytes);
IpAddress m_address;
const uint8_t* m_buffer = nullptr;
int32_t m_receivedBytes = 0;
};
using ReceivedPackets = AZStd::fixed_vector<ReceivedPacket, MaxUdpReceivePacketCount>;
UdpReaderThread();
~UdpReaderThread();
//! Adds the provided socket to the socket reader for processing.
//! @param socket pointer to the UdpSocket to read incoming data from
//! @return boolean true on success, false for failure
bool RegisterSocket(UdpSocket* socket);
//! Removes the provided socket from the socket reader for processing.
//! @param socket pointer to the UdpSocket to read incoming data from
void UnregisterSocket(UdpSocket* socket);
//! Returns the set of all packets consumed off the socket during the last call to ReadDataFromSocket().
//! @return all packets consumed off the socket during the last call to ReadDataFromSocket()
const ReceivedPackets* GetReceivedPackets(UdpSocket* socket) const;
//! Should be called immediately before any registered sockets have processed their received packets.
void SwapBuffers();
//! Returns the number of active sockets bound to this thread.
//! @return the number of active sockets bound to this thread
uint32_t GetSocketCount() const;
//! Gets the total elapsed time spent updating the background thread in milliseconds
//! @return the total elapsed time spent updating the background thread in milliseconds
AZ::TimeMs GetUpdateTimeMs() const;
private:
//! Helper to determine if a given socket is monitored by this reader thread instance
bool SocketExists(UdpSocket* socket) const;
void OnStart() override;
void OnStop() override;
void OnUpdate(AZ::TimeMs updateRateMs) override;
AZ_DISABLE_COPY_MOVE(UdpReaderThread);
struct SocketEntry
{
UdpSocket* m_socket;
ReceivedPackets m_receivedPackets;
};
struct ReaderBuffer
{
AZStd::vector<SocketEntry> m_entries;
ByteBuffer<MaxUdpReceiveBufferSize> m_receiveBuffer;
};
AZStd::recursive_mutex m_mutex;
int32_t m_backIndex = 0;
AZStd::array<ReaderBuffer, 2> m_readerBuffers;
AZStd::vector<UdpSocket*> m_pendingAdds;
AZ::TimeMs m_updateTimeMs = AZ::TimeMs{ 0 };
};
}
@@ -0,0 +1,146 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/UdpTransport/UdpReliableQueue.h>
#include <AzNetworking/UdpTransport/UdpConnection.h>
#include <AzNetworking/UdpTransport/UdpNetworkInterface.h>
#include <AzNetworking/UdpTransport/UdpPacketHeader.h>
#include <AzNetworking/Serialization/NetworkOutputSerializer.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Console/ILogger.h>
namespace AzNetworking
{
AZ_CVAR(uint32_t, net_MaxReliablePacketsInWindow, 16384, nullptr, AZ::ConsoleFunctorFlags::Null, "The maximum number of reliable packets to allow to be queued up before triggering a disconnect");
UdpReliableQueue::~UdpReliableQueue()
{
;
}
SequenceId UdpReliableQueue::GetNextSequenceId()
{
return m_reliableSequenceGenerator.GetNextSequenceId();
}
uint32_t UdpReliableQueue::GetQueueSize() const
{
return static_cast<uint32_t>(m_packetWindow.size());
}
bool UdpReliableQueue::PrepareForSend(PacketId packetId, SequenceId reliableSequenceId, const IPacket& packet)
{
AZLOG(NET_ReliableQueueDebug, "Inserting packetId %u with reliable sequenceId %u", static_cast<uint32_t>(packetId), static_cast<uint32_t>(reliableSequenceId));
if (m_packetWindow.size() > net_MaxReliablePacketsInWindow)
{
return false;
}
PendingPacketMap::const_iterator iter = m_packetWindow.find(packetId);
if (iter != m_packetWindow.end())
{
AZ_Assert(false, "Attempted to reinsert an existing packetId into the reliable queue");
return false;
}
m_packetWindow[packetId] = { reliableSequenceId, packet.Clone() };
return true;
}
bool UdpReliableQueue::OnPacketReceived(const UdpPacketHeader& header)
{
const SequenceId receivedSequence = header.GetReliableSequenceId();
if (SequenceMoreRecent(receivedSequence, m_lastReceivedReliableSequenceId))
{
const SequenceId sequenceDelta = SequenceId(receivedSequence - m_lastReceivedReliableSequenceId);
m_lastReceivedReliableSequenceId = receivedSequence;
m_receivedSequenceHistory.PushBackBits(static_cast<uint32_t>(sequenceDelta));
m_receivedSequenceHistory.SetBit(0, true);
}
else
{
const SequenceId sequenceDelta = SequenceId(m_lastReceivedReliableSequenceId - receivedSequence);
if (static_cast<uint32_t>(sequenceDelta) >= m_receivedSequenceHistory.GetValidBitCount())
{
// Too old to process
// @TODO: disconnect? out of range sequence is potentially an unrecoverable error
AZLOG(NET_ReliableQueue, "Reliable sequenceId is outside our tracked reliable packet window");
return false;
}
if (m_receivedSequenceHistory.GetBit(static_cast<uint32_t>(sequenceDelta)))
{
// Received packet is a duplicate of one already processed
AZLOG(NET_ReliableQueue, "Received duplicate of reliable packetId %u, discarding", static_cast<uint32_t>(receivedSequence));
return false;
}
m_receivedSequenceHistory.SetBit(static_cast<uint32_t>(sequenceDelta), true);
}
return true;
}
void UdpReliableQueue::OnPacketAcked([[maybe_unused]] UdpNetworkInterface& networkInterface,
[[maybe_unused]] UdpConnection& connection, PacketId packetId)
{
AZLOG(NET_ReliableQueueDebug, "Acked packetId %u", static_cast<uint32_t>(packetId));
PendingPacketMap::const_iterator iter = m_packetWindow.find(packetId);
if (iter != m_packetWindow.end())
{
m_packetWindow.erase(iter);
}
}
bool UdpReliableQueue::OnPacketLost(UdpNetworkInterface& networkInterface, UdpConnection& connection, PacketId packetId)
{
AZLOG(NET_ReliableQueueDebug, "Lost packetId %u", static_cast<uint32_t>(packetId));
bool result = false;
AZStd::unique_ptr<IPacket> lostPacket;
SequenceId lostReliableSequenceId = InvalidSequenceId;
PendingPacketMap::iterator iter = m_packetWindow.find(packetId);
if (iter != m_packetWindow.end())
{
AZ_Assert(iter->second.m_packet != nullptr, "Timed out reliable packet was nullptr");
lostPacket = AZStd::move(iter->second.m_packet); // This transfers ownership out of the pending packet to this local scoped alloc
lostReliableSequenceId = iter->second.m_reliableSequenceId;
m_packetWindow.erase(iter);
}
else
{
AZLOG_ERROR("Failed to find timed out packetId %u in reliable queue", static_cast<uint32_t>(packetId));
}
if (lostReliableSequenceId != InvalidSequenceId)
{
AZLOG(NET_ReliableQueue, "Resending reliable packetId %u due to loss", static_cast<uint32_t>(lostReliableSequenceId));
// This punches down an abstraction layer purposefully to resend using the existing reliable SequenceId
// NOTE: This will call back into UdpReliableQueue::PrepareForSend!!
if (networkInterface.SendPacket(connection, *lostPacket, lostReliableSequenceId) == InvalidPacketId)
{
// Packet failed to retransmit, meaning no retry attempt was made
// Since we've lost a reliable packet, the appropriate response is to terminate the connection
connection.Disconnect(DisconnectReason::ReliableTransportFailure, TerminationEndpoint::Local);
result = true;
}
networkInterface.GetMetrics().m_resentPackets++;
}
return result;
}
}
@@ -0,0 +1,87 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/PacketLayer/IPacket.h>
#include <AzNetworking/ConnectionLayer/IConnection.h>
#include <AzNetworking/ConnectionLayer/SequenceGenerator.h>
#include <AzNetworking/UdpTransport/UdpPacketIdWindow.h>
#include <AzCore/std/containers/unordered_map.h>
namespace AzNetworking
{
class NetworkOutputSerializer;
class UdpConnection;
class UdpNetworkInterface;
class UdpPacketHeader;
struct PendingPacket
{
SequenceId m_reliableSequenceId;
AZStd::unique_ptr<IPacket> m_packet;
};
//! @class UdpReliableQueue
//! @brief provides a reliability queue on top of the unreliable UDP connection layer.
class UdpReliableQueue
{
public:
UdpReliableQueue() = default;
~UdpReliableQueue();
//! Returns the next sequence id for this generator instance.
//! @return the next sequence id for this generator instance
SequenceId GetNextSequenceId();
//! Returns the number of unacked reliable messages still pending in the reliable queue.
//! @return the number of unacked reliable messages still pending in the reliable queue
uint32_t GetQueueSize() const;
//! Called when we're going to transmit a packet that we want to be reliable.
//! @param packetId packet id of the packet we're sending
//! @param reliableSequenceId the reliable sequence identifier of the packet we're sending
//! @param packet reference to the packet being transmitted
//! @return boolean true on success, false on failure
bool PrepareForSend(PacketId packetId, SequenceId reliableSequenceId, const IPacket& packet);
//! Called when a reliable packet has been received.
//! @param header the header for the received reliable packet
bool OnPacketReceived(const UdpPacketHeader& header);
//! Called when a packet is acked by the remote connection.
//! @param networkInterface reference to the network interface bound to the UdpConnection instance
//! @param connection reference of the connection instance generating the event
//! @param packetId packet id of the acked packet
void OnPacketAcked(UdpNetworkInterface& networkInterface, UdpConnection& connection, PacketId packetId);
//! Called when a packet is deemed lost by the remote connection.
//! @param networkInterface reference to the network interface bound to the UdpConnection instance
//! @param connection reference of the connection instance generating the event
//! @param packetId packet id of the lost packet
//! @return boolean true if the packet was lost and no retry attempt was made, false otherwise
bool OnPacketLost(UdpNetworkInterface& networkInterface, UdpConnection& connection, PacketId packetId);
private:
static constexpr uint32_t PacketWindowAckCount = 16384; // The total number of packet id's to track
using PacketAckContainer = RingbufferBitset<PacketWindowAckCount>;
using PendingPacketMap = AZStd::unordered_map<PacketId, PendingPacket>;
SequenceGenerator m_reliableSequenceGenerator;
SequenceId m_lastReceivedReliableSequenceId = InvalidSequenceId;
PacketAckContainer m_receivedSequenceHistory;
PendingPacketMap m_packetWindow;
};
}
@@ -0,0 +1,265 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzNetworking/Utilities/NetworkCommon.h>
#include <AzNetworking/UdpTransport/UdpSocket.h>
#include <AzNetworking/ConnectionLayer/IConnection.h>
#include <AzNetworking/Utilities/Endian.h>
#include <AzNetworking/Utilities/NetworkIncludes.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Console/ILogger.h>
#include <AzCore/std/algorithm.h>
namespace AzNetworking
{
AZ_CVAR(int32_t, net_UdpSendBufferSize, 1 * 1024 * 1024, nullptr, AZ::ConsoleFunctorFlags::Null, "Default UDP socket send buffer size");
AZ_CVAR(int32_t, net_UdpRecvBufferSize, 1 * 1024 * 1024, nullptr, AZ::ConsoleFunctorFlags::Null, "Default UDP socket receive buffer size");
AZ_CVAR(bool, net_UdpIgnoreWin10054, true, nullptr, AZ::ConsoleFunctorFlags::Null, "If true, will ignore 10054 socket errors on windows");
UdpSocket::~UdpSocket()
{
Close();
}
bool UdpSocket::IsEncrypted() const
{
return false;
}
DtlsEndpoint::ConnectResult UdpSocket::ConnectDtlsEndpoint(DtlsEndpoint&, const IpAddress&, UdpPacketEncodingBuffer&) const
{
// No-op, no encryption wrapper required
return DtlsEndpoint::ConnectResult::Complete;
}
DtlsEndpoint::ConnectResult UdpSocket::AcceptDtlsEndpoint(DtlsEndpoint&, const IpAddress&, const UdpPacketEncodingBuffer& dtlsData) const
{
if (dtlsData.GetSize() > 0)
{
AZLOG_WARN("Encryption is disabled on accepting endpoint, but connector provided a DTLS handshake blob. Check that encryption is properly disabled on *BOTH* endpoints");
return DtlsEndpoint::ConnectResult::Failed;
}
// No-op, no encryption wrapper required
return DtlsEndpoint::ConnectResult::Complete;
}
bool UdpSocket::Open(uint16_t port, CanAcceptConnections, TrustZone)
{
AZ_Assert(!IsOpen(), "Open called on an active socket");
if (IsOpen())
{
return false;
}
// Open the socket
{
m_socketFd = static_cast<SocketFd>(::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
if (!IsOpen())
{
const int32_t error = GetLastNetworkError();
AZLOG_ERROR("Failed to create socket (%d:%s)", error, GetNetworkErrorDesc(error));
m_socketFd = InvalidSocketFd;
return false;
}
}
// Handle binding
{
sockaddr_in hints;
hints.sin_family = AF_INET;
hints.sin_addr.s_addr = INADDR_ANY;
hints.sin_port = htons(port);
if (::bind(static_cast<int32_t>(m_socketFd), (const sockaddr *)&hints, sizeof(hints)) != 0)
{
const int32_t error = GetLastNetworkError();
AZLOG_ERROR("Failed to bind socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error));
return false;
}
}
if (!SetSocketBufferSizes(m_socketFd, net_UdpSendBufferSize, net_UdpRecvBufferSize))
{
return false;
}
if (!SetSocketNonBlocking(m_socketFd))
{
return false;
}
return true;
}
void UdpSocket::Close()
{
CloseSocket(m_socketFd);
m_socketFd = InvalidSocketFd;
}
#ifdef ENABLE_LATENCY_DEBUG
//! Checks packets deferred by latency debug and sends any that have passed their latency threshold
void UdpSocket::ProcessDeferredPackets()
{
const AZ::TimeMs currTimeMs = AZ::GetElapsedTimeMs();
while (m_sendBuffer.size() > 0)
{
const DeferredData& sendData = m_sendBuffer.front();
if (sendData.m_timeDeferredMs >= currTimeMs)
{
SendInternal(sendData.m_address, sendData.m_dataBuffer.GetBuffer(), sendData.m_dataBuffer.GetSize(), sendData.m_encrypt, *sendData.m_dtlsEndpoint);
m_sendBuffer.pop_front();
}
else
{
// Send buffer is sorted on append so all subsequent elements should also have time remaining
break;
}
}
}
#endif
int32_t UdpSocket::Send
(
const IpAddress& address,
const uint8_t* data,
uint32_t size,
bool encrypt,
DtlsEndpoint& dtlsEndpoint,
[[maybe_unused]] const ConnectionQuality& connectionQuality
) const
{
AZ_Assert(size > 0, "Invalid data size for send");
AZ_Assert(data != nullptr, "NULL data pointer passed to send");
AZ_Assert(address.GetAddress(ByteOrder::Host) != 0, "Invalid address");
AZ_Assert(address.GetPort(ByteOrder::Host) != 0, "Invalid address");
#ifdef ENABLE_LATENCY_DEBUG
if ((connectionQuality.m_latencyMs > AZ::TimeMs{ 0 }) || (connectionQuality.m_varianceMs > AZ::TimeMs{ 0 }))
{
const AZ::TimeMs jitterMs = aznumeric_cast<AZ::TimeMs>(m_random.GetRandom()) % (connectionQuality.m_varianceMs / aznumeric_cast<AZ::TimeMs>(2));
const AZ::TimeMs currTimeMs = AZ::GetElapsedTimeMs();
const AZ::TimeMs deferTimeMs = (connectionQuality.m_latencyMs / aznumeric_cast<AZ::TimeMs>(2)) + jitterMs;
m_sendBuffer.push_back(DeferredData(currTimeMs + deferTimeMs, address, data, size, encrypt, dtlsEndpoint));
std::sort(m_sendBuffer.begin(), m_sendBuffer.end());
}
#endif
if (!IsOpen())
{
return 0;
}
#ifdef ENABLE_LATENCY_DEBUG
if (connectionQuality.m_lossPercentage > 0)
{
if (int32_t(m_random.GetRandom() % 100) < (connectionQuality.m_lossPercentage / 2))
{
// Pretend we sent, but don't actually send
return true;
}
}
#endif
int32_t sentBytes = size;
#ifdef ENABLE_LATENCY_DEBUG
if (connectionQuality.m_latencyMs <= AZ::TimeMs{ 0 })
#endif
{
sentBytes = SendInternal(address, data, size, encrypt, dtlsEndpoint);
if (sentBytes < 0)
{
const int32_t error = GetLastNetworkError();
if (ErrorIsWouldBlock(error)) // Filter would block messages
{
return SocketOpResultSuccess;
}
AZLOG_ERROR("Failed to write to socket (%d:%s)", error, GetNetworkErrorDesc(error));
}
}
m_sentPackets++;
m_sentBytes += sentBytes;
return sentBytes;
}
int32_t UdpSocket::Receive(IpAddress& outAddress, uint8_t* outData, uint32_t size) const
{
AZ_Assert(size > 0, "Invalid data size for send");
AZ_Assert(outData != nullptr, "NULL data pointer passed to send");
if (!IsOpen())
{
return 0;
}
sockaddr_in from;
socklen_t fromLen = sizeof(from);
const int32_t receivedBytes = recvfrom(static_cast<int32_t>(m_socketFd), reinterpret_cast<char*>(outData), static_cast<int32_t>(size), 0, (sockaddr*)&from, &fromLen);
outAddress = IpAddress(ByteOrder::Network, from.sin_addr.s_addr, from.sin_port);
if (receivedBytes < 0)
{
const int32_t error = GetLastNetworkError();
if (ErrorIsWouldBlock(error)) // Filter would block messages
{
return 0;
}
bool ignoreForciblyClosedError = false;
if (ErrorIsForciblyClosed(error, ignoreForciblyClosedError))
{
if (ignoreForciblyClosedError)
{
return 0;
}
else
{
return SocketOpResultError;
}
}
AZLOG_ERROR("Failed to read from socket (%d:%s)", error, GetNetworkErrorDesc(error));
}
if (receivedBytes <= 0)
{
return 0;
}
m_recvPackets++;
m_recvBytes += receivedBytes;
return receivedBytes;
}
int32_t UdpSocket::SendInternal(const IpAddress& address, const uint8_t* data, uint32_t size,
[[maybe_unused]] bool dontEncypt, [[maybe_unused]] DtlsEndpoint& dtlsEndpoint) const
{
sockaddr_in destAddr;
memset(&destAddr, 0, sizeof(destAddr));
destAddr.sin_family = AF_INET;
destAddr.sin_addr.s_addr = address.GetAddress(ByteOrder::Network);
destAddr.sin_port = address.GetPort(ByteOrder::Network);
return sendto(static_cast<int32_t>(m_socketFd), reinterpret_cast<const char*>(data), size, 0, (sockaddr*)&destAddr, sizeof(destAddr));
}
}
@@ -0,0 +1,163 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzNetworking/Utilities/IpAddress.h>
#include <AzNetworking/Utilities/NetworkCommon.h>
#include <AzNetworking/ConnectionLayer/IConnection.h>
#include <AzNetworking/UdpTransport/DtlsEndpoint.h>
#include <AzCore/Math/Random.h>
#include <AzCore/std/containers/fixed_vector.h>
#ifndef _RELEASE
# define ENABLE_LATENCY_DEBUG 1
#endif
namespace AzNetworking
{
// Forwards
struct ConnectionQuality;
//! @class UdpSocket
//! @brief wrapper class for managing UDP sockets.
class UdpSocket
{
public:
enum class CanAcceptConnections
{
False, // Socket will not able to accept incoming connections, removes any requirement for RSA materials to open an SSL context (no private key file)
True // Socket can accept incoming connections and may require a valid certificate and private key file
};
UdpSocket() = default;
virtual ~UdpSocket();
//! Returns true if this is an encrypted socket, false if not.
//! @return boolean true if this is an encrypted socket, false if not
virtual bool IsEncrypted() const;
//! Creates an encryption socket wrapper.
//! @param dtlsEndpoint the encryption wrapper instance to create a connection over
//! @param address the IP address of the endpoint to connect to
//! @param outDtlsData data buffer to store the dtls handshake packet
//! @return a connect result specifying whether the connection is still pending, failed, or complete
virtual DtlsEndpoint::ConnectResult ConnectDtlsEndpoint(DtlsEndpoint& dtlsEndpoint, const IpAddress& address, UdpPacketEncodingBuffer& outDtlsData) const;
//! Accepts an encryption socket wrapper.
//! @param dtlsEndpoint the encryption wrapper instance to create a connection over
//! @param address the IP address of the endpoint to connect to
//! @param dtlsData data buffer containing the dtls handshake packet
//! @return a connect result specifying whether the connection is still pending, failed, or complete
virtual DtlsEndpoint::ConnectResult AcceptDtlsEndpoint(DtlsEndpoint& dtlsEndpoint, const IpAddress& address, const UdpPacketEncodingBuffer& dtlsData) const;
//! Opens the UDP socket on the given port.
//! @param port the port number to open the UDP socket on, 0 will bind to any available port
//! @param canAccept if true, the socket will be opened in a way that allows accepting incoming connections
//! @param trustZone for encrypted connections, the level of trust we associate with this connection (internal or external)
//! @return boolean true on success
virtual bool Open(uint16_t port, CanAcceptConnections canAccept, TrustZone trustZone);
//! Closes an open socket.
virtual void Close();
#ifdef ENABLE_LATENCY_DEBUG
//! Checks packets deferred by latency debug and sends any that have passed their latency threshold
virtual void ProcessDeferredPackets();
#endif
//! Returns true if the UDP socket is currently in an open state.
//! @return boolean true if the socket is in a connected state
bool IsOpen() const;
//! Sends a single payload over the UDP socket to the connected endpoint.
//! @param address the address to send the payload to
//! @param data pointer to the data to send
//! @param size size of the payload in bytes
//! @param encrypt signals that the payload should be encrypted before transmitting if encryption is supported
//! @param dtlsEndpoint data required for DTLS encryption
//! @param connectionQuality debug connection quality parameters
//! @return number of bytes sent, <= 0 on error
int32_t Send(const IpAddress& address, const uint8_t* data, uint32_t size, bool encrypt, DtlsEndpoint& dtlsEndpoint, const ConnectionQuality& connectionQuality) const;
//! Receives a payload from the UDP socket.
//! @param outAddress on success, the address of the endpoint that sent the data
//! @param outData on success, address to write the received data to
//! @param size maximum size the output buffer supports for receiving
//! @return number of bytes received, <= 0 on error
int32_t Receive(IpAddress& outAddress, uint8_t* outData, uint32_t size) const;
//! Returns the underlying socket file descriptor.
//! @return the underlying socket file descriptor
SocketFd GetSocketFd() const;
//! Returns the total number of packets sent on this socket.
//! @return the total number of packets sent on this socket
uint32_t GetSentPackets() const;
//! Returns the total number of bytes received on this socket.
//! @return the total number of bytes received on this socket
uint32_t GetSentBytes() const;
//! Returns the total number of packets sent on this socket.
//! @return the total number of packets sent on this socket
uint32_t GetRecvPackets() const;
//! Returns the total number of bytes received on this socket.
//! @return the total number of bytes received on this socket
uint32_t GetRecvBytes() const;
protected:
virtual int32_t SendInternal(const IpAddress& address, const uint8_t* data, uint32_t size, bool encrypt, DtlsEndpoint& dtlsEndpoint) const;
private:
SocketFd m_socketFd = InvalidSocketFd;
mutable uint32_t m_sentPackets = 0;
mutable uint32_t m_sentBytes = 0;
mutable uint32_t m_recvPackets = 0;
mutable uint32_t m_recvBytes = 0;
#ifdef ENABLE_LATENCY_DEBUG
struct DeferredData
{
DeferredData(AZ::TimeMs timeDeferredMs, const IpAddress& address, const uint8_t* data, uint32_t size, bool encrypt, DtlsEndpoint& dtlsEndpoint)
: m_timeDeferredMs(timeDeferredMs)
, m_address(address)
, m_encrypt(encrypt)
, m_dtlsEndpoint(&dtlsEndpoint)
{
m_dataBuffer.CopyValues(data, size);
}
bool operator <(const DeferredData& rhs) const
{
return m_timeDeferredMs < rhs.m_timeDeferredMs;
}
AZ::TimeMs m_timeDeferredMs;
IpAddress m_address;
bool m_encrypt;
DtlsEndpoint* m_dtlsEndpoint;
UdpPacketEncodingBuffer m_dataBuffer;
};
mutable AZStd::deque<DeferredData> m_sendBuffer;
mutable AZStd::deque<DeferredData> m_recvBuffer;
mutable AZ::SimpleLcgRandom m_random;
#endif
};
}
#include <AzNetworking/UdpTransport/UdpSocket.inl>
@@ -0,0 +1,46 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
namespace AzNetworking
{
inline bool UdpSocket::IsOpen() const
{
return (m_socketFd > SocketFd{ 0 });
}
inline SocketFd UdpSocket::GetSocketFd() const
{
return m_socketFd;
}
inline uint32_t UdpSocket::GetSentPackets() const
{
return m_sentPackets;
}
inline uint32_t UdpSocket::GetSentBytes() const
{
return m_sentBytes;
}
inline uint32_t UdpSocket::GetRecvPackets() const
{
return m_recvPackets;
}
inline uint32_t UdpSocket::GetRecvBytes() const
{
return m_recvBytes;
}
}