Add native GNS server bridge
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
name: GNS Transport Bridge
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "server/native_transport/**"
|
||||
- ".github/workflows/gns-transport.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "server/native_transport/**"
|
||||
- ".github/workflows/gns-transport.yml"
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux native GNS bridge
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y cmake ninja-build libssl-dev libprotobuf-dev protobuf-compiler
|
||||
|
||||
- name: Fetch pinned GameNetworkingSockets
|
||||
run: |
|
||||
GNS_SHA=f4525e39ee10f6b45181fd92d01fee75f7d71756
|
||||
rm -rf /tmp/co-gns-src
|
||||
git init /tmp/co-gns-src
|
||||
git -C /tmp/co-gns-src remote add origin https://github.com/ValveSoftware/GameNetworkingSockets.git
|
||||
git -C /tmp/co-gns-src fetch --depth 1 origin "$GNS_SHA"
|
||||
git -C /tmp/co-gns-src checkout --detach FETCH_HEAD
|
||||
test "$(git -C /tmp/co-gns-src rev-parse HEAD)" = "$GNS_SHA"
|
||||
|
||||
- name: Build and run native bridge test
|
||||
run: |
|
||||
cmake -S server/native_transport -B /tmp/co-server-gns-build -G Ninja \
|
||||
-DGNS_SOURCE_DIR=/tmp/co-gns-src \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build /tmp/co-server-gns-build --target co-gns-server-bridge-tests -j2
|
||||
ctest --test-dir /tmp/co-server-gns-build --output-on-failure
|
||||
|
||||
- name: Confirm shared bridge artifact
|
||||
run: |
|
||||
test -f /tmp/co-server-gns-build/libcommonwealth_online_gns_bridge.so
|
||||
ldd /tmp/co-server-gns-build/libcommonwealth_online_gns_bridge.so
|
||||
@@ -0,0 +1,45 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(CommonwealthOnlineGnsServerBridge LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
set(GNS_SOURCE_DIR "" CACHE PATH "Path to the pinned GameNetworkingSockets source tree")
|
||||
if(NOT EXISTS "${GNS_SOURCE_DIR}/CMakeLists.txt")
|
||||
message(FATAL_ERROR "GNS_SOURCE_DIR must point to an initialized GameNetworkingSockets source tree")
|
||||
endif()
|
||||
|
||||
set(BUILD_STATIC_LIB OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_SHARED_LIB ON CACHE BOOL "" FORCE)
|
||||
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_TOOLS OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_ICE OFF CACHE BOOL "" FORCE)
|
||||
set(USE_STEAMWEBRTC OFF CACHE BOOL "" FORCE)
|
||||
set(USE_CRYPTO OpenSSL CACHE STRING "" FORCE)
|
||||
set(USE_CRYPTO25519 OpenSSL CACHE STRING "" FORCE)
|
||||
|
||||
add_subdirectory("${GNS_SOURCE_DIR}" "${CMAKE_BINARY_DIR}/gns")
|
||||
|
||||
add_library(commonwealth_online_gns_bridge SHARED co_gns_server_bridge.cpp)
|
||||
target_include_directories(commonwealth_online_gns_bridge PUBLIC "${CMAKE_CURRENT_LIST_DIR}")
|
||||
target_compile_definitions(commonwealth_online_gns_bridge PRIVATE
|
||||
CO_GNS_BRIDGE_BUILD
|
||||
STEAMNETWORKINGSOCKETS_STANDALONELIB
|
||||
)
|
||||
target_compile_options(commonwealth_online_gns_bridge PRIVATE -Wall -Wextra -Werror)
|
||||
target_link_libraries(commonwealth_online_gns_bridge PRIVATE GameNetworkingSockets::shared)
|
||||
set_target_properties(commonwealth_online_gns_bridge PROPERTIES OUTPUT_NAME "commonwealth_online_gns_bridge")
|
||||
|
||||
add_executable(co-gns-server-bridge-tests test_co_gns_server_bridge.cpp)
|
||||
target_compile_definitions(co-gns-server-bridge-tests PRIVATE STEAMNETWORKINGSOCKETS_STANDALONELIB)
|
||||
target_compile_options(co-gns-server-bridge-tests PRIVATE -Wall -Wextra -Werror)
|
||||
target_link_libraries(co-gns-server-bridge-tests PRIVATE
|
||||
commonwealth_online_gns_bridge
|
||||
GameNetworkingSockets::shared
|
||||
)
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME co-gns-server-bridge COMMAND co-gns-server-bridge-tests)
|
||||
@@ -0,0 +1,570 @@
|
||||
#include "co_gns_server_bridge.h"
|
||||
|
||||
#include <steam/steamnetworkingsockets.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t kMaximumMessageBytes = 64uz * 1024uz;
|
||||
constexpr std::size_t kReceiveBatchSize = 64;
|
||||
|
||||
std::mutex g_runtimeMutex;
|
||||
std::size_t g_runtimeReferenceCount = 0;
|
||||
|
||||
class ServerBridge;
|
||||
|
||||
std::mutex g_ownerMutex;
|
||||
std::unordered_map<HSteamListenSocket, ServerBridge*> g_listenOwners;
|
||||
std::unordered_map<HSteamNetConnection, ServerBridge*> g_connectionOwners;
|
||||
|
||||
void WriteError(char* buffer, std::size_t bufferSize, const std::string& message)
|
||||
{
|
||||
if (buffer == nullptr || bufferSize == 0) {
|
||||
return;
|
||||
}
|
||||
std::snprintf(buffer, bufferSize, "%s", message.c_str());
|
||||
}
|
||||
|
||||
bool AcquireRuntime(std::string& error)
|
||||
{
|
||||
const std::scoped_lock lock(g_runtimeMutex);
|
||||
if (g_runtimeReferenceCount > 0) {
|
||||
++g_runtimeReferenceCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
SteamNetworkingErrMsg errorMessage{};
|
||||
if (!GameNetworkingSockets_Init(nullptr, errorMessage)) {
|
||||
error = errorMessage[0] ? std::string{ errorMessage } : "GameNetworkingSockets_Init failed.";
|
||||
return false;
|
||||
}
|
||||
g_runtimeReferenceCount = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ReleaseRuntime()
|
||||
{
|
||||
const std::scoped_lock lock(g_runtimeMutex);
|
||||
if (g_runtimeReferenceCount == 0) {
|
||||
return;
|
||||
}
|
||||
--g_runtimeReferenceCount;
|
||||
if (g_runtimeReferenceCount == 0) {
|
||||
GameNetworkingSockets_Kill();
|
||||
}
|
||||
}
|
||||
|
||||
struct QueuedEvent
|
||||
{
|
||||
co_gns_event metadata{};
|
||||
std::vector<std::uint8_t> payload;
|
||||
};
|
||||
|
||||
class ServerBridge
|
||||
{
|
||||
public:
|
||||
~ServerBridge()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool Start(const char* bindHost, std::uint16_t port, std::string& error)
|
||||
{
|
||||
if (!AcquireRuntime(error)) {
|
||||
return false;
|
||||
}
|
||||
runtimeHeld_ = true;
|
||||
networking_ = SteamNetworkingSockets();
|
||||
if (networking_ == nullptr) {
|
||||
error = "SteamNetworkingSockets returned no server interface.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
pollGroup_ = networking_->CreatePollGroup();
|
||||
if (pollGroup_ == k_HSteamNetPollGroup_Invalid) {
|
||||
error = "GameNetworkingSockets failed to create a server poll group.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
SteamNetworkingIPAddr address{};
|
||||
address.Clear();
|
||||
const std::string host = bindHost != nullptr ? std::string{ bindHost } : std::string{};
|
||||
if (host.empty() || host == "0.0.0.0") {
|
||||
address.SetIPv4(0U, port);
|
||||
} else {
|
||||
if (!address.ParseString(host.c_str()) || !address.IsIPv4()) {
|
||||
error = "GNS bind address must be a valid IPv4 address.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
address.m_port = port;
|
||||
}
|
||||
|
||||
SteamNetworkingConfigValue_t option{};
|
||||
option.SetPtr(
|
||||
k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged,
|
||||
reinterpret_cast<void*>(+ConnectionStatusChanged));
|
||||
listenSocket_ = networking_->CreateListenSocketIP(address, 1, std::addressof(option));
|
||||
if (listenSocket_ == k_HSteamListenSocket_Invalid) {
|
||||
error = "GameNetworkingSockets failed to create a listen socket.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
const std::scoped_lock ownerLock(g_ownerMutex);
|
||||
g_listenOwners[listenSocket_] = this;
|
||||
}
|
||||
|
||||
SteamNetworkingIPAddr actualAddress{};
|
||||
if (!networking_->GetListenSocketAddress(listenSocket_, std::addressof(actualAddress))) {
|
||||
error = "GameNetworkingSockets could not report the bound listen address.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
localPort_ = actualAddress.m_port;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
ISteamNetworkingSockets* networking = networking_;
|
||||
HSteamListenSocket listenSocket = listenSocket_;
|
||||
HSteamNetPollGroup pollGroup = pollGroup_;
|
||||
std::vector<HSteamNetConnection> connections;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
connections.assign(connections_.begin(), connections_.end());
|
||||
connections_.clear();
|
||||
connectedConnections_.clear();
|
||||
events_.clear();
|
||||
listenSocket_ = k_HSteamListenSocket_Invalid;
|
||||
pollGroup_ = k_HSteamNetPollGroup_Invalid;
|
||||
networking_ = nullptr;
|
||||
localPort_ = 0;
|
||||
}
|
||||
|
||||
{
|
||||
const std::scoped_lock ownerLock(g_ownerMutex);
|
||||
if (listenSocket != k_HSteamListenSocket_Invalid) {
|
||||
g_listenOwners.erase(listenSocket);
|
||||
}
|
||||
for (const auto connection : connections) {
|
||||
g_connectionOwners.erase(connection);
|
||||
}
|
||||
}
|
||||
|
||||
if (networking != nullptr) {
|
||||
for (const auto connection : connections) {
|
||||
networking->CloseConnection(connection, 0, nullptr, false);
|
||||
}
|
||||
if (listenSocket != k_HSteamListenSocket_Invalid) {
|
||||
networking->CloseListenSocket(listenSocket);
|
||||
}
|
||||
if (pollGroup != k_HSteamNetPollGroup_Invalid) {
|
||||
networking->DestroyPollGroup(pollGroup);
|
||||
}
|
||||
}
|
||||
|
||||
if (runtimeHeld_) {
|
||||
runtimeHeld_ = false;
|
||||
ReleaseRuntime();
|
||||
}
|
||||
}
|
||||
|
||||
std::uint16_t LocalPort() const
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
return localPort_;
|
||||
}
|
||||
|
||||
std::uint32_t ConnectionCount() const
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
return static_cast<std::uint32_t>(connectedConnections_.size());
|
||||
}
|
||||
|
||||
int Poll(co_gns_event& outEvent, void* payloadBuffer, std::uint32_t payloadCapacity)
|
||||
{
|
||||
ISteamNetworkingSockets* networking = nullptr;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
networking = networking_;
|
||||
}
|
||||
if (networking == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
networking->RunCallbacks();
|
||||
|
||||
bool queueIsEmpty = false;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
queueIsEmpty = events_.empty();
|
||||
}
|
||||
if (queueIsEmpty) {
|
||||
PumpMessages();
|
||||
}
|
||||
|
||||
const std::scoped_lock lock(mutex_);
|
||||
if (events_.empty()) {
|
||||
std::memset(std::addressof(outEvent), 0, sizeof(outEvent));
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto& next = events_.front();
|
||||
outEvent = next.metadata;
|
||||
if (outEvent.type == CO_GNS_EVENT_MESSAGE && next.payload.size() > payloadCapacity) {
|
||||
return -2;
|
||||
}
|
||||
if (outEvent.type == CO_GNS_EVENT_MESSAGE && !next.payload.empty()) {
|
||||
if (payloadBuffer == nullptr) {
|
||||
return -2;
|
||||
}
|
||||
std::memcpy(payloadBuffer, next.payload.data(), next.payload.size());
|
||||
}
|
||||
events_.pop_front();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int Send(
|
||||
std::uint32_t connectionId,
|
||||
const void* payload,
|
||||
std::uint32_t payloadSize,
|
||||
std::uint32_t delivery)
|
||||
{
|
||||
if (payload == nullptr || payloadSize == 0) {
|
||||
return CO_GNS_SEND_ERROR;
|
||||
}
|
||||
if (payloadSize > kMaximumMessageBytes) {
|
||||
return CO_GNS_SEND_TOO_LARGE;
|
||||
}
|
||||
if (delivery != CO_GNS_DELIVERY_UNRELIABLE_SEQUENCED && delivery != CO_GNS_DELIVERY_RELIABLE_ORDERED) {
|
||||
return CO_GNS_SEND_ERROR;
|
||||
}
|
||||
|
||||
const auto connection = static_cast<HSteamNetConnection>(connectionId);
|
||||
ISteamNetworkingSockets* networking = nullptr;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
if (!connectedConnections_.contains(connection)) {
|
||||
return CO_GNS_SEND_NOT_CONNECTED;
|
||||
}
|
||||
networking = networking_;
|
||||
}
|
||||
if (networking == nullptr) {
|
||||
return CO_GNS_SEND_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
const int flags = delivery == CO_GNS_DELIVERY_UNRELIABLE_SEQUENCED ?
|
||||
k_nSteamNetworkingSend_UnreliableNoDelay :
|
||||
k_nSteamNetworkingSend_ReliableNoNagle;
|
||||
const auto result = networking->SendMessageToConnection(
|
||||
connection,
|
||||
payload,
|
||||
payloadSize,
|
||||
flags,
|
||||
nullptr);
|
||||
switch (result) {
|
||||
case k_EResultOK:
|
||||
return CO_GNS_SEND_SENT;
|
||||
case k_EResultIgnored:
|
||||
return CO_GNS_SEND_DROPPED;
|
||||
case k_EResultLimitExceeded:
|
||||
return CO_GNS_SEND_BACKPRESSURE;
|
||||
case k_EResultNoConnection:
|
||||
case k_EResultInvalidState:
|
||||
return CO_GNS_SEND_NOT_CONNECTED;
|
||||
default:
|
||||
return CO_GNS_SEND_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
int Disconnect(std::uint32_t connectionId, std::int32_t reason, const char* debug)
|
||||
{
|
||||
const auto connection = static_cast<HSteamNetConnection>(connectionId);
|
||||
ISteamNetworkingSockets* networking = nullptr;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
if (!connections_.contains(connection)) {
|
||||
return 0;
|
||||
}
|
||||
connections_.erase(connection);
|
||||
connectedConnections_.erase(connection);
|
||||
networking = networking_;
|
||||
}
|
||||
{
|
||||
const std::scoped_lock ownerLock(g_ownerMutex);
|
||||
g_connectionOwners.erase(connection);
|
||||
}
|
||||
if (networking == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return networking->CloseConnection(connection, reason, debug, false) ? 1 : 0;
|
||||
}
|
||||
|
||||
private:
|
||||
static ServerBridge* FindOwner(const SteamNetConnectionStatusChangedCallback_t& info)
|
||||
{
|
||||
const std::scoped_lock ownerLock(g_ownerMutex);
|
||||
const auto connectionOwner = g_connectionOwners.find(info.m_hConn);
|
||||
if (connectionOwner != g_connectionOwners.end()) {
|
||||
return connectionOwner->second;
|
||||
}
|
||||
const auto listenOwner = g_listenOwners.find(info.m_info.m_hListenSocket);
|
||||
return listenOwner == g_listenOwners.end() ? nullptr : listenOwner->second;
|
||||
}
|
||||
|
||||
static void ConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* info)
|
||||
{
|
||||
if (info == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (auto* owner = FindOwner(*info); owner != nullptr) {
|
||||
owner->OnConnectionStatusChanged(*info);
|
||||
}
|
||||
}
|
||||
|
||||
static co_gns_event MakeEvent(
|
||||
std::uint32_t type,
|
||||
HSteamNetConnection connection,
|
||||
std::int32_t reason,
|
||||
std::uint32_t payloadSize,
|
||||
const char* debug)
|
||||
{
|
||||
co_gns_event event{};
|
||||
event.type = type;
|
||||
event.connection_id = static_cast<std::uint32_t>(connection);
|
||||
event.reason = reason;
|
||||
event.payload_size = payloadSize;
|
||||
if (debug != nullptr && debug[0] != '\0') {
|
||||
std::snprintf(event.debug, sizeof(event.debug), "%s", debug);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
void OnConnectionStatusChanged(const SteamNetConnectionStatusChangedCallback_t& info)
|
||||
{
|
||||
if (info.m_info.m_eState == k_ESteamNetworkingConnectionState_Connecting &&
|
||||
info.m_info.m_hListenSocket == listenSocket_) {
|
||||
if (networking_->AcceptConnection(info.m_hConn) != k_EResultOK ||
|
||||
!networking_->SetConnectionPollGroup(info.m_hConn, pollGroup_)) {
|
||||
networking_->CloseConnection(info.m_hConn, 0, "Could not admit GNS connection", false);
|
||||
return;
|
||||
}
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
connections_.insert(info.m_hConn);
|
||||
}
|
||||
{
|
||||
const std::scoped_lock ownerLock(g_ownerMutex);
|
||||
g_connectionOwners[info.m_hConn] = this;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.m_info.m_eState == k_ESteamNetworkingConnectionState_Connected) {
|
||||
const std::scoped_lock lock(mutex_);
|
||||
if (connections_.contains(info.m_hConn) && connectedConnections_.insert(info.m_hConn).second) {
|
||||
events_.push_back(QueuedEvent{
|
||||
MakeEvent(CO_GNS_EVENT_CONNECTED, info.m_hConn, 0, 0, nullptr),
|
||||
{}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.m_info.m_eState != k_ESteamNetworkingConnectionState_ClosedByPeer &&
|
||||
info.m_info.m_eState != k_ESteamNetworkingConnectionState_ProblemDetectedLocally) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool knownConnection = false;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
knownConnection = connections_.erase(info.m_hConn) > 0;
|
||||
connectedConnections_.erase(info.m_hConn);
|
||||
if (knownConnection) {
|
||||
events_.push_back(QueuedEvent{
|
||||
MakeEvent(
|
||||
CO_GNS_EVENT_DISCONNECTED,
|
||||
info.m_hConn,
|
||||
info.m_info.m_eEndReason,
|
||||
0,
|
||||
info.m_info.m_szEndDebug),
|
||||
{}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!knownConnection) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
const std::scoped_lock ownerLock(g_ownerMutex);
|
||||
g_connectionOwners.erase(info.m_hConn);
|
||||
}
|
||||
networking_->CloseConnection(info.m_hConn, 0, nullptr, false);
|
||||
}
|
||||
|
||||
void PumpMessages()
|
||||
{
|
||||
ISteamNetworkingSockets* networking = nullptr;
|
||||
HSteamNetPollGroup pollGroup = k_HSteamNetPollGroup_Invalid;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
networking = networking_;
|
||||
pollGroup = pollGroup_;
|
||||
}
|
||||
if (networking == nullptr || pollGroup == k_HSteamNetPollGroup_Invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<SteamNetworkingMessage_t*, kReceiveBatchSize> messages{};
|
||||
const auto count = networking->ReceiveMessagesOnPollGroup(
|
||||
pollGroup,
|
||||
messages.data(),
|
||||
static_cast<int>(messages.size()));
|
||||
if (count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int index = 0; index < count; ++index) {
|
||||
auto* message = messages[static_cast<std::size_t>(index)];
|
||||
if (message == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const auto size = message->m_cbSize > 0 ? static_cast<std::size_t>(message->m_cbSize) : 0uz;
|
||||
const auto connection = message->m_conn;
|
||||
QueuedEvent queued{};
|
||||
if (size > kMaximumMessageBytes) {
|
||||
queued.metadata = MakeEvent(
|
||||
CO_GNS_EVENT_OVERSIZE_MESSAGE,
|
||||
connection,
|
||||
0,
|
||||
static_cast<std::uint32_t>((std::min)(size, static_cast<std::size_t>(UINT32_MAX))),
|
||||
"Inbound GNS message exceeded 64 KiB");
|
||||
} else {
|
||||
queued.metadata = MakeEvent(
|
||||
CO_GNS_EVENT_MESSAGE,
|
||||
connection,
|
||||
0,
|
||||
static_cast<std::uint32_t>(size),
|
||||
nullptr);
|
||||
if (size > 0 && message->m_pData != nullptr) {
|
||||
const auto* begin = static_cast<const std::uint8_t*>(message->m_pData);
|
||||
queued.payload.assign(begin, begin + size);
|
||||
}
|
||||
}
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
if (connections_.contains(connection)) {
|
||||
events_.push_back(std::move(queued));
|
||||
}
|
||||
}
|
||||
message->Release();
|
||||
}
|
||||
}
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
ISteamNetworkingSockets* networking_{ nullptr };
|
||||
HSteamListenSocket listenSocket_{ k_HSteamListenSocket_Invalid };
|
||||
HSteamNetPollGroup pollGroup_{ k_HSteamNetPollGroup_Invalid };
|
||||
std::unordered_set<HSteamNetConnection> connections_;
|
||||
std::unordered_set<HSteamNetConnection> connectedConnections_;
|
||||
std::deque<QueuedEvent> events_;
|
||||
std::uint16_t localPort_{ 0 };
|
||||
bool runtimeHeld_{ false };
|
||||
};
|
||||
}
|
||||
|
||||
extern "C"
|
||||
{
|
||||
int co_gns_server_create(
|
||||
const char* bind_host,
|
||||
uint16_t port,
|
||||
co_gns_server_handle* out_handle,
|
||||
char* error_buffer,
|
||||
size_t error_buffer_size)
|
||||
{
|
||||
if (out_handle == nullptr) {
|
||||
WriteError(error_buffer, error_buffer_size, "out_handle is required.");
|
||||
return 0;
|
||||
}
|
||||
*out_handle = nullptr;
|
||||
auto* server = new ServerBridge();
|
||||
std::string error;
|
||||
if (!server->Start(bind_host, port, error)) {
|
||||
delete server;
|
||||
WriteError(error_buffer, error_buffer_size, error);
|
||||
return 0;
|
||||
}
|
||||
*out_handle = server;
|
||||
WriteError(error_buffer, error_buffer_size, "");
|
||||
return 1;
|
||||
}
|
||||
|
||||
void co_gns_server_destroy(co_gns_server_handle handle)
|
||||
{
|
||||
delete static_cast<ServerBridge*>(handle);
|
||||
}
|
||||
|
||||
uint16_t co_gns_server_local_port(co_gns_server_handle handle)
|
||||
{
|
||||
const auto* server = static_cast<ServerBridge*>(handle);
|
||||
return server == nullptr ? 0 : server->LocalPort();
|
||||
}
|
||||
|
||||
uint32_t co_gns_server_connection_count(co_gns_server_handle handle)
|
||||
{
|
||||
const auto* server = static_cast<ServerBridge*>(handle);
|
||||
return server == nullptr ? 0 : server->ConnectionCount();
|
||||
}
|
||||
|
||||
int co_gns_server_poll(
|
||||
co_gns_server_handle handle,
|
||||
co_gns_event* out_event,
|
||||
void* payload_buffer,
|
||||
uint32_t payload_capacity)
|
||||
{
|
||||
auto* server = static_cast<ServerBridge*>(handle);
|
||||
if (server == nullptr || out_event == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
return server->Poll(*out_event, payload_buffer, payload_capacity);
|
||||
}
|
||||
|
||||
int co_gns_server_send(
|
||||
co_gns_server_handle handle,
|
||||
uint32_t connection_id,
|
||||
const void* payload,
|
||||
uint32_t payload_size,
|
||||
uint32_t delivery)
|
||||
{
|
||||
auto* server = static_cast<ServerBridge*>(handle);
|
||||
return server == nullptr ? CO_GNS_SEND_ERROR :
|
||||
server->Send(connection_id, payload, payload_size, delivery);
|
||||
}
|
||||
|
||||
int co_gns_server_disconnect(
|
||||
co_gns_server_handle handle,
|
||||
uint32_t connection_id,
|
||||
int32_t reason,
|
||||
const char* debug)
|
||||
{
|
||||
auto* server = static_cast<ServerBridge*>(handle);
|
||||
return server == nullptr ? 0 : server->Disconnect(connection_id, reason, debug);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#if defined(CO_GNS_BRIDGE_BUILD)
|
||||
#define CO_GNS_API __declspec(dllexport)
|
||||
#else
|
||||
#define CO_GNS_API __declspec(dllimport)
|
||||
#endif
|
||||
#else
|
||||
#define CO_GNS_API __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void* co_gns_server_handle;
|
||||
|
||||
enum co_gns_event_type
|
||||
{
|
||||
CO_GNS_EVENT_NONE = 0,
|
||||
CO_GNS_EVENT_CONNECTED = 1,
|
||||
CO_GNS_EVENT_DISCONNECTED = 2,
|
||||
CO_GNS_EVENT_MESSAGE = 3,
|
||||
CO_GNS_EVENT_OVERSIZE_MESSAGE = 4
|
||||
};
|
||||
|
||||
enum co_gns_delivery
|
||||
{
|
||||
CO_GNS_DELIVERY_UNRELIABLE_SEQUENCED = 0,
|
||||
CO_GNS_DELIVERY_RELIABLE_ORDERED = 1
|
||||
};
|
||||
|
||||
enum co_gns_send_result
|
||||
{
|
||||
CO_GNS_SEND_ERROR = -1,
|
||||
CO_GNS_SEND_SENT = 0,
|
||||
CO_GNS_SEND_DROPPED = 1,
|
||||
CO_GNS_SEND_BACKPRESSURE = 2,
|
||||
CO_GNS_SEND_NOT_CONNECTED = 3,
|
||||
CO_GNS_SEND_TOO_LARGE = 4
|
||||
};
|
||||
|
||||
typedef struct co_gns_event
|
||||
{
|
||||
uint32_t type;
|
||||
uint32_t connection_id;
|
||||
int32_t reason;
|
||||
uint32_t payload_size;
|
||||
char debug[128];
|
||||
} co_gns_event;
|
||||
|
||||
CO_GNS_API int co_gns_server_create(
|
||||
const char* bind_host,
|
||||
uint16_t port,
|
||||
co_gns_server_handle* out_handle,
|
||||
char* error_buffer,
|
||||
size_t error_buffer_size);
|
||||
|
||||
CO_GNS_API void co_gns_server_destroy(co_gns_server_handle handle);
|
||||
CO_GNS_API uint16_t co_gns_server_local_port(co_gns_server_handle handle);
|
||||
CO_GNS_API uint32_t co_gns_server_connection_count(co_gns_server_handle handle);
|
||||
|
||||
CO_GNS_API int co_gns_server_poll(
|
||||
co_gns_server_handle handle,
|
||||
co_gns_event* out_event,
|
||||
void* payload_buffer,
|
||||
uint32_t payload_capacity);
|
||||
|
||||
CO_GNS_API int co_gns_server_send(
|
||||
co_gns_server_handle handle,
|
||||
uint32_t connection_id,
|
||||
const void* payload,
|
||||
uint32_t payload_size,
|
||||
uint32_t delivery);
|
||||
|
||||
CO_GNS_API int co_gns_server_disconnect(
|
||||
co_gns_server_handle handle,
|
||||
uint32_t connection_id,
|
||||
int32_t reason,
|
||||
const char* debug);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,215 @@
|
||||
#include "co_gns_server_bridge.h"
|
||||
|
||||
#include <steam/steamnetworkingsockets.h>
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
HSteamNetConnection g_clientConnection = k_HSteamNetConnection_Invalid;
|
||||
bool g_clientConnected = false;
|
||||
bool g_clientFailed = false;
|
||||
|
||||
void ClientConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* info)
|
||||
{
|
||||
if (info == nullptr || info->m_hConn != g_clientConnection) {
|
||||
return;
|
||||
}
|
||||
switch (info->m_info.m_eState) {
|
||||
case k_ESteamNetworkingConnectionState_Connected:
|
||||
g_clientConnected = true;
|
||||
break;
|
||||
case k_ESteamNetworkingConnectionState_ClosedByPeer:
|
||||
case k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
|
||||
g_clientFailed = info->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally;
|
||||
if (auto* networking = SteamNetworkingSockets(); networking != nullptr) {
|
||||
networking->CloseConnection(info->m_hConn, 0, nullptr, false);
|
||||
}
|
||||
g_clientConnection = k_HSteamNetConnection_Invalid;
|
||||
g_clientConnected = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
struct PolledEvent
|
||||
{
|
||||
co_gns_event event{};
|
||||
std::string payload;
|
||||
};
|
||||
|
||||
std::optional<PolledEvent> PollBridge(co_gns_server_handle server)
|
||||
{
|
||||
std::array<char, 64 * 1024> payload{};
|
||||
co_gns_event event{};
|
||||
const auto result = co_gns_server_poll(
|
||||
server,
|
||||
&event,
|
||||
payload.data(),
|
||||
static_cast<std::uint32_t>(payload.size()));
|
||||
assert(result >= 0);
|
||||
if (result == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
PolledEvent out{};
|
||||
out.event = event;
|
||||
if (event.type == CO_GNS_EVENT_MESSAGE) {
|
||||
out.payload.assign(payload.data(), event.payload_size);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <class Predicate>
|
||||
bool WaitUntil(co_gns_server_handle server, Predicate&& predicate, std::chrono::milliseconds timeout = 3s)
|
||||
{
|
||||
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
PollBridge(server);
|
||||
if (predicate()) {
|
||||
return true;
|
||||
}
|
||||
std::this_thread::sleep_for(2ms);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<PolledEvent> WaitForEvent(
|
||||
co_gns_server_handle server,
|
||||
std::uint32_t eventType,
|
||||
std::chrono::milliseconds timeout = 3s)
|
||||
{
|
||||
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
auto event = PollBridge(server);
|
||||
if (event && event->event.type == eventType) {
|
||||
return event;
|
||||
}
|
||||
std::this_thread::sleep_for(2ms);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> WaitForClientMessage(
|
||||
co_gns_server_handle server,
|
||||
ISteamNetworkingSockets* networking,
|
||||
std::chrono::milliseconds timeout = 3s)
|
||||
{
|
||||
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
PollBridge(server);
|
||||
ISteamNetworkingMessage* message = nullptr;
|
||||
const auto count = networking->ReceiveMessagesOnConnection(g_clientConnection, &message, 1);
|
||||
if (count > 0 && message != nullptr) {
|
||||
std::string payload;
|
||||
if (message->m_cbSize > 0 && message->m_pData != nullptr) {
|
||||
payload.assign(
|
||||
static_cast<const char*>(message->m_pData),
|
||||
static_cast<std::size_t>(message->m_cbSize));
|
||||
}
|
||||
message->Release();
|
||||
return payload;
|
||||
}
|
||||
assert(count >= 0);
|
||||
std::this_thread::sleep_for(2ms);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
co_gns_server_handle server = nullptr;
|
||||
std::array<char, 256> error{};
|
||||
assert(co_gns_server_create("127.0.0.1", 0, &server, error.data(), error.size()) == 1);
|
||||
assert(server != nullptr);
|
||||
const auto port = co_gns_server_local_port(server);
|
||||
assert(port != 0);
|
||||
|
||||
auto* networking = SteamNetworkingSockets();
|
||||
assert(networking != nullptr);
|
||||
SteamNetworkingIPAddr serverAddress{};
|
||||
serverAddress.Clear();
|
||||
serverAddress.SetIPv4(0x7f000001U, port);
|
||||
SteamNetworkingConfigValue_t option{};
|
||||
option.SetPtr(
|
||||
k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged,
|
||||
reinterpret_cast<void*>(+ClientConnectionStatusChanged));
|
||||
g_clientConnection = networking->ConnectByIPAddress(serverAddress, 1, &option);
|
||||
assert(g_clientConnection != k_HSteamNetConnection_Invalid);
|
||||
|
||||
const auto connectedEvent = WaitForEvent(server, CO_GNS_EVENT_CONNECTED);
|
||||
assert(connectedEvent.has_value());
|
||||
const auto serverConnectionId = connectedEvent->event.connection_id;
|
||||
assert(serverConnectionId != 0);
|
||||
assert(WaitUntil(server, []() { return g_clientConnected && !g_clientFailed; }));
|
||||
assert(co_gns_server_connection_count(server) == 1);
|
||||
|
||||
const std::string reliable = R"({"type":"playerState","characterName":"Nomad"})";
|
||||
assert(networking->SendMessageToConnection(
|
||||
g_clientConnection,
|
||||
reliable.data(),
|
||||
static_cast<std::uint32_t>(reliable.size()),
|
||||
k_nSteamNetworkingSend_ReliableNoNagle,
|
||||
nullptr) == k_EResultOK);
|
||||
const auto messageEvent = WaitForEvent(server, CO_GNS_EVENT_MESSAGE);
|
||||
assert(messageEvent.has_value());
|
||||
assert(messageEvent->event.connection_id == serverConnectionId);
|
||||
assert(messageEvent->payload == reliable);
|
||||
assert(messageEvent->payload.find('\n') == std::string::npos);
|
||||
|
||||
const std::string reliableResponse = R"({"type":"sessionReady","playerId":1})";
|
||||
assert(co_gns_server_send(
|
||||
server,
|
||||
serverConnectionId,
|
||||
reliableResponse.data(),
|
||||
static_cast<std::uint32_t>(reliableResponse.size()),
|
||||
CO_GNS_DELIVERY_RELIABLE_ORDERED) == CO_GNS_SEND_SENT);
|
||||
const auto clientReliable = WaitForClientMessage(server, networking);
|
||||
assert(clientReliable.has_value());
|
||||
assert(*clientReliable == reliableResponse);
|
||||
|
||||
const std::string snapshotResponse = R"({"type":"transform","snapshotSequence":8,"x":1})";
|
||||
assert(co_gns_server_send(
|
||||
server,
|
||||
serverConnectionId,
|
||||
snapshotResponse.data(),
|
||||
static_cast<std::uint32_t>(snapshotResponse.size()),
|
||||
CO_GNS_DELIVERY_UNRELIABLE_SEQUENCED) == CO_GNS_SEND_SENT);
|
||||
const auto clientSnapshot = WaitForClientMessage(server, networking);
|
||||
assert(clientSnapshot.has_value());
|
||||
assert(*clientSnapshot == snapshotResponse);
|
||||
|
||||
const std::string tooLarge((64 * 1024) + 1, 'x');
|
||||
assert(co_gns_server_send(
|
||||
server,
|
||||
serverConnectionId,
|
||||
tooLarge.data(),
|
||||
static_cast<std::uint32_t>(tooLarge.size()),
|
||||
CO_GNS_DELIVERY_RELIABLE_ORDERED) == CO_GNS_SEND_TOO_LARGE);
|
||||
assert(networking->SendMessageToConnection(
|
||||
g_clientConnection,
|
||||
tooLarge.data(),
|
||||
static_cast<std::uint32_t>(tooLarge.size()),
|
||||
k_nSteamNetworkingSend_ReliableNoNagle,
|
||||
nullptr) == k_EResultOK);
|
||||
const auto oversizeEvent = WaitForEvent(server, CO_GNS_EVENT_OVERSIZE_MESSAGE);
|
||||
assert(oversizeEvent.has_value());
|
||||
assert(oversizeEvent->event.connection_id == serverConnectionId);
|
||||
assert(oversizeEvent->event.payload_size == tooLarge.size());
|
||||
|
||||
assert(co_gns_server_disconnect(server, serverConnectionId, 1000, "test complete") == 1);
|
||||
assert(WaitUntil(server, []() { return g_clientConnection == k_HSteamNetConnection_Invalid; }));
|
||||
assert(!g_clientFailed);
|
||||
|
||||
co_gns_server_destroy(server);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user