From ab7e02ef363239c352e884622109a5091460f863 Mon Sep 17 00:00:00 2001 From: Kawe Mazidjatari <48657826+Mauler125@users.noreply.github.com> Date: Sat, 30 Mar 2024 02:53:12 +0100 Subject: [PATCH] Tier2: create modular WebSocket system This code was actually part of the LiveAPI system, but there were many opportunities to make this particular code modular, so it has been decoupled and moved to Tier2. The LiveAPI system will soon use this class instead. The implementation has also been improved by adding dedicated routines for updating socket parameters, disconnecting/reconnecting and destroying sockets. This commit also removes legacy workaround code in UtlVector which was used for before we had early enough access to the game's memalloc singleton. This code was no longer used. This commit also implements the CUtlStringList class, which is now used for the new websocket class to split each socket connection up by a comma delimiter. --- src/public/tier1/strtools.h | 4 + src/public/tier1/utlvector.h | 58 +++---- src/public/tier2/websocket.h | 101 +++++++++++ src/tier1/strtools.cpp | 65 +++++++ src/tier2/CMakeLists.txt | 11 +- src/tier2/websocket.cpp | 317 +++++++++++++++++++++++++++++++++++ 6 files changed, 516 insertions(+), 40 deletions(-) create mode 100644 src/public/tier2/websocket.h create mode 100644 src/tier2/websocket.cpp diff --git a/src/public/tier1/strtools.h b/src/public/tier1/strtools.h index ba644b57..20508762 100644 --- a/src/public/tier1/strtools.h +++ b/src/public/tier1/strtools.h @@ -90,6 +90,10 @@ ssize_t V_vsnprintfRet(char* pDest, size_t maxLen, const char* pFormat, va_list // Strip white space at the beginning and end of a string ssize_t V_StrTrim(char* pStr); +class CUtlStringList; +void V_SplitString2(const char* pString, const char** pSeparators, ssize_t nSeparators, CUtlStringList& outStrings); +void V_SplitString(const char* pString, const char* pSeparator, CUtlStringList& outStrings); + int V_UTF8ToUnicode(const char* pUTF8, wchar_t* pwchDest, int cubDestSizeInBytes); int V_UnicodeToUTF8(const wchar_t* pUnicode, char* pUTF8, int cubDestSizeInBytes); diff --git a/src/public/tier1/utlvector.h b/src/public/tier1/utlvector.h index fb1642e1..0caf449b 100644 --- a/src/public/tier1/utlvector.h +++ b/src/public/tier1/utlvector.h @@ -61,14 +61,6 @@ public: // Copy the array. CUtlVector& operator=(const CUtlVector& other); - // NOTE: - // Do not call after initialization or after adding elements. - // This is added so it could be constructed nicely. Since the - // game executable in monolithic, we couldn't import the malloc - // functions, and thus not construct automatically when using - // the game's memalloc singleton. - void Init(); - // element access T& operator[](int i); const T& operator[](int i) const; @@ -664,15 +656,6 @@ inline CUtlVector& CUtlVector::operator=(const CUtlVector& oth return *this; } -template< typename T, class A > -void CUtlVector::Init() -{ - m_Memory.m_pMemory = nullptr; - m_Memory.m_nAllocationCount = 0; - m_Memory.m_nGrowSize = 0; - m_Size = 0; -} - //----------------------------------------------------------------------------- // element access //----------------------------------------------------------------------------- @@ -1417,24 +1400,23 @@ void CUtlVector::Validate(CValidator& validator, char* pchName) // A vector class for storing pointers, so that the elements pointed to by the pointers are deleted // on exit. -template class CUtlVectorAutoPurge : public CUtlVector< T, CUtlMemory< T, int> > +template class CUtlVectorAutoPurge : public CUtlVector< T > { public: ~CUtlVectorAutoPurge(void) { this->PurgeAndDeleteElements(); } - }; // easy string list class with dynamically allocated strings. For use with V_SplitString, etc. // Frees the dynamic strings in destructor. -class CUtlStringList : public CUtlVectorAutoPurge< char*> +class CUtlStringList : public CUtlVectorAutoPurge { public: void CopyAndAddToTail(char const* pString) // clone the string and add to the end { - char* pNewStr = new char[1 + strlen(pString)]; + char* const pNewStr = new char[strlen(pString) + 1]; strcpy(pNewStr, pString); AddToTail(pNewStr); } @@ -1446,27 +1428,25 @@ public: CUtlStringList() {} - // !TODO: + CUtlStringList(char const* pString, char const* pSeparator) + { + SplitString(pString, pSeparator); + } - //CUtlStringList(char const* pString, char const* pSeparator) - //{ - // SplitString(pString, pSeparator); - //} + CUtlStringList(char const* pString, const char** pSeparators, ssize_t nSeparators) + { + SplitString2(pString, pSeparators, nSeparators); + } - //CUtlStringList(char const* pString, const char** pSeparators, int nSeparators) - //{ - // SplitString2(pString, pSeparators, nSeparators); - //} + void SplitString(char const* pString, char const* pSeparator) + { + V_SplitString(pString, pSeparator, *this); + } - //void SplitString(char const* pString, char const* pSeparator) - //{ - // V_SplitString(pString, pSeparator, *this); - //} - - //void SplitString2(char const* pString, const char** pSeparators, int nSeparators) - //{ - // V_SplitString2(pString, pSeparators, nSeparators, *this); - //} + void SplitString2(char const* pString, const char** pSeparators, ssize_t nSeparators) + { + V_SplitString2(pString, pSeparators, nSeparators, *this); + } private: CUtlStringList(const CUtlStringList& other); // copying directly will cause double-release of the same strings; maybe we need to do a deep copy, but unless and until such need arises, this will guard against double-release }; diff --git a/src/public/tier2/websocket.h b/src/public/tier2/websocket.h new file mode 100644 index 00000000..0cec4a0c --- /dev/null +++ b/src/public/tier2/websocket.h @@ -0,0 +1,101 @@ +#ifndef TIER2_WEBSOCKETCREATOR_H +#define TIER2_WEBSOCKETCREATOR_H + +#define WEBSOCKET_DEFAULT_BUFFER_SIZE 1024 + +struct ProtoWebSocketRefT; + +class CWebSocket +{ +public: + enum ConnState_e + { + CS_CREATE = 0, + + CS_CONNECTED, + CS_LISTENING, + + CS_DESTROYED, + + CS_RETRY, + CS_UNAVAIL + }; + + struct ConnParams_s + { + ConnParams_s() + { + bufSize = WEBSOCKET_DEFAULT_BUFFER_SIZE; + retryTime = 0.0f; + maxRetries = 0; + + timeOut = -1; + keepAlive = -1; + laxSSL = 0; + } + + int32_t bufSize; + float retryTime; + int32_t maxRetries; + + int32_t timeOut; + int32_t keepAlive; + int32_t laxSSL; + }; + + struct ConnContext_s + { + ConnContext_s(const char* const addr) + { + webSocket = nullptr; + address = addr; + + state = CS_CREATE; + + tryCount = 0; + lastQueryTime = 0; + } + + bool Connect(const double queryTime, const ConnParams_s& params); + bool Status(const double queryTime); + + void SetParams(const ConnParams_s& params); + + void Disconnect(); + void Reconnect(); + void Destroy(); + + ProtoWebSocketRefT* webSocket; + ConnState_e state; + + int tryCount; + double lastQueryTime; + + CUtlString address; + }; + + CWebSocket(); + + bool Init(const char* const addressList, const ConnParams_s& params, const char*& initError); + void Shutdown(); + + bool SetupFromList(const char* const addressList); + void UpdateParams(const ConnParams_s& params); + + void Update(); + void DeleteUnavailable(); + + void DestroyAll(); + void ReconnectAll(); + void ClearAll(); + + void SendData(const char* const dataBuf, const int32_t dataSize); + bool IsInitialized() const; + +private: + bool m_initialized; + ConnParams_s m_connParams; + CUtlVector m_addressList; +}; + +#endif // TIER2_WEBSOCKETCREATOR_H diff --git a/src/tier1/strtools.cpp b/src/tier1/strtools.cpp index 98bfebe8..b7958849 100644 --- a/src/tier1/strtools.cpp +++ b/src/tier1/strtools.cpp @@ -18,6 +18,20 @@ static int FastToLower(char c) return i; } +//----------------------------------------------------------------------------- +// Allocate a string buffer +//----------------------------------------------------------------------------- +char* AllocString(const char* pStr, ssize_t nMaxChars) +{ + const ssize_t allocLen = (nMaxChars == -1) + ? strlen(pStr) + 1 + : Min((ssize_t)strlen(pStr), nMaxChars) + 1; + + char* const pOut = new char[allocLen]; + V_strncpy(pOut, pStr, allocLen); + + return pOut; +} //----------------------------------------------------------------------------- // A special high-performance case-insensitive compare function @@ -404,6 +418,57 @@ ssize_t V_StrTrim(char* pStr) return pDest - pStart; } +void V_SplitString2(const char* pString, const char** pSeparators, ssize_t nSeparators, CUtlStringList& outStrings) +{ + outStrings.Purge(); + const char* pCurPos = pString; + + while (true) + { + ssize_t iFirstSeparator = -1; + const char* pFirstSeparator = nullptr; + + for (ssize_t i = 0; i < nSeparators; i++) + { + const char* const pTest = V_stristr(pCurPos, pSeparators[i]); + + if (pTest && (!pFirstSeparator || pTest < pFirstSeparator)) + { + iFirstSeparator = i; + pFirstSeparator = pTest; + } + } + + if (pFirstSeparator) + { + // Split on this separator and continue on. + const ssize_t separatorLen = strlen(pSeparators[iFirstSeparator]); + + if (pFirstSeparator > pCurPos) + { + outStrings.AddToTail(AllocString(pCurPos, pFirstSeparator - pCurPos)); + } + + pCurPos = pFirstSeparator + separatorLen; + } + else + { + // Copy the rest of the string + if (strlen(pCurPos)) + { + outStrings.AddToTail(AllocString(pCurPos, -1)); + } + + return; + } + } +} + +void V_SplitString(const char* pString, const char* pSeparator, CUtlStringList& outStrings) +{ + V_SplitString2(pString, &pSeparator, 1, outStrings); +} + //----------------------------------------------------------------------------- // Purpose: Converts a UTF-8 string into a unicode string //----------------------------------------------------------------------------- diff --git a/src/tier2/CMakeLists.txt b/src/tier2/CMakeLists.txt index bad7615e..3cff30a8 100644 --- a/src/tier2/CMakeLists.txt +++ b/src/tier2/CMakeLists.txt @@ -9,6 +9,7 @@ add_sources( SOURCE_GROUP "Utility" "meshutils.cpp" "renderutils.cpp" "socketcreator.cpp" + "websocket.cpp" ) file( GLOB TIER2_PUBLIC_HEADERS @@ -20,4 +21,12 @@ add_sources( SOURCE_GROUP "Public" end_sources() -target_include_directories( ${PROJECT_NAME} PRIVATE "${ENGINE_SOURCE_DIR}/tier0/" "${ENGINE_SOURCE_DIR}/tier1/" ) +target_include_directories( ${PROJECT_NAME} PRIVATE + "${ENGINE_SOURCE_DIR}/tier0/" + "${ENGINE_SOURCE_DIR}/tier1/" +) + +target_include_directories( ${PROJECT_NAME} PRIVATE + "${THIRDPARTY_SOURCE_DIR}/dirtysdk/include/" + "${THIRDPARTY_SOURCE_DIR}/ea/" +) diff --git a/src/tier2/websocket.cpp b/src/tier2/websocket.cpp new file mode 100644 index 00000000..405b9882 --- /dev/null +++ b/src/tier2/websocket.cpp @@ -0,0 +1,317 @@ +//===========================================================================// +// +// Purpose: WebSocket implementation +// +//===========================================================================// +#include "tier2/websocket.h" + +#include "DirtySDK/dirtysock.h" +#include "DirtySDK/dirtysock/netconn.h" +#include "DirtySDK/proto/protossl.h" +#include "DirtySDK/proto/protowebsocket.h" + + +//----------------------------------------------------------------------------- +// constructors/destructors +//----------------------------------------------------------------------------- +CWebSocket::CWebSocket() +{ + m_initialized = false; +} + +//----------------------------------------------------------------------------- +// Purpose: initialization of the socket system +//----------------------------------------------------------------------------- +bool CWebSocket::Init(const char* const addressList, const ConnParams_s& params, const char*& initError) +{ + if (!NetConnStatus('open', 0, NULL, 0)) + { + initError = "Network connection module not initialized"; + return false; + } + + if (!SetupFromList(addressList)) + { + initError = (*addressList) + ? "Failed to parse address list" + : "Address list is empty"; + + return false; + } + + m_connParams = params; + m_initialized = true; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: shutdown of the socket system +//----------------------------------------------------------------------------- +void CWebSocket::Shutdown() +{ + m_initialized = false; + ClearAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: setup connection list, returns false if connection list is empty +//----------------------------------------------------------------------------- +bool CWebSocket::SetupFromList(const char* const addressList) +{ + const CUtlStringList addresses(addressList, ","); + + FOR_EACH_VEC(addresses, i) + { + const ConnContext_s conn(addresses[i]); + m_addressList.AddToTail(conn); + } + + return addresses.Count() != 0; +} + +//----------------------------------------------------------------------------- +// Purpose: update parameters for each connection +//----------------------------------------------------------------------------- +void CWebSocket::UpdateParams(const ConnParams_s& params) +{ + m_connParams = params; + + for (ConnContext_s& conn : m_addressList) + { + if (conn.webSocket) + conn.SetParams(params); + } +} + +//----------------------------------------------------------------------------- +// Purpose: socket state machine +//----------------------------------------------------------------------------- +void CWebSocket::Update() +{ + if (!IsInitialized()) + return; + + const double queryTime = Plat_FloatTime(); + + for (ConnContext_s& conn : m_addressList) + { + if (conn.webSocket) + ProtoWebSocketUpdate(conn.webSocket); + + if (conn.state == CS_CREATE || conn.state == CS_RETRY) + { + conn.Connect(queryTime, m_connParams); + continue; + } + + if (conn.state == CS_CONNECTED || conn.state == CS_LISTENING) + { + conn.Status(queryTime); + continue; + } + + if (conn.state == CS_DESTROYED) + { + if (conn.tryCount > m_connParams.maxRetries) + { + // All retry attempts have been used; mark unavailable for deletion + conn.state = CS_UNAVAIL; + } + else + { + // Mark as retry, this will recreate the socket and reattempt + // the connection + conn.state = CS_RETRY; + } + } + } + + DeleteUnavailable(); +} + +//----------------------------------------------------------------------------- +// Purpose: delete all connections marked unavailable +//----------------------------------------------------------------------------- +void CWebSocket::DeleteUnavailable() +{ + FOR_EACH_VEC_BACK(m_addressList, i) + { + if (m_addressList[i].state == CS_UNAVAIL) + { + m_addressList.FastRemove(i); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: destroy all connections +//----------------------------------------------------------------------------- +void CWebSocket::DestroyAll() +{ + for (ConnContext_s& conn : m_addressList) + { + conn.Destroy(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: reconnect all connections +//----------------------------------------------------------------------------- +void CWebSocket::ReconnectAll() +{ + for (ConnContext_s& conn : m_addressList) + { + conn.Reconnect(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: destroy and purge all connections +//----------------------------------------------------------------------------- +void CWebSocket::ClearAll() +{ + DestroyAll(); + m_addressList.Purge(); +} + +//----------------------------------------------------------------------------- +// Purpose: send data to all sockets +//----------------------------------------------------------------------------- +void CWebSocket::SendData(const char* const dataBuf, const int32_t dataSize) +{ + if (!IsInitialized()) + return; + + for (ConnContext_s& conn : m_addressList) + { + if (conn.state != CS_LISTENING) + continue; + + if (ProtoWebSocketSend(conn.webSocket, dataBuf, dataSize) < 0) + { + conn.Destroy(); // Reattempt the connection for this socket + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: returns whether the socket system is enabled and able to run +//----------------------------------------------------------------------------- +bool CWebSocket::IsInitialized() const +{ + return m_initialized; +} + +//----------------------------------------------------------------------------- +// Purpose: connect to a socket +//----------------------------------------------------------------------------- +bool CWebSocket::ConnContext_s::Connect(const double queryTime, const ConnParams_s& params) +{ + const double retryTimeTotal = lastQueryTime + params.retryTime; + const double currTime = Plat_FloatTime(); + + if (retryTimeTotal > currTime) + return false; // Still within retry period + + tryCount++; + webSocket = ProtoWebSocketCreate(params.bufSize); + + if (!webSocket) + { + state = CS_UNAVAIL; + return false; + } + + SetParams(params); + + if (ProtoWebSocketConnect(webSocket, address.String()) != NULL) + { + // Failure + Destroy(); + return false; + } + + state = CS_CONNECTED; + lastQueryTime = queryTime; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: check the connection status and destroy if not connected (-1) +//----------------------------------------------------------------------------- +bool CWebSocket::ConnContext_s::Status(const double queryTime) +{ + const int32_t status = ProtoWebSocketStatus(webSocket, 'stat', NULL, 0); + + if (status == -1) + { + Destroy(); + lastQueryTime = queryTime; + + return false; + } + else if (!status) + { + lastQueryTime = queryTime; + return false; + } + + tryCount = 0; + state = CS_LISTENING; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: set parameters for this socket +//----------------------------------------------------------------------------- +void CWebSocket::ConnContext_s::SetParams(const ConnParams_s& params) +{ + Assert(webSocket); + + if (params.timeOut > 0) + { + ProtoWebSocketControl(webSocket, 'time', params.timeOut, 0, NULL); + } + + if (params.keepAlive > 0) + { + ProtoWebSocketControl(webSocket, 'keep', params.keepAlive, 0, NULL); + } + + ProtoWebSocketControl(webSocket, 'ncrt', params.laxSSL, 0, NULL); + ProtoWebSocketUpdate(webSocket); +} + +//----------------------------------------------------------------------------- +// Purpose: disconnect and mark socket as unavailable for removal +//----------------------------------------------------------------------------- +void CWebSocket::ConnContext_s::Disconnect() +{ + ProtoWebSocketDisconnect(webSocket); + ProtoWebSocketUpdate(webSocket); + ProtoWebSocketDestroy(webSocket); + + webSocket = nullptr; + state = CS_UNAVAIL; +} + +//----------------------------------------------------------------------------- +// Purpose: reconnect without burning retry attempts +//----------------------------------------------------------------------------- +void CWebSocket::ConnContext_s::Reconnect() +{ + Disconnect(); + state = CS_CREATE; +} + +//----------------------------------------------------------------------------- +// Purpose: reconnect while burning retry attempts +//----------------------------------------------------------------------------- +void CWebSocket::ConnContext_s::Destroy() +{ + Disconnect(); + state = CS_DESTROYED; +}