// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Containers/Ticker.h" #include "Containers/Queue.h" #include "Misc/TransactionallySafeCriticalSection.h" #if WITH_WEBSOCKETS && WITH_LIBWEBSOCKETS #include "IWebSocket.h" #if PLATFORM_WINDOWS # include "Windows/AllowWindowsPlatformTypes.h" #endif THIRD_PARTY_INCLUDES_START #include "libwebsockets.h" THIRD_PARTY_INCLUDES_END #if PLATFORM_WINDOWS # include "Windows/HideWindowsPlatformTypes.h" #endif /** Buffer for one outgoing packet */ struct FLwsSendBuffer { /** * Constructor * @param Data pointer to data to fill our buffer with * @param Size size of Data * @param bInIsBinary Whether or not this should be treated as a binary packet */ FLwsSendBuffer(const uint8* Data, const SIZE_T Size, const bool bInIsBinary); /** * Get the actual payload size * Payload includes additional room for libwebsockets to use * @return payload size */ int32 GetPayloadSize() const; /** * Whether we have written our entire payload yet or not * @return whether we have written our entire payload yet or not */ bool IsDone() const { return !HasError() && BytesWritten >= GetPayloadSize(); } /** * Whether we have encountered an error or not * @return whether we have encountered an error or not */ bool HasError() const { return bHasError; } /** Whether or not the packet is a binary packet, if not it is treated as a string */ const bool bIsBinary; /** Number of bytes from Payload already written */ int32 BytesWritten; /** Payload of the packet */ TArray Payload; /** Has an error occurred while writing? */ bool bHasError; }; /** Buffer for one incoming binary packet */ struct FLwsReceiveBufferBinary { /** * Constructor * @param Data pointer to data to fill our buffer with * @param Size size of Data * @param InBytesRemaining Number of bytes remaining in the packet */ FLwsReceiveBufferBinary(const uint8* Data, const int32 Size, const int32 InBytesRemaining); /** Payload received */ TArray Payload; /** Number of bytes remaining in the packet */ const int32 BytesRemaining; }; typedef TUniquePtr FLwsReceiveBufferBinaryPtr; /** Buffer for one incoming binary packet fragment */ struct FLwsReceiveBufferBinaryFragment { /** * Constructor * @param Data pointer to data to fill our buffer with * @param Size size of Data * @param bInIsLastFragment Whether or not this is the last fragment */ FLwsReceiveBufferBinaryFragment(const uint8* Data, const int32 Size, const bool bInIsLastFragment); /** Payload received */ TArray Payload; /** Whether or not this is the last fragment */ const bool bIsLastFragment; }; typedef TUniquePtr FLwsReceiveBufferBinaryFragmentPtr; /** Buffer for one incoming text packet, fully received */ struct FLwsReceiveBufferText { /** * Constructor * @param InText The packet contents */ FLwsReceiveBufferText(FString&& InText); /** Text packet received */ const FString Text; }; typedef TUniquePtr FLwsReceiveBufferTextPtr; class FLwsWebSocketsManager; class FLwsWebSocket : public IWebSocket , public TSharedFromThis { public: /** Destructor */ virtual ~FLwsWebSocket(); // IWebSocket virtual void Connect() override; virtual void Close(const int32 Code = 1000, const FString& Reason = FString()) override; virtual bool IsConnected() override { return LastGameThreadState == EState::Connected || LastGameThreadState == EState::ClosingByRequest; } virtual void Send(const FString& Data); virtual void Send(const void* Data, SIZE_T Size, bool bIsBinary) override; virtual void SetTextMessageMemoryLimit(uint64 TextMessageMemoryLimit) override; /** Delegate called when a web socket connection has been established */ DECLARE_DERIVED_EVENT(FLwsWebSocket, IWebSocket::FWebSocketConnectedEvent, FWebSocketConnectedEvent); virtual FWebSocketConnectedEvent& OnConnected() override { return ConnectedEvent; } /** Delegate called when a web socket connection has errored */ DECLARE_DERIVED_EVENT(FLwsWebSocket, IWebSocket::FWebSocketConnectionErrorEvent, FWebSocketConnectionErrorEvent); virtual FWebSocketConnectionErrorEvent& OnConnectionError() override { return ConnectionErrorEvent; } /** Delegate called when a web socket connection has been closed */ DECLARE_DERIVED_EVENT(FLwsWebSocket, IWebSocket::FWebSocketClosedEvent, FWebSocketClosedEvent); virtual FWebSocketClosedEvent& OnClosed() override { return ClosedEvent; } /** Delegate called when a web socket text message has been received */ DECLARE_DERIVED_EVENT(FLwsWebSocket, IWebSocket::FWebSocketMessageEvent, FWebSocketMessageEvent); virtual FWebSocketMessageEvent& OnMessage() override { return MessageEvent; } /** Delegate called when a web socket binary message has been received */ DECLARE_DERIVED_EVENT(FLwsWebSocket, IWebSocket::FWebSocketBinaryMessageEvent, FWebSocketBinaryMessageEvent); virtual FWebSocketBinaryMessageEvent& OnBinaryMessage() override { return BinaryMessageEvent; } /** Delegate called when a any web socket message has been received */ DECLARE_DERIVED_EVENT(FLwsWebSocket, IWebSocket::FWebSocketRawMessageEvent, FWebSocketRawMessageEvent); virtual FWebSocketRawMessageEvent& OnRawMessage() override { return RawMessageEvent; } DECLARE_DERIVED_EVENT(FLwsWebSocket, IWebSocket::FWebSocketMessageSentEvent, FWebSocketMessageSentEvent); virtual FWebSocketMessageSentEvent& OnMessageSent() override { return OnMessageSentEvent; } /** Callback on events for our libwebsockets connection */ int LwsCallback(lws* Instance, lws_callback_reasons Reason, void* Data, size_t Length); /** * Tick on the game thread */ void GameThreadTick(); /** * Handle being removed the game thread - trigger our OnClosed/OnConnectionError delegate */ void GameThreadFinalize(); /** * Setup to be run on the libwebsockets thread * @param LwsContext libwebsockets context, should only be used when creating a libwebsockets socket * @return true if we successfully initialized, false if we are done (should not be ticked on the libwebsockets thread) */ bool LwsThreadInitialize(struct lws_context &LwsContext); /** * Tick on the libwebsockets thread */ void LwsThreadTick(); private: // Private token only allows members or friends to call MakeShared struct FPrivateToken { explicit FPrivateToken() = default; }; friend class FLwsWebSocketsManager; public: /** Constructor */ FLwsWebSocket(FPrivateToken, const FString& Url, const TArray& Protocols, const FString& UpgradeHeader, uint64 TextMessageMemoryLimit); private: /** * Start connecting * @param LwsContext libwebsockets context */ void ConnectInternal(struct lws_context &LwsContext); /** Send data from our pending outgoing queue */ void SendFromQueue(); /** Clear our queues (does not send / process) */ void ClearData(); /** * Write the specified send buffer * @return true if the write was successful (not necessarily finished), false if an error occurred */ bool WriteBuffer(FLwsSendBuffer& Buffer); private: /** Critical section to lock access to state and close request variables */ FTransactionallySafeCriticalSection StateLock; /** Possible state values */ enum class EState : uint8 { /** Constructed, nothing to do */ None, /** Awaiting connection start */ StartConnecting, /** Connecting */ Connecting, /** Connected */ Connected, /** Closing (self initiated) */ ClosingByRequest, /** Closed */ Closed, /** Errored, nothing to do */ Error, }; /** Get string representation of EState */ static const TCHAR* ToString(const EState InState); /** Structure containing reason for entering the close/error state */ struct FClosedReason { /** Descriptive reason for the state change */ FString Reason; /** Close status (for State=Closed) */ uint16 CloseStatus; /** Was close clean? (for State=Closed) */ bool bWasClean; }; /** Our current state */ EState State; /** Reason for changing state */ FClosedReason ClosedReason; /** Last state seen on the game thread */ EState LastGameThreadState; /** Parameters from a close request from the owner of this web socket */ struct FCloseRequest { /** Constructor */ FCloseRequest() : Code(0), Reason(nullptr) {} /** Code specified when calling Close() */ int32 Code; /** Reason specified when calling Close(), converted to ANSICHAR for libwebsockets */ ANSICHAR *Reason; }; /** Close request from the owner of this web socket, to be processed on the libwebsockets thread */ FCloseRequest CloseRequest; /** Was the send queue empty last time we checked it on our thread? */ bool bWasSendQueueEmpty; // Events FWebSocketConnectedEvent ConnectedEvent; FWebSocketConnectionErrorEvent ConnectionErrorEvent; FWebSocketClosedEvent ClosedEvent; FWebSocketMessageEvent MessageEvent; FWebSocketBinaryMessageEvent BinaryMessageEvent; FWebSocketRawMessageEvent RawMessageEvent; FWebSocketMessageSentEvent OnMessageSentEvent; /** libwebsockets connection */ struct lws *LwsConnection; /** Url we are connecting to */ FString Url; /** Protocols to use with this connection */ TArray Protocols; /** The upgrade header(s) to send with the upgrade request */ FString UpgradeHeader; /** * Whether or not OnMessage was bound to when Connect() was called. * For performance reasons if nothing was bound at Connect() time, we will never trigger OnMessage */ bool bWantsMessageEvents; /** * Whether or not OnRawMessage was bound to when Connect() was called. * For performance reasons if nothing was bound at Connect() time, we will never trigger OnRawMessage */ bool bWantsRawMessageEvents; /** * Whether or not OnBinaryMessage was bound to when Connect() was called. * For performance reasons if nothing was bound at Connect() time, we will never trigger OnBinaryMessage */ bool bWantsBinaryMessageEvents; /** Buffer of an incomplete packet received */ FString ReceiveBuffer; /** Received binary packets, waiting for delegates to be triggered on the game thread */ TQueue ReceiveBinaryQueue; /** Received fragments of binary packets, waiting for delegates to be triggered on the game thread */ TQueue ReceiveBinaryFragmentQueue; /** Received text packets, waiting for delegates to be triggered on the game thread */ TQueue ReceiveTextQueue; /** Pending outgoing packets, populated by the game thread and processed on the libwebsockets thread */ TQueue SendQueue; uint64 MaxTextMessageBufferSize; // Unique identifier for logging /** Incrementing identifier to give each web socket a unique identifier */ static int32 IncrementingIdentifier; /** Our unique identifier */ const int32 Identifier; }; typedef TSharedRef FLwsWebSocketRef; #endif