/**************************************************************************** Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. Copyright (c) 2019-present Axmol Engine contributors (see AUTHORS.md). https://axmolengine.github.io/ Permission is * hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to * whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this * permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS * PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************/ #pragma once #if !defined(__EMSCRIPTEN__) # include # include # include # include // for std::shared_ptr # include # include # include # include "platform/PlatformMacros.h" # include "platform/StdC.h" # include "base/Scheduler.h" # include "base/ConcurrentDeque.h" # include "yasio/yasio_fwd.hpp" # include "yasio/byte_buffer.hpp" # include "network/Uri.h" # include "llhttp.h" # include "websocket_parser.h" NS_AX_BEGIN class EventListenerCustom; namespace network { struct WebSocketProtocol; /** * WebSocket is wrapper of the libwebsockets-protocol, let the develop could call the websocket easily. * Please note that all public methods of WebSocket have to be invoked on Cocos Thread. */ class AX_DLL WebSocket { friend struct WebSocketProtocol; public: /** * Constructor of WebSocket. * * @js ctor */ WebSocket(); /** * Destructor of WebSocket. * * @js NA * @lua NA */ virtual ~WebSocket(); /** * ErrorCode enum used to represent the error in the websocket. */ enum class ErrorCode { OK, TIME_OUT, /** < value 0 */ CONNECTION_FAILURE, /** < value 1 */ NO_SEC_ACCEPT, VERIFY_SEC_ACCEPT_FAILURE, UPGRADE_FAILURE }; /** * State enum used to represent the Websocket state. */ enum class State { CONNECTING, /** < value 0 */ OPEN, /** < value 1 */ CLOSING, /** < value 2 */ CLOSED, /** < value 3 */ }; class Event : public ax::Ref { public: enum class Type { ON_OPEN, ON_ERROR, ON_CLOSE, ON_MESSAGE, }; Type getType() const { return _type; } protected: Type _type; }; class WsOpenEvent : public Event { public: WsOpenEvent() { this->_type = Type::ON_OPEN; } }; class CloseEvent : public Event { public: CloseEvent() { this->_type = Type::ON_CLOSE; } }; class ErrorEvent : public Event { public: ErrorEvent(ErrorCode errorCode) : _errorCode(errorCode) { this->_type = Type::ON_ERROR; } ErrorCode getErrorCode() const { return _errorCode; } private: ErrorCode _errorCode; }; class MessageEvent : public Event { public: MessageEvent(yasio::sbyte_buffer&& message, bool isBinary) : _message(std::move(message)), _isBinary(isBinary) { this->_type = Type::ON_MESSAGE; } yasio::sbyte_buffer& getMessage() { return _message; } const yasio::sbyte_buffer& getMessage() const { return _message; } bool isBinary() const { return _isBinary; } private: yasio::sbyte_buffer _message; bool _isBinary; /* TODO: issued; bool isBinary; void* ext; */ }; /** * Data structure for message view */ struct Data { Data() : bytes(nullptr), len(0), issued(0), isBinary(false), ext(nullptr) {} Data(MessageEvent* event) : bytes(event->getMessage().data()) , len(event->getMessage().size()) , isBinary(event->isBinary()) , issued(0) , ext(nullptr) {} const char* bytes; size_t len, issued; bool isBinary; void* ext; }; /** * The delegate class is used to process websocket events. * * The most member function are pure virtual functions,they should be implemented the in subclass. * @lua NA */ class Delegate { public: /** Destructor of Delegate. */ virtual ~Delegate() {} /** * This function to be called after the client connection complete a handshake with the remote server. * This means that the WebSocket connection is ready to send and receive data. * * @param ws The WebSocket object connected */ virtual void onOpen(WebSocket* ws) = 0; /** * This function to be called when data has appeared from the server for the client connection. * * @param ws The WebSocket object connected. * @param data Data object for message. */ virtual void onMessage(WebSocket* ws, const Data& data) = 0; /** * When the WebSocket object connected wants to close or the protocol won't get used at all and current * _readyState is State::CLOSING,this function is to be called. * * @param ws The WebSocket object connected. */ virtual void onClose(WebSocket* ws) = 0; /** * This function is to be called in the following cases: * 1. client connection is failed. * 2. the request client connection has been unable to complete a handshake with the remote server. * 3. the protocol won't get used at all after this callback and current _readyState is State::CONNECTING. * 4. when a socket descriptor needs to be removed from an external polling array. in is again the struct * libwebsocket_pollargs containing the fd member to be removed. If you are using the internal polling loop, you * can just ignore it and current _readyState is State::CONNECTING. * * @param ws The WebSocket object connected. * @param error WebSocket::ErrorCode enum,would be ErrorCode::TIME_OUT or ErrorCode::CONNECTION_FAILURE. */ virtual void onError(WebSocket* ws, const ErrorCode& error) = 0; }; /** * @brief The initialized method for websocket. * It needs to be invoked right after websocket instance is allocated. * @param delegate The delegate which want to receive event from websocket. * @param url The URL of websocket server. * @param caFilePath The ca file path for wss connection * @param protocols The websocket protocols that agree with websocket server * @return true: Success, false: Failure. * @lua NA */ bool open(Delegate* delegate, std::string_view url, std::string_view caFilePath = "", const char* protocols = nullptr); /** * @brief Sends string data to websocket server. * * @param message string data. * @lua sendstring */ void send(std::string_view message); /** * @brief Sends binary data to websocket server. * * @param binaryMsg binary string data. * @param len the size of binary string data. * @lua sendstring */ void send(const void* data, unsigned int len); /** * @brief Closes the connection to server synchronously. * @note It's a synchronous method, it will not return until websocket thread exits. */ void close(); /** * @brief Closes the connection to server asynchronously. * @note It's an asynchronous method, it just notifies websocket thread to exit and returns directly, * If using 'closeAsync' to close websocket connection, * be careful of not using destructed variables in the callback of 'onClose'. */ void closeAsync(); /** * @brief Gets current state of connection. * @return State the state value could be State::CONNECTING, State::OPEN, State::CLOSING or State::CLOSED */ State getReadyState() const { return _state; }; /** * @brief Gets the URL of websocket connection. */ inline std::string_view getUrl() const { return _url; } /** * @brief Gets the protocol selected by websocket server. */ inline std::string_view getProtocol() const { return _selectedProtocol; } protected: void purgePendingEvents(); void dispatchEvents(); void setupParsers(); void generateHandshakeSecKey(); void handleNetworkEvent(yasio::io_event* event); void do_handshake(const char* d, size_t n) { enum llhttp_errno err = llhttp_execute(&_context, d, n); if (err != HPE_OK) { _handshakeFinished = true; } } static int on_header_field(llhttp_t* context, const char* at, size_t length) { auto thiz = (WebSocket*)context->data; thiz->_currentHeader.insert(thiz->_currentHeader.end(), at, at + length); return 0; } static int on_header_field_complete(llhttp_t* context) { auto thiz = (WebSocket*)context->data; std::transform(thiz->_currentHeader.begin(), thiz->_currentHeader.end(), thiz->_currentHeader.begin(), ::tolower); return 0; } static int on_header_value(llhttp_t* context, const char* at, size_t length) { auto thiz = (WebSocket*)context->data; thiz->_currentHeaderValue.insert(thiz->_currentHeaderValue.end(), at, at + length); return 0; } static int on_header_value_complete(llhttp_t* context) { auto thiz = (WebSocket*)context->data; thiz->_responseHeaders.emplace(std::move(thiz->_currentHeader), std::move(thiz->_currentHeaderValue)); return 0; } static int on_body(llhttp_t* context, const char* at, size_t length) { auto thiz = (WebSocket*)context->data; thiz->_responseData.insert(thiz->_responseData.end(), at, at + length); return 0; } static int on_complete(llhttp_t* context) { auto thiz = (WebSocket*)context->data; thiz->_responseCode = context->status_code; thiz->_handshakeFinished = true; return 0; } static int on_frame_header(websocket_parser*); static int on_frame_body(websocket_parser*, const char* at, size_t length); static int on_frame_end(websocket_parser*); protected: using ResponseHeaderMap = std::multimap; Scheduler* _scheduler; std::shared_ptr> _isDestroyed; // for close sync std::shared_ptr> _syncCloseState; std::string _url; Uri _requestUri; std::string _selectedProtocol; std::string _caFilePath; std::string _handshakeSecKey; std::string _verifySecKey; Delegate* _delegate{}; yasio::io_service* _service; yasio::transport_handle_t _transport{}; yasio::highp_timer* _heartbeatTimer; State _state = State::CLOSED; bool _handshakeFinished = false; // for parse handshake response llhttp_t _context; llhttp_settings_t _contextSettings; // for parse websocket frame websocket_parser _wsParser; websocket_parser_settings _wsParserSettings; enum class FrameState { BEGIN, HEADER, BODY, END, FIN, }; FrameState _frameState = FrameState::BEGIN; int _opcode = 0; std::string _currentHeader; std::string _currentHeaderValue; ResponseHeaderMap _responseHeaders; /// the returned raw header data. You can also dump it as a string std::string _responseData; /// the handshake repsonse message when fail /// the http status code returned from server, e.g. 200, 404, refer to /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status int _responseCode = -1; // for receiveData yasio::sbyte_buffer _receivedData; std::recursive_mutex _receivedDataMtx; EventListenerCustom* _resetDirectorListener; ConcurrentDeque _eventQueue; }; } // namespace network NS_AX_END #else # include "network/WebSocket-wasm.h" #endif