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; +}