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.
This commit is contained in:
Kawe Mazidjatari 2024-03-30 02:53:12 +01:00
parent 1c32339305
commit e02f8f36fe
6 changed files with 516 additions and 40 deletions

View File

@ -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);

View File

@ -61,14 +61,6 @@ public:
// Copy the array.
CUtlVector<T, A>& operator=(const CUtlVector<T, A>& other);
// NOTE<R5SDK>:
// 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<T, A>& CUtlVector<T, A>::operator=(const CUtlVector<T, A>& oth
return *this;
}
template< typename T, class A >
void CUtlVector<T, A>::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<T, A>::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 T> class CUtlVectorAutoPurge : public CUtlVector< T, CUtlMemory< T, int> >
template<class T> 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<char*>
{
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
};

View File

@ -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<ConnContext_s> m_addressList;
};
#endif // TIER2_WEBSOCKETCREATOR_H

View File

@ -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
//-----------------------------------------------------------------------------

View File

@ -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/"
)

317
r5dev/tier2/websocket.cpp Normal file
View File

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