From cdc208dd4d9de7083c110887622faea6c79a8008 Mon Sep 17 00:00:00 2001 From: kaetemi Date: Thu, 23 Feb 2023 09:40:56 +0800 Subject: [PATCH] Implementing QUIC connection in client, ref ryzom/ryzomcore#628 --- nel/include/nel/net/inet_host.h | 4 + ryzom/client/src/CMakeLists.txt | 33 ++ ryzom/client/src/quic_connection.cpp | 437 +++++++++++++++++++++++++++ ryzom/client/src/quic_connection.h | 75 +++++ 4 files changed, 549 insertions(+) create mode 100644 ryzom/client/src/quic_connection.cpp create mode 100644 ryzom/client/src/quic_connection.h diff --git a/nel/include/nel/net/inet_host.h b/nel/include/nel/net/inet_host.h index b8e5a3b60b..266d4de81e 100644 --- a/nel/include/nel/net/inet_host.h +++ b/nel/include/nel/net/inet_host.h @@ -81,6 +81,10 @@ class CInetHost /// Returns the first address inline const CInetAddress &address() const { return m_Addresses[0]; } + /// Hostname + const std::string &hostname() const { return m_Hostname; } + + /// Port inline uint16 port() const { return m_Addresses[0].port(); } void setPort(uint16 port); diff --git a/ryzom/client/src/CMakeLists.txt b/ryzom/client/src/CMakeLists.txt index 061b304a22..cb8ad7bb82 100644 --- a/ryzom/client/src/CMakeLists.txt +++ b/ryzom/client/src/CMakeLists.txt @@ -66,6 +66,8 @@ IF(WITH_RYZOM_CLIENT) session_*.cpp session_*.h steam_client.cpp steam_client.h string_manager_client.cpp string_manager_client.h + *_connection.cpp *_connection.h + *download*.cpp *download*.h ) SOURCE_GROUP("network" FILES ${RZCLIENT_NETWORK}) FILE(GLOB RZCLIENT_GLOBALS @@ -73,6 +75,37 @@ IF(WITH_RYZOM_CLIENT) global.cpp global.h ) SOURCE_GROUP("globals" FILES ${RZCLIENT_GLOBALS}) + FILE(GLOB RZCLIENT_ANIMATION + animation.cpp animation.h + animation_*.cpp animation_*.h + ) + SOURCE_GROUP("animation" FILES ${RZCLIENT_ANIMATION}) + FILE(GLOB RZCLIENT_FX + fx*.cpp fx*.h + *_fx*.cpp *_fx*.h + projectile_*.cpp projectile_*.h + ) + SOURCE_GROUP("fx" FILES ${RZCLIENT_FX}) + FILE(GLOB RZCLIENT_WEATHER + sky.cpp sky.h + sky_*.cpp sky_*.h + weather.cpp weather.h + weather_*.cpp weather_*.h + precipitation.cpp precipitation.h + precipitation_*.cpp precipitation_*.h + light_cycle_*.cpp light_cycle_*.h + ) + SOURCE_GROUP("weather" FILES ${RZCLIENT_WEATHER}) + FILE(GLOB RZCLIENT_OUTPOSTS + outpost.cpp outpost.h + outpost_*.cpp outpost_*.h + ) + SOURCE_GROUP("outposts" FILES ${RZCLIENT_OUTPOSTS}) + FILE(GLOB RZCLIENT_DECALS + decal.cpp decal.h + decal*.cpp decal_*.h + ) + SOURCE_GROUP("decals" FILES ${RZCLIENT_DECALS}) # on Mac, create a .App Bundle if(APPLE) diff --git a/ryzom/client/src/quic_connection.cpp b/ryzom/client/src/quic_connection.cpp new file mode 100644 index 0000000000..8001cf2884 --- /dev/null +++ b/ryzom/client/src/quic_connection.cpp @@ -0,0 +1,437 @@ +// Ryzom - MMORPG Framework +// Copyright (C) 2023 Jan BOON (Kaetemi) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#include "stdpch.h" +#include "quic_connection.h" + +#include "nel/misc/mutex.h" +#include "nel/misc/buf_fifo.h" +#include "nel/misc/string_view.h" +#include "nel/misc/string_common.h" + +#include "config.h" + +#ifdef NL_MSQUIC_AVAILABLE +#include + +#ifdef NL_OS_WINDOWS +#pragma warning(push) +#pragma warning(disable : 6553) // Annotation does not apply to value type. +#include +#pragma warning(pop) +#endif + +#define MsQuic m->Api + +using namespace NLMISC; +using namespace NLNET; + +class CQuicConnectionImpl +{ +public: + CQuicConnectionImpl() + : Api(NULL) + , Registration(NULL) + , Configuration(NULL) + , State(CQuicConnection::Disconnected) + , ShutdownFlag(false) + , MaxSendLength(NULL) + { + } + + const QUIC_API_TABLE *Api; + HQUIC Registration; + HQUIC Configuration; + HQUIC Connection; + + CMutex BufferMutex; + NLMISC::CBufFIFO Buffer; + + CMutex StateMutex; + CQuicConnection::TState State; + + CMutex ShutdownMutex; // Don't have fancy atomics and synchronization primitives here, so mutex everything! + bool ShutdownFlag; + + uint32 MaxSendLength; + + static QUIC_STATUS +#ifdef _Function_class_ + _Function_class_(QUIC_CONNECTION_CALLBACK) +#endif + connectionCallback(HQUIC connection, void *context, QUIC_CONNECTION_EVENT *ev); +}; + +CQuicConnection::CQuicConnection() +#ifdef NL_CPP14 + : m(std::make_unique()) +#else + : m(new CQuicConnectionImpl()) +#endif +{ + // Open library + QUIC_STATUS status = MsQuicOpenVersion(QUIC_API_VERSION_2, (const void **)&MsQuic); + if (QUIC_FAILED(status)) + { + nlwarning("MsQuicOpenVersion failed with status 0x%x", status); + return; + } + + // Registration, this creates the worker threads + QUIC_REGISTRATION_CONFIG regConfig = { 0 }; + regConfig.AppName = "Ryzom Core (Client)"; + regConfig.ExecutionProfile = QUIC_EXECUTION_PROFILE_LOW_LATENCY; + status = MsQuic->RegistrationOpen(®Config, &m->Registration); + if (QUIC_FAILED(status)) + { + nlwarning("MsQuic->RegistrationOpen failed with status 0x%x", status); + release(); + return; + } +} + +CQuicConnection::~CQuicConnection() +{ + disconnect(); + release(); +} + +void CQuicConnection::connect(const NLNET::CInetHost &addr) +{ + disconnect(); + + if (!MsQuic) + { + nlwarning("QUIC API not available"); + } + + { + CAutoMutex lock(m->StateMutex); + m->State = Connecting; + } + + { + CAutoMutex lock(m->ShutdownMutex); + m->ShutdownFlag = false; + } + + static const char *protocolName = "ryzomcore4"; + static const QUIC_BUFFER alpn = { sizeof(protocolName) - 1, (uint8_t *)protocolName }; + + // Configuration, initialized in start, but destroyed on release only (may attempt more than once) + QUIC_STATUS status = QUIC_STATUS_SUCCESS; + if (!m->Configuration) + { + QUIC_SETTINGS settings = { 0 }; + settings.DatagramReceiveEnabled = TRUE; + settings.IsSet.DatagramReceiveEnabled = TRUE; + settings.MigrationEnabled = TRUE; + settings.IsSet.MigrationEnabled = TRUE; + settings.PeerBidiStreamCount = 0; + settings.IsSet.PeerBidiStreamCount = TRUE; + settings.PeerUnidiStreamCount = 0; // TODO: Configured from msg.xml + settings.IsSet.PeerUnidiStreamCount = TRUE; + // settings.SendBufferingEnabled = TRUE; + // settings.IsSet.SendBufferingEnabled = TRUE; + // settings.GreaseQuicBitEnabled = TRUE; + // settings.IsSet.GreaseQuicBitEnabled = TRUE; + // settings.MinimumMtu = m_MsgSize + size of QUIC header; // Probably violates QUIC protocol if we do this, also no need + // settings.IsSet.MinimumMtu = TRUE; + status = MsQuic->ConfigurationOpen(m->Registration, &alpn, 1, &settings, sizeof(settings), NULL, &m->Configuration); + if (QUIC_FAILED(status)) + { + disconnect(); + nlwarning("MsQuic->ConfigurationOpen failed with status 0x%x", status); + return; + } + + // Load credentials for client, client doesn't need a certificate + QUIC_CREDENTIAL_CONFIG credConfig; + memset(&credConfig, 0, sizeof(credConfig)); + credConfig.Flags = QUIC_CREDENTIAL_FLAG_CLIENT; + credConfig.Type = QUIC_CREDENTIAL_TYPE_NONE; + status = MsQuic->ConfigurationLoadCredential(m->Configuration, &credConfig); + if (QUIC_FAILED(status)) + { + disconnect(); + MsQuic->ConfigurationClose(m->Configuration); + m->Configuration = nullptr; + nlwarning("MsQuic->ConfigurationLoadCredential failed with status 0x%x", status); + return; + } + } + + // Connect to the specified address + status = MsQuic->ConnectionOpen(m->Registration, CQuicConnectionImpl::connectionCallback, (void *)this, &m->Connection); + if (QUIC_FAILED(status)) + { + disconnect(); + nlwarning("MsQuic->ConnectionOpen failed with status 0x%x", status); + return; + } + + // Start the connection + status = MsQuic->ConnectionStart(m->Connection, m->Configuration, QUIC_ADDRESS_FAMILY_UNSPEC, nlUtf8ToMbcs(addr.hostname()), addr.port()); + if (QUIC_FAILED(status)) + { + disconnect(); + nlwarning("MsQuic->ConnectionStart to %s failed with status 0x%x", addr.toStringLong().c_str(), status); + return; + } +} + +void CQuicConnection::disconnect() +{ + // Stop connection + if (m->Connection) + { + MsQuic->ConnectionShutdown(m->Connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0); + try + { + for (;;) // Spin wait because we don't have fancy mechanisms when supporting legacy code base + { + CAutoMutex lock(m->ShutdownMutex); + if (m->ShutdownFlag) + break; + nlSleep(1); + } + } + catch (const std::exception &e) + { + nlwarning("Exception while waiting for connection shutdown: %s", e.what()); + } + + MsQuic->ConnectionClose(m->Connection); + m->Connection = NULL; + } + + { + CAutoMutex lock(m->StateMutex); + m->State = Disconnected; + } +} + +void CQuicConnection::release() +{ + // Close configuration + if (m->Configuration) + { + MsQuic->ConfigurationClose(m->Configuration); + m->Configuration = NULL; + } + + // Close registration + if (m->Registration) + { + MsQuic->RegistrationClose(m->Registration); + m->Registration = NULL; + } + + // Close library + if (MsQuic) + { + MsQuicClose(MsQuic); + MsQuic = NULL; + } +} + +CQuicConnection::TState CQuicConnection::state() const +{ + CAutoMutex lock(m->StateMutex); + return m->State; +} + +QUIC_STATUS +#ifdef _Function_class_ +_Function_class_(QUIC_CONNECTION_CALLBACK) +#endif + CQuicConnectionImpl::connectionCallback(HQUIC connection, void *context, QUIC_CONNECTION_EVENT *ev) +{ + CQuicConnection *self = (CQuicConnection *)context; + CQuicConnectionImpl *m = self->m.get(); + QUIC_STATUS status = QUIC_STATUS_NOT_SUPPORTED; + switch (ev->Type) + { + case QUIC_CONNECTION_EVENT_CONNECTED: { + nlinfo("Connected"); + nlassert(CStringView((const char *)ev->CONNECTED.NegotiatedAlpn, ev->CONNECTED.NegotiatedAlpnLength) == "ryzomcore4"); + // MsQuic->ConnectionSendResumptionTicket(connection, QUIC_SEND_RESUMPTION_FLAG_NONE, 0, NULL); // What does this even do? + { + CAutoMutex lock(m->StateMutex); + m->State = CQuicConnection::Connected; + } + status = QUIC_STATUS_SUCCESS; + break; + } + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT: { + nlinfo("Shutdown initiated by transport"); + { + CAutoMutex lock(m->StateMutex); + m->State = CQuicConnection::Disconnected; + } + status = QUIC_STATUS_SUCCESS; + break; + } + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: { + nlinfo("Shutdown initiated by peer"); + { + CAutoMutex lock(m->StateMutex); + m->State = CQuicConnection::Disconnected; + } + status = QUIC_STATUS_SUCCESS; + break; + } + case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: { + nlinfo("Shutdown complete"); + { + CAutoMutex lock(m->StateMutex); + m->State = CQuicConnection::Disconnected; + } + { + CAutoMutex lock(m->ShutdownMutex); + m->ShutdownFlag = true; + } + status = QUIC_STATUS_SUCCESS; + break; + } + case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: + nlinfo("Datagram received"); + // YES PLEASE + self->datagramReceived(ev->DATAGRAM_RECEIVED.Buffer->Buffer, ev->DATAGRAM_RECEIVED.Buffer->Length); + status = QUIC_STATUS_SUCCESS; + break; + case QUIC_CONNECTION_EVENT_DATAGRAM_STATE_CHANGED: + nlinfo("Datagram state changed"); + m->MaxSendLength = ev->DATAGRAM_STATE_CHANGED.SendEnabled ? ev->DATAGRAM_STATE_CHANGED.MaxSendLength : 0; + status = QUIC_STATUS_SUCCESS; + break; + case QUIC_CONNECTION_EVENT_LOCAL_ADDRESS_CHANGED: + case QUIC_CONNECTION_EVENT_PEER_ADDRESS_CHANGED: + case QUIC_CONNECTION_EVENT_IDEAL_PROCESSOR_CHANGED: + case QUIC_CONNECTION_EVENT_DATAGRAM_SEND_STATE_CHANGED: + case QUIC_CONNECTION_EVENT_RESUMED: + case QUIC_CONNECTION_EVENT_PEER_CERTIFICATE_RECEIVED: + case QUIC_CONNECTION_EVENT_STREAMS_AVAILABLE: // TODO: Match with msg.xml + // Don't care + status = QUIC_STATUS_SUCCESS; + break; + case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: + case QUIC_CONNECTION_EVENT_PEER_NEEDS_STREAMS: + case QUIC_CONNECTION_EVENT_RESUMPTION_TICKET_RECEIVED: + // Not supported + break; + } + return status; +} + +void CQuicConnection::sendDatagram(const uint8 *buffer, uint32 size) +{ + if (m->Connection) + { + QUIC_BUFFER buf; + buf.Buffer = (uint8 *)buffer; + buf.Length = size; + MsQuic->DatagramSend(m->Connection, &buf, 1, QUIC_SEND_FLAG_NONE, this); + } +} + +bool CQuicConnection::datagramAvailable() +{ + CAutoMutex lock(m->BufferMutex); + return !m->Buffer.empty(); +} + +bool CQuicConnection::receiveDatagram(NLMISC::CBitMemStream &msgin) +{ + CAutoMutex lock(m->BufferMutex); + if (!m->Buffer.empty()) + return false; + uint8 *buffer; + uint32 size; + m->Buffer.front(buffer, size); + msgin.clear(); + memcpy(msgin.bufferToFill(size), buffer, size); + return true; +} + +void CQuicConnection::datagramReceived(const uint8 *buffer, uint32 length) +{ + CAutoMutex lock(m->BufferMutex); + m->Buffer.push(buffer, length); +} + +#else + +using namespace NLMISC; +using namespace NLNET; + +class CQuicConnectionImpl +{ +public: + int Dummy; +}; + +CQuicConnection::CQuicConnection() +#ifdef NL_CPP14 + : m(std::make_unique()) +#else + : m(new CQuicConnectionImpl()) +#endif +{ +} + +CQuicConnection::~CQuicConnection() +{ +} + +void CQuicConnection::connect(const NLNET::CInetHost &addr) +{ +} + +void CQuicConnection::disconnect() +{ +} + +void CQuicConnection::release() +{ +} + +CQuicConnection::TState CQuicConnection::state() const +{ + return Disconnected; +} + +void CQuicConnection::sendDatagram(const uint8 *buffer, uint32 size) +{ +} + +bool CQuicConnection::datagramAvailable() +{ + return false; +} + +bool CQuicConnection::receiveDatagram(NLMISC::CBitMemStream &msgin) +{ + return false; +} + +void CQuicConnection::datagramReceived(const uint8 *buffer, uint32 length) +{ +} + +#endif + +/* end of file */ diff --git a/ryzom/client/src/quic_connection.h b/ryzom/client/src/quic_connection.h new file mode 100644 index 0000000000..3b217f083d --- /dev/null +++ b/ryzom/client/src/quic_connection.h @@ -0,0 +1,75 @@ +// Ryzom - MMORPG Framework +// Copyright (C) 2023 Jan BOON (Kaetemi) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#ifndef CL_QUIC_CONNECTION_H +#define CL_QUIC_CONNECTION_H + +#include "nel/misc/types_nl.h" + +#include + +#include "nel/misc/bit_mem_stream.h" +#include "nel/net/inet_host.h" + +class CQuicConnectionImpl; + +class CQuicConnection +{ +public: + enum TState + { + Disconnected, + Connecting, + Connected + }; + + CQuicConnection(); + ~CQuicConnection(); + + /// Connect + void connect(const NLNET::CInetHost &addr); // const CInetAddress &addr); + + /// Shutdown and close gracefully, this object can be reused immediately for a new connection + void disconnect(); + + /// Release. Instance is useless after this call + void release(); + + /// Check if still connecting or connected + TState state() const; + + /// Send a datagram, fancier than a telegram, but not as reliable + void sendDatagram(const uint8 *buffer, uint32 size); + + /// Check if any datagram has been received + bool datagramAvailable(); + + /// Receive a datagram + bool receiveDatagram(NLMISC::CBitMemStream &msgin); + +private: + friend CQuicConnectionImpl; + + /// Internal implementation specific + CUniquePtr m; + + /// Received datagram + void datagramReceived(const uint8 *buffer, uint32 length); +}; + +#endif /* NL_QUIC_TRANSCEIVER_H */ + +/* end of file */