Skip to content

Commit

Permalink
netplay: introduce abstractions for client/server-side sockets and co…
Browse files Browse the repository at this point in the history
…nnection providers

This patch introduces multiple high-level abstractions over raw low-level
sockets, which are necessary for supporting network backends other
than default legacy `TCP_DIRECT` implementation:

1. `WzConnectionProvider` - abstracts the way WZ establishes
   server-side and client-side connections. This thing effectively
   provides usable listen sockets and client connections to work with,
   hence the name.
2. `IListenSocket` - abstraction over listen sockets.
3. `IClientConnection` - abstraction over client-side sockets (and
   also server-side connections to the game clients).
4. `IConnectionPollGroup` - generalization of socket sets for polling
   multiple connections in one go.
5. `ConnectionProviderRegistry` - trivial singleton class providing
   storage for connection providers.
6. `ConnectionAddress` - opaque connection address object, aimed
   to replace direct uses of `addrinfo` and provide a bit more
   abstract way to represent connection credentials. Still looks like
   a crutch right now, but it's better than nothing, nonetheless.

The existing implementation in `netplay/netsocket.h(.cpp)` has been
moved to the `tcp` subfolder and wrapped entirely into the `tcp`
namespace.

The patch provides `TCP*`-prefixed implementations of the base
interfaces mentioned above, which are implemented in terms of the
old `netsocket` code.

There's now a `ConnectionProviderType::TCP_DIRECT` enumeration
descriptor for accessing the default connection provider.

All uses in the high-level code (`netplay.cpp`, `joiningscreen.cpp`)
are amended appropriately to use the all-new high-level abstractions
instead of old low-level tcp-specific `Socket` and `SocketSet`.

NOTE: there are still a few functions from the `tcp::` namespace
used directly in the Discord RPC integration code, but these
shouldn't pose any problem to either extract these into a more generic
abstraction layer or to be rewritten not to use these functions at all,
because they don't actually use any low-level stuff that's hard to
refactor.

Signed-off-by: Pavel Solodovnikov <[email protected]>
  • Loading branch information
ManManson committed Oct 25, 2024
1 parent 4468c15 commit 5353467
Show file tree
Hide file tree
Showing 29 changed files with 1,482 additions and 258 deletions.
12 changes: 10 additions & 2 deletions lib/netplay/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,16 @@ add_dependencies(autorevision_netcodeversion autorevision) # Ensure ordering and
############################
# netplay library

file(GLOB HEADERS "*.h")
file(GLOB SRC "*.cpp")
file(GLOB_RECURSE HEADERS "*.h")
file(GLOB_RECURSE SRC "*.cpp")

if(MSVC AND CMAKE_VERSION VERSION_GREATER 3.7)
# Automatic detection of source groups via `source_group(TREE <root>)` syntax
# has been introduced in CMake 3.8.
# Please consult https://cmake.org/cmake/help/latest/command/source_group.html for additional info.
source_group(TREE "${CMAKE_CURRENT_LIST_DIR}" PREFIX "Sources" FILES ${SRC})
source_group(TREE "${CMAKE_CURRENT_LIST_DIR}" PREFIX "Headers" FILES ${HEADERS})
endif()

find_package (Threads REQUIRED)
find_package (ZLIB REQUIRED)
Expand Down
50 changes: 50 additions & 0 deletions lib/netplay/byteorder_funcs_wrapper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
This file is part of Warzone 2100.
Copyright (C) 1999-2004 Eidos Interactive
Copyright (C) 2005-2024 Warzone 2100 Project
Warzone 2100 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Warzone 2100 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Warzone 2100; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "lib/netplay/byteorder_funcs_wrapper.h"

#include "lib/framework/wzglobal.h"

// bring in the original `htonl`/`htons`/`ntohs`/`htohl` functions
#if defined WZ_OS_WIN
# include <winsock.h>
#else // *NIX / *BSD variants
# include <arpa/inet.h>
#endif

uint32_t wz_htonl(uint32_t hostlong)
{
return htonl(hostlong);
}

uint16_t wz_htons(uint16_t hostshort)
{
return htons(hostshort);
}

uint32_t wz_ntohl(uint32_t netlong)
{
return ntohl(netlong);
}

uint16_t wz_ntohs(uint16_t netshort)
{
return ntohs(netshort);
}
34 changes: 34 additions & 0 deletions lib/netplay/byteorder_funcs_wrapper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
This file is part of Warzone 2100.
Copyright (C) 1999-2004 Eidos Interactive
Copyright (C) 2005-2024 Warzone 2100 Project
Warzone 2100 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Warzone 2100 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Warzone 2100; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#pragma once

#include <stdint.h>

///
/// byteorder functions wrappers for WZ just to avoid polluting all places,
/// where these functions are needed, with conditional includes of <arpa/inet.h>
/// and winsock headers.
///

uint32_t wz_htonl(uint32_t hostlong);
uint16_t wz_htons(uint16_t hostshort);
uint32_t wz_ntohl(uint32_t netlong);
uint16_t wz_ntohs(uint16_t netshort);
120 changes: 120 additions & 0 deletions lib/netplay/client_connection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
This file is part of Warzone 2100.
Copyright (C) 2024 Warzone 2100 Project
Warzone 2100 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Warzone 2100 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Warzone 2100; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#pragma once

#include <string>
#include <stddef.h>

#include "lib/framework/types.h" // bring in `ssize_t` for MSVC
#include "lib/netplay/net_result.h"

/// <summary>
/// Basic abstraction over client connection sockets.
///
/// These are capable of reading (`readAll` and `readNoInt`) and
/// writing data (via `writeAll()` + `flush()` combination).
///
/// The internal implementation may also implement advanced compression mechanisms
/// on top of these connections by providing non-trivial `enableCompression()` overload.
///
/// In this case, `writeAll()` should somehow accumulate the data into a write queue,
/// compressing the outcoming data on-the-fly; and `flush()` should empty the write queue
/// and actually post a message to the transmission queue, which, in turn, will be emptied
/// by the internal connection interface in a timely manner, when there are enough messages
/// to be sent over the network.
/// </summary>
class IClientConnection
{
public:

virtual ~IClientConnection() = default;

/// <summary>
/// Read exactly `size` bytes into `buf` buffer.
/// Supports setting a timeout value in milliseconds.
/// </summary>
/// <param name="buf">Destination buffer to read the data into.</param>
/// <param name="size">The size of data to be read in bytes.</param>
/// <param name="timeout">Timeout value in milliseconds.</param>
/// <returns>On success, returns the number of bytes read;
/// On failure, returns an `std::error_code` (having `GenericSystemErrorCategory` error category)
/// describing the actual error.</returns>
virtual net::result<ssize_t> readAll(void* buf, size_t size, unsigned timeout) = 0;
/// <summary>
/// Reads at most `max_size` bytes into `buf` buffer.
/// Raw count of bytes (after compression) is returned in `rawByteCount`.
/// </summary>
/// <param name="buf">Destination buffer to read the data into.</param>
/// <param name="max_size">The maximum number of bytes to read from the client socket.</param>
/// <param name="rawByteCount">Output parameter: Raw count of bytes (after compression).</param>
/// <returns>On success, returns the number of bytes read;
/// On failure, returns an `std::error_code` (having `GenericSystemErrorCategory` error category)
/// describing the actual error.</returns>
virtual net::result<ssize_t> readNoInt(void* buf, size_t max_size, size_t* rawByteCount) = 0;
/// <summary>
/// Nonblocking write of `size` bytes to the socket. The data will be written to a
/// separate write queue in asynchronous manner, possibly by a separate thread.
/// Raw count of bytes (after compression) will be returned in `rawByteCount`, which
/// will often be 0 until the socket is flushed.
///
/// The reason for this method to be async is that in some cases we want
/// client connections to have compression mechanism enabled. This naturally
/// introduces the 2-phase write process, which involves a write queue (accumulating
/// the data for compression on-the-fly) and a submission (transmission)
/// queue (for transmitting of compressed and assembled messages),
/// which is managed by the network backend implementation.
/// </summary>
/// <param name="buf">Source buffer to read the data from.</param>
/// <param name="size">The number of bytes to write to the socket.</param>
/// <param name="rawByteCount">Output parameter: raw count of bytes (after compression) written.</param>
/// <returns>The total number of bytes written.</returns>
virtual net::result<ssize_t> writeAll(const void* buf, size_t size, size_t* rawByteCount) = 0;
/// <summary>
/// This method indicates whether the socket has some data ready to be read (i.e.
/// whether the next `readAll/readNoInt` operation will execute without blocking or not).
/// </summary>
virtual bool readReady() const = 0;
/// <summary>
/// Actually sends the data written with `writeAll()`. Only useful with sockets
/// which have compression enabled.
/// Note that flushing too often makes compression less effective.
/// Raw count of bytes (after compression) is returned in `rawByteCount`.
/// </summary>
/// <param name="rawByteCount">Raw count of bytes (after compression) as written
/// to the submission queue by the flush operation.</param>
virtual void flush(size_t* rawByteCount) = 0;
/// <summary>
/// Enables compression for the current socket.
///
/// This makes all subsequent write operations asynchronous, plus
/// the written data will need to be flushed explicitly at some point.
/// </summary>
virtual void enableCompression() = 0;
/// <summary>
/// Enables or disables the use of Nagle algorithm for the socket.
///
/// For direct TCP connections this is equivalent to setting `TCP_NODELAY` to the
/// appropriate value (i.e.:
/// `enable == true` <=> `TCP_NODELAY == false`;
/// `enable == false` <=> `TCP_NODELAY == true`).
/// </summary>
virtual void useNagleAlgorithm(bool enable) = 0;
/// <summary>
/// Returns textual representation of the socket's connection address.
/// </summary>
virtual std::string textAddress() const = 0;
};
65 changes: 65 additions & 0 deletions lib/netplay/connection_address.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
This file is part of Warzone 2100.
Copyright (C) 2024 Warzone 2100 Project
Warzone 2100 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Warzone 2100 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Warzone 2100; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "lib/netplay/connection_address.h"
#include "lib/netplay/tcp/netsocket.h" // for `resolveHost`

#include "lib/framework/frame.h" // for `ASSERT`

struct ConnectionAddress::Impl final
{
explicit Impl(SocketAddress* addr)
: mAddr_(addr)
{}

~Impl()
{
ASSERT(mAddr_ != nullptr, "Invalid addrinfo stored in the connection address");
freeaddrinfo(mAddr_);
}

SocketAddress* mAddr_;
};

ConnectionAddress::ConnectionAddress() = default;
ConnectionAddress::ConnectionAddress(ConnectionAddress&&) = default;
ConnectionAddress::~ConnectionAddress() = default;

const SocketAddress* ConnectionAddress::asRawSocketAddress() const
{
return mPimpl_->mAddr_;
}


net::result<ConnectionAddress> ConnectionAddress::parse(const char* hostname, uint16_t port)
{
ConnectionAddress res;
const auto addr = tcp::resolveHost(hostname, port);
if (!addr.has_value())
{
return tl::make_unexpected(addr.error());
}
res.mPimpl_ = std::make_unique<Impl>(addr.value());
return net::result<ConnectionAddress>{std::move(res)};
}

net::result<ConnectionAddress> ConnectionAddress::parse(const std::string& hostname, uint16_t port)
{
return parse(hostname.c_str(), port);
}
72 changes: 72 additions & 0 deletions lib/netplay/connection_address.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
This file is part of Warzone 2100.
Copyright (C) 2024 Warzone 2100 Project
Warzone 2100 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Warzone 2100 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Warzone 2100; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#pragma once

#include <stdint.h>

#include <memory>
#include <string>

#include "lib/netplay/net_result.h"

#if defined WZ_OS_UNIX
# include <netdb.h>
#elif defined WZ_OS_WIN
# include <ws2tcpip.h>
#endif

typedef struct addrinfo SocketAddress;

/// <summary>
/// Opaque class representing abstract connection address to use with various
/// network backend implementations. The internal representation is made
/// hidden on purpose since we don't want to actually leak internal data layout
/// to clients.
///
/// Instead, we would like to introduce "conversion routines" yielding
/// various representations for convenient consumption with various network
/// backends.
///
/// NOTE: this class may or may not represent a chain of resolved network addresses
/// instead of just a single one, much like a `addrinfo` structure.
///
/// Currently, only knows how to convert itself to `addrinfo` struct,
/// which is used with the `TCP_DIRECT` network backend.
///
/// New conversion routines should be introduced for other network backends,
/// if deemed necessary.
/// </summary>
class ConnectionAddress
{
public:

ConnectionAddress();
ConnectionAddress(ConnectionAddress&&);
ConnectionAddress(const ConnectionAddress&) = delete;
~ConnectionAddress();

static net::result<ConnectionAddress> parse(const char* hostname, uint16_t port);
static net::result<ConnectionAddress> parse(const std::string& hostname, uint16_t port);

// NOTE: The lifetime of the returned `addrinfo` struct is bounded by the parent object's lifetime!
const SocketAddress* asRawSocketAddress() const;

private:

struct Impl;
std::unique_ptr<Impl> mPimpl_;
};
Loading

0 comments on commit 5353467

Please sign in to comment.