mirror of
https://github.com/heroiclabs/nakama-cpp.git
synced 2026-11-13 20:12:24 -10:00
380 lines
14 KiB
C++
380 lines
14 KiB
C++
/*
|
|
* Copyright 2022 The Nakama Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "NWebsocketLibHC.h"
|
|
#include <chrono>
|
|
#include <httpClient/httpClient.h>
|
|
#include <mutex>
|
|
#include <nakama-cpp/log/NLogger.h>
|
|
|
|
namespace Nakama {
|
|
|
|
// libhttpclient has a bug where it can call callback even after we closed websocket, which
|
|
// leads to hard to reproduce and debug use-after-free errors (https://github.com/microsoft/libHttpClient/issues/698)
|
|
// Workaround is to track all allocated instances in global registry and check for existence in the websocket callback
|
|
static struct {
|
|
void pop(NWebsocketLibHC* key) {
|
|
std::unique_lock lock(_mutex);
|
|
auto it = std::find(_instances.begin(), _instances.end(), key);
|
|
if (it != std::end(_instances)) {
|
|
_instances.erase(it);
|
|
}
|
|
}
|
|
|
|
void push(NWebsocketLibHC* key) {
|
|
std::unique_lock lock(_mutex);
|
|
auto it = std::find(_instances.begin(), _instances.end(), key);
|
|
if (it == std::end(_instances)) {
|
|
_instances.push_back(key);
|
|
}
|
|
}
|
|
|
|
// returns locked unique_lock if key is found, unlocked otherwise
|
|
std::unique_lock<std::mutex> lockKey(NWebsocketLibHC* key) {
|
|
std::unique_lock lock(_mutex);
|
|
auto it = std::find(_instances.begin(), _instances.end(), key);
|
|
if (it == std::end(_instances)) {
|
|
lock.unlock();
|
|
}
|
|
return lock;
|
|
}
|
|
|
|
private:
|
|
std::mutex _mutex;
|
|
std::vector<NWebsocketLibHC*> _instances;
|
|
} g_registry;
|
|
|
|
HC_DEFINE_TRACE_AREA(wsTransportLibHC, HCTraceLevel::Verbose);
|
|
|
|
// We do not call HCCleanup() on purpose to prevent indefinite hang.
|
|
// Because of that, lets not re-init every instantiation , even though it is safe, it still
|
|
// piling up reference count. It is better to have exactly one HCInitialize unpaired
|
|
// with HCCleanup, than unspecified number of them.
|
|
static bool g_libhc_initialized = false;
|
|
|
|
static NTimestamp getUnixTimestampMs() {
|
|
using namespace std::chrono;
|
|
auto ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch());
|
|
|
|
return ms.count();
|
|
}
|
|
|
|
// Converts our loglevel to HCTracelevel
|
|
static HCTraceLevel HCLevelForNLevel(NLogLevel lvl) {
|
|
switch (lvl) {
|
|
case NLogLevel::Debug:
|
|
return HCTraceLevel::Warning;
|
|
case NLogLevel::Info:
|
|
return HCTraceLevel::Information;
|
|
case NLogLevel::Warn:
|
|
return HCTraceLevel::Warning;
|
|
case NLogLevel::Error:
|
|
return HCTraceLevel::Error;
|
|
case NLogLevel::Fatal:
|
|
return HCTraceLevel::Error;
|
|
}
|
|
|
|
return HCTraceLevel::Off;
|
|
}
|
|
|
|
static void configureNLogger() {
|
|
static bool configured = false;
|
|
auto l = NLogger::getSink();
|
|
if (!l) {
|
|
return;
|
|
}
|
|
|
|
if (configured)
|
|
return;
|
|
configured = true;
|
|
|
|
HCSettingsSetTraceLevel(HCLevelForNLevel(l->getLevel()));
|
|
HCTraceSetClientCallback(
|
|
[](const char* areaName, HCTraceLevel level, uint64_t /*threadId*/, uint64_t /*timestamp*/, const char* message) {
|
|
switch (level) {
|
|
case HCTraceLevel::Verbose:
|
|
return NLogger::Debug(message, areaName);
|
|
case HCTraceLevel::Information:
|
|
return NLogger::Info(message, areaName);
|
|
case HCTraceLevel::Important:
|
|
case HCTraceLevel::Warning:
|
|
return NLogger::Warn(message, areaName);
|
|
case HCTraceLevel::Error:
|
|
return NLogger::Error(message, areaName);
|
|
case HCTraceLevel::Off:
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
|
|
std::unique_ptr<NWebsocketLibHC> NWebsocketLibHC::New(const NPlatformParameters& platformParams) {
|
|
std::unique_ptr<NWebsocketLibHC> obj = nullptr;
|
|
|
|
configureNLogger();
|
|
|
|
#ifdef __ANDROID__
|
|
HCInitArgs initArgs{.javaVM = platformParams.javaVM, .applicationContext = platformParams.applicationContext};
|
|
HCInitArgs* initArgsParam = &initArgs;
|
|
#else
|
|
(void)platformParams;
|
|
HCInitArgs* initArgsParam = nullptr;
|
|
#endif
|
|
if (!g_libhc_initialized) {
|
|
HRESULT hr = HCInitialize(initArgsParam);
|
|
if (FAILED(hr)) {
|
|
HC_TRACE_ERROR_HR(wsTransportLibHC, hr, "HCInitialize failed");
|
|
return nullptr;
|
|
}
|
|
g_libhc_initialized = true;
|
|
}
|
|
XTaskQueueHandle q = nullptr;
|
|
HRESULT hr = XTaskQueueCreate(
|
|
XTaskQueueDispatchMode::ThreadPool,
|
|
XTaskQueueDispatchMode::Manual, // callbacks from ticks
|
|
&q);
|
|
|
|
if (FAILED(hr)) {
|
|
HC_TRACE_ERROR(wsTransportLibHC, "Error XTaskQueueCreate(): hr=%d", hr);
|
|
return nullptr;
|
|
}
|
|
return std::unique_ptr<NWebsocketLibHC>(new NWebsocketLibHC(q));
|
|
}
|
|
|
|
NWebsocketLibHC::NWebsocketLibHC(XTaskQueueHandle q)
|
|
: m_queue(q, &XTaskQueueCloseHandle), m_ws(nullptr, &HCWebSocketCloseHandle), m_is_binary(false),
|
|
_activityTimeoutMs(0), _lastReceivedMessageTimeMs(0) {
|
|
g_registry.push(this);
|
|
}
|
|
|
|
NWebsocketLibHC::~NWebsocketLibHC() noexcept {
|
|
disconnect();
|
|
|
|
g_registry.pop(this);
|
|
|
|
// We can proceed with destroying this instance if even in the presence
|
|
// of outstanding connections, like those which hang, because closing
|
|
// HCWebsocketHandle in m_ws seem to prevent callbacks passed to
|
|
// `HCWebSocketCreate` from firing even for hanging connections left
|
|
// "live" in the guts of Libhttpclient.
|
|
m_ws.reset(nullptr);
|
|
m_queue.reset(nullptr);
|
|
|
|
// Don't cleanup on purpose, because of https://github.com/microsoft/libHttpClient/issues/696
|
|
// HCCleanup();
|
|
HC_TRACE_INFORMATION(wsTransportLibHC, "Destroying instance %p", this);
|
|
}
|
|
|
|
void NWebsocketLibHC::connect(const std::string& url, NRtTransportType type) {
|
|
{
|
|
HCWebsocketHandle ws = nullptr;
|
|
auto hr = HCWebSocketCreate(
|
|
&ws, reinterpret_cast<HCWebSocketMessageFunction>(&ws_on_text_msg),
|
|
reinterpret_cast<HCWebSocketBinaryMessageFunction>(&ws_on_binary_msg),
|
|
reinterpret_cast<HCWebSocketCloseEventFunction>(&ws_on_close), this);
|
|
if (FAILED(hr)) {
|
|
HC_TRACE_ERROR(wsTransportLibHC, "Error HCWebSocketCreate(): hr=%d", hr);
|
|
this->fireOnError("Websocket connection error");
|
|
return;
|
|
}
|
|
#if HC_PLATFORM == HC_PLATFORM_WIN32 || HC_PLATFORM == HC_PLATFORM_GDK
|
|
HCWebSocketSetMaxReceiveBufferSize(ws, 1024 * 1024);
|
|
#endif
|
|
m_ws.reset(ws);
|
|
_state.store(State::Connecting, std::memory_order_relaxed);
|
|
}
|
|
|
|
auto* asyncBlock = new XAsyncBlock{};
|
|
asyncBlock->context = this;
|
|
asyncBlock->queue = m_queue.get(); // unique_ptr m_queue will outlive asyncBlock, so thats fine
|
|
asyncBlock->callback = [](XAsyncBlock* ab) {
|
|
const std::unique_ptr<XAsyncBlock> asyncBlock(ab);
|
|
auto self = static_cast<NWebsocketLibHC*>(asyncBlock->context);
|
|
|
|
WebSocketCompletionResult result{};
|
|
HRESULT hr = HCGetWebSocketConnectResult(asyncBlock.get(), &result);
|
|
if (SUCCEEDED(hr)) {
|
|
// Check that m_ws is still present, meaning no disconnects have been called
|
|
// actual socket handle comparison is commented due to https://github.com/microsoft/libHttpClient/issues/697
|
|
// because of that, we won't be able to process connect - disconnect - connect sequence correctly
|
|
if (self->m_ws /*&& self->m_ws.get() == result.websocket*/) {
|
|
if (SUCCEEDED(result.errorCode)) {
|
|
self->_lastReceivedMessageTimeMs = getUnixTimestampMs();
|
|
self->_state.store(State::Connected, std::memory_order_relaxed);
|
|
self->fireOnConnected();
|
|
} else {
|
|
self->_state.store(State::Disconnected, std::memory_order_relaxed);
|
|
self->fireOnError("Websocket connection error");
|
|
}
|
|
} else {
|
|
// While we were connecting, user called disconnect() and closed websocket, making
|
|
// this connection notification useless, so we ignore it and don't trigger any callbacks
|
|
HC_TRACE_WARNING(
|
|
wsTransportLibHC,
|
|
"User requested disconnect, while connection was in progress. Ignoring connection result.");
|
|
}
|
|
} else {
|
|
self->_state.store(State::Disconnected, std::memory_order_relaxed);
|
|
self->fireOnError("Websocket connection error: HCGetWebsocketConnectResult failed");
|
|
}
|
|
};
|
|
m_is_binary = (NRtTransportType::Binary == type);
|
|
HCWebSocketConnectAsync(url.c_str(), "", m_ws.get(), asyncBlock);
|
|
}
|
|
|
|
void NWebsocketLibHC::disconnect() {
|
|
if (m_ws) {
|
|
HCWebSocketDisconnect(m_ws.get());
|
|
|
|
// Prevent any late responses from being processed
|
|
m_ws.reset(nullptr);
|
|
|
|
_connected = false; // white lie, we didn't receive close confirmation from server yet
|
|
_state.store(State::Disconnected, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
|
|
bool NWebsocketLibHC::isConnecting() const { return _state.load(std::memory_order_relaxed) == State::Connecting; }
|
|
|
|
void NWebsocketLibHC::setActivityTimeout(uint32_t timeoutMs) { _activityTimeoutMs = timeoutMs; }
|
|
|
|
uint32_t NWebsocketLibHC::getActivityTimeout() const { return _activityTimeoutMs; }
|
|
|
|
void NWebsocketLibHC::tick() {
|
|
while (XTaskQueueDispatch(m_queue.get(), XTaskQueuePort::Completion, 0)) {
|
|
}
|
|
|
|
if (isConnected() && _activityTimeoutMs > 0 &&
|
|
getUnixTimestampMs() - _lastReceivedMessageTimeMs >= _activityTimeoutMs) {
|
|
disconnect();
|
|
}
|
|
}
|
|
|
|
bool NWebsocketLibHC::send(const NBytes& data) {
|
|
if (!isConnected()) {
|
|
return false;
|
|
}
|
|
|
|
HC_TRACE_INFORMATION(wsTransportLibHC, "Sending bytes");
|
|
|
|
auto* asyncBlock = new XAsyncBlock{};
|
|
|
|
asyncBlock->context = this;
|
|
asyncBlock->queue = m_queue.get(); // unique_ptr m_queue will outlive asyncBlock, so thats fine
|
|
asyncBlock->callback = [](XAsyncBlock* ab) {
|
|
std::unique_ptr<XAsyncBlock> asyncBlock(ab);
|
|
WebSocketCompletionResult result;
|
|
HRESULT hr = HCGetWebSocketSendMessageResult(asyncBlock.get(), &result);
|
|
if (FAILED(result.errorCode) || FAILED(hr)) {
|
|
HC_TRACE_ERROR(
|
|
wsTransportLibHC, "Send error: hr=0x%08x ws.errorCode=0x%08x platformErr=0x%08x", hr, result.errorCode,
|
|
result.platformErrorCode);
|
|
static_cast<NWebsocketLibHC*>(asyncBlock->context)->fireOnError("Error sending msg");
|
|
}
|
|
};
|
|
HRESULT hr;
|
|
if (m_is_binary) {
|
|
hr = HCWebSocketSendBinaryMessageAsync(
|
|
m_ws.get(), reinterpret_cast<const uint8_t*>(data.data()), uint32_t(data.size()), asyncBlock);
|
|
} else {
|
|
hr = HCWebSocketSendMessageAsync(m_ws.get(), data.c_str(), asyncBlock);
|
|
}
|
|
if (FAILED(hr)) {
|
|
HC_TRACE_ERROR(wsTransportLibHC, "Send error: hr=0x%08x", hr);
|
|
}
|
|
return SUCCEEDED(hr);
|
|
}
|
|
|
|
void __stdcall NWebsocketLibHC::ws_on_text_msg(HCWebsocketHandle ws, const char* s, void* context) {
|
|
HC_TRACE_INFORMATION(wsTransportLibHC, "ws_on_text_msg ws=%p", ws);
|
|
auto self = static_cast<NWebsocketLibHC*>(context);
|
|
if (auto lock = g_registry.lockKey(self)) {
|
|
self->on_msg(ws, std::string(s));
|
|
}
|
|
}
|
|
|
|
void __stdcall NWebsocketLibHC::ws_on_binary_msg(
|
|
HCWebsocketHandle ws,
|
|
const uint8_t* bytes,
|
|
uint32_t size,
|
|
void* context) {
|
|
HC_TRACE_INFORMATION(wsTransportLibHC, "ws_on_binary_msg ws=%p", ws);
|
|
auto self = static_cast<NWebsocketLibHC*>(context);
|
|
if (auto lock = g_registry.lockKey(self)) {
|
|
self->on_msg(ws, std::string(reinterpret_cast<const char*>(bytes), size));
|
|
}
|
|
}
|
|
|
|
// Callback is called on an unspecified thread when socket is closed.
|
|
// Arrange fireOnDisconnected() to be called from the tick()
|
|
void __stdcall NWebsocketLibHC::ws_on_close(HCWebsocketHandle ws, HCWebSocketCloseStatus status, void* context) {
|
|
HC_TRACE_INFORMATION(wsTransportLibHC, "ws_on_close ws=%p", ws);
|
|
auto self = static_cast<NWebsocketLibHC*>(context);
|
|
|
|
if (auto lock = g_registry.lockKey(self)) {
|
|
using ctx_t = std::tuple<NWebsocketLibHC*, uint16_t, HCWebsocketHandle>;
|
|
auto ctx = new ctx_t(self, static_cast<uint16_t>(status), ws);
|
|
HC_TRACE_INFORMATION(wsTransportLibHC, "ws_on_close: accessing instance %p", self);
|
|
|
|
XTaskQueueSubmitCallback(self->m_queue.get(), XTaskQueuePort::Completion, ctx, [](void* context, bool) {
|
|
std::unique_ptr<ctx_t> ctx(static_cast<ctx_t*>(context));
|
|
auto self = std::get<0>(*ctx);
|
|
auto code = std::get<1>(*ctx);
|
|
auto ws = std::get<2>(*ctx);
|
|
|
|
// m_ws is closed on disconnects, but already scheduled notifications are still processed.
|
|
// Here we check that notification is for the "active" socket, not a previously closed one to avoid race
|
|
if (self->m_ws && self->m_ws.get() == ws) {
|
|
self->m_ws.reset(nullptr);
|
|
self->_state.store(State::Disconnected, std::memory_order_relaxed);
|
|
NRtClientDisconnectInfo info;
|
|
info.code = code;
|
|
info.remote = true;
|
|
self->fireOnDisconnected(info);
|
|
} else {
|
|
// if we are here, then user initiated disconnect and callback was already fired
|
|
HC_TRACE_INFORMATION(
|
|
wsTransportLibHC, "ws_on_close->tick: ignoring close for use-initiated disconnect on %p", ws);
|
|
}
|
|
});
|
|
} else {
|
|
HC_TRACE_INFORMATION(wsTransportLibHC, "ws_on_close: ignoring close for already deleted instance %p", self);
|
|
}
|
|
}
|
|
|
|
// Called by libhttpClient when message arrives. Schedule fireOnMessage to be called on the
|
|
// tick() thread
|
|
// NOTE: If message exceeds internal buffer (1MiB), partial message will be passed here with no indication
|
|
// that it was truncated.
|
|
void NWebsocketLibHC::on_msg(HCWebsocketHandle ws, std::string&& s) {
|
|
using ctx_t = std::tuple<NWebsocketLibHC*, std::string, HCWebsocketHandle>;
|
|
auto ctx = new ctx_t(this, s, ws);
|
|
|
|
XTaskQueueSubmitCallback(this->m_queue.get(), XTaskQueuePort::Completion, ctx, [](void* context, bool) {
|
|
std::unique_ptr<ctx_t> ctx(static_cast<ctx_t*>(context));
|
|
auto self = std::get<0>(*ctx);
|
|
auto& buf = std::get<1>(*ctx);
|
|
auto ws = std::get<2>(*ctx);
|
|
|
|
// m_ws is closed on disconnects, but already scheduled notifications are still processed.
|
|
// Here we check that notification is for the "active" socket, not a previously closed one to avoid race
|
|
if (self->m_ws && self->m_ws.get() == ws) {
|
|
self->fireOnMessage(buf);
|
|
self->_lastReceivedMessageTimeMs = getUnixTimestampMs();
|
|
}
|
|
});
|
|
}
|
|
} // namespace Nakama
|