Skip to content

Commit

Permalink
netplay/tcp: generalize checkSockets() function to be usable with v…
Browse files Browse the repository at this point in the history
…arious polling APIs

The patch introduces the following abstractions to make it easier to
switch between various polling APIs in the TCP_DIRECT netcode:

1. `IDescriptorSet` interface to abstract away the details of a
   particular polling API (`select()` and `poll()` are supported).
2. `PollEventType` enumeration to be used in concrete subclasses
   of `IDescriptorSet` to describe the type of events we are interested
   in, when polling a given descriptor set (generally speaking,
   both `select` and `poll` can listen for multiple types of events
   simultaneously, but in our particular case we listen for only
   one of them at a time).
3. `SelectDescriptorSet<PollEventType>` and
   `PollDescriptorSet<PollEventType>` descriptor set types which
   actually implement the support for `select` and `poll` APIs.
4. Helper function `tcp::pollImpl(descriptorSet, timeout)` for
   automatically retrying a polling operation upon encountering
   `EINTR` or `EAGAIN` signals.
5. `checkSocketsReadable()` function now makes direct use of
   `SelectDescriptorSet`(Windows, note on support below) and
   `PollDescriptorSet`(Linux/macOS/*BSD).

NOTE: We don't use `poll()`(`WSAPoll()`, to be accurate) on Windows
for the time being, since it's affected by a bug in Windows versions
prior to Windows 10 version 2004 (and there wasn't any prior analysis
on how many potential players will be affected):

`WSAPoll()` function can time out on socket connection errors instead
of returning an error early.

For more information on the bug, see: https://stackoverflow.com/questions/21653003/is-this-wsapoll-bug-for-non-blocking-sockets-fixed
and also https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsapoll#remarks

Signed-off-by: Pavel Solodovnikov <pavel.al.solodovnikov@gmail.com>
  • Loading branch information
ManManson committed Feb 8, 2025
1 parent 470140e commit 01d9051
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 36 deletions.
72 changes: 72 additions & 0 deletions lib/netplay/tcp/descriptor_set.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-2.0-or-later

/*
This file is part of Warzone 2100.
Copyright (C) 2025 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 "lib/framework/wzglobal.h"

#ifdef WZ_OS_WIN
# include <winsock2.h>
#elif defined(WZ_OS_UNIX)
using SOCKET = int;
#endif

#include <chrono>

namespace tcp
{

enum class PollEventType
{
READABLE,
WRITABLE
};

/// <summary>
/// Abstract class for describing polling descriptor sets used by various polling APIs (e.g. `select()` and `poll()`).
/// </summary>
class IDescriptorSet
{
public:

virtual ~IDescriptorSet() = default;

virtual void add(SOCKET fd) = 0;
virtual void clear() = 0;

/// <summary>
/// Polling algorithm implementation for this descriptor set kind.
/// Should represent the same semantics as `select()` or `poll()` APIs, i.e. after calling `pollImpl()` one
/// should check individual descriptors via `isSet()` to see, which of them were marked as ready.
/// </summary>
/// <param name="timeout">Timeout value in milliseconds</param>
/// <returns>
/// Error code from internal polling API, per `select()` or `poll()` API documentation:
/// * -1 (SOCKET_ERROR) on error
/// * 0 on timeout (none of the polling descriptors were ready by the end of an internal polling function call)
/// * <num_ready_fds> - positive number of ready descriptors
/// </returns>
virtual int pollImpl(std::chrono::milliseconds timeout) = 0;

virtual bool isSet(SOCKET fd) const = 0;
};

} // namespace tcp
71 changes: 35 additions & 36 deletions lib/netplay/tcp/netsocket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
#include <map>

#include "lib/netplay/zlib_compression_adapter.h"
#ifdef WZ_OS_WIN
# include "lib/netplay/tcp/select_descriptor_set.h"
#else
# include "lib/netplay/tcp/poll_descriptor_set.h"
#endif
#include "lib/netplay/tcp/polling_algo_impl.h"

#if defined(__clang__)
#pragma clang diagnostic ignored "-Wshorten-64-to-32" // FIXME!!
Expand Down Expand Up @@ -861,68 +867,61 @@ int checkSocketsReadable(const SocketSet& set, unsigned int timeout)
return 0;
}

#if defined(WZ_OS_UNIX)
SOCKET maxfd = INT_MIN;
#elif defined(WZ_OS_WIN)
SOCKET maxfd = 0;
#endif

bool compressedReady = false;
for (size_t i = 0; i < set.fds.size(); ++i)
for (const auto& socket : set.fds)
{
ASSERT(set.fds[i]->fd[SOCK_CONNECTION] != INVALID_SOCKET, "Invalid file descriptor!");
ASSERT(socket->fd[SOCK_CONNECTION] != INVALID_SOCKET, "Invalid file descriptor!");

if (set.fds[i]->isCompressed && !set.fds[i]->compressionAdapter.decompressionNeedInput())
if (socket->isCompressed && !socket->compressionAdapter.decompressionNeedInput())
{
compressedReady = true;
break;
}

maxfd = std::max(maxfd, set.fds[i]->fd[SOCK_CONNECTION]);
}

if (compressedReady)
{
// A socket already has some data ready. Don't really poll the sockets.

int ret = 0;
for (size_t i = 0; i < set.fds.size(); ++i)
for (auto& socket : set.fds)
{
set.fds[i]->ready = set.fds[i]->isCompressed && !set.fds[i]->compressionAdapter.decompressionNeedInput();
++ret;
socket->ready = socket->isCompressed && !socket->compressionAdapter.decompressionNeedInput();
}
return ret;
return set.fds.size();
}

int ret;
fd_set fds;
do
// For now, use `select()` on Windows instead of `poll()` because of a bug in
// Windows versions prior to "Windows 10 2004", which can lead to `poll()`
// function timing out on socket connection errors instead of returning an error early.
//
// For more information on the bug, see: https://stackoverflow.com/questions/21653003/is-this-wsapoll-bug-for-non-blocking-sockets-fixed
// and also https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsapoll#remarks
#ifdef WZ_OS_WIN
SelectDescriptorSet<PollEventType::READABLE> readableSet;
#else
PollDescriptorSet<PollEventType::READABLE> readableSet;
#endif
for (const auto& socket : set.fds)
{
struct timeval tv = {(int)(timeout / 1000), (int)(timeout % 1000) * 1000}; // Cast to int to avoid narrowing needed for C++11.

FD_ZERO(&fds);
for (size_t i = 0; i < set.fds.size(); ++i)
{
const SOCKET fd = set.fds[i]->fd[SOCK_CONNECTION];

FD_SET(fd, &fds);
}

ret = select(maxfd + 1, &fds, nullptr, nullptr, &tv);
readableSet.add(socket->fd[SOCK_CONNECTION]);
}
while (ret == SOCKET_ERROR && getSockErr() == EINTR);

const int ret = pollImpl(readableSet, std::chrono::milliseconds(timeout));

if (ret == SOCKET_ERROR)
{
debug(LOG_ERROR, "select failed: %s", strSockError(getSockErr()));
debug(LOG_ERROR, "poll failed: %s", strSockError(getSockErr()));
return SOCKET_ERROR;
}

for (size_t i = 0; i < set.fds.size(); ++i)
else if (ret == 0)
{
set.fds[i]->ready = FD_ISSET(set.fds[i]->fd[SOCK_CONNECTION], &fds);
debug(LOG_WARNING, "poll timed out after waiting for %d milliseconds", timeout);
return 0;
}

for (auto& socket : set.fds)
{
socket->ready = readableSet.isSet(socket->fd[SOCK_CONNECTION]);
}
return ret;
}

Expand Down
92 changes: 92 additions & 0 deletions lib/netplay/tcp/poll_descriptor_set.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: GPL-2.0-or-later

/*
This file is part of Warzone 2100.
Copyright (C) 2025 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 "descriptor_set.h"

#include "lib/framework/frame.h" // for MAX_PLAYERS

#ifdef WZ_OS_WIN
# include <winsock2.h>
#elif defined(WZ_OS_UNIX)
# include <poll.h> // for pollfd, poll
#endif

#include <array>
#include <stdexcept>

namespace tcp
{

/// <summary>
/// Descriptor set interface specialization using the `poll()` API for actual polling.
/// </summary>
/// <typeparam name="EventType">Type of updates (readable/writable sockets) to poll for.</typeparam>
template <PollEventType EventType>
class PollDescriptorSet : public IDescriptorSet
{
public:

explicit PollDescriptorSet() = default;

virtual void add(SOCKET fd) override
{
constexpr short evt = EventType == PollEventType::READABLE ? POLLIN : POLLOUT;

assert(size_ < MAX_PLAYERS);
if (size_ >= MAX_PLAYERS)
{
throw std::runtime_error("Too many poll descriptors (>= MAX_PLAYERS)");
}
fds_[size_++] = { fd, evt, 0 };
}

virtual void clear() override
{
fds_.assign({});

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / Fedora :LATEST [GCC]

'struct std::array<pollfd, 11>' has no member named 'assign'

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / Alpine :LATEST [GCC]

'struct std::array<pollfd, 11>' has no member named 'assign'

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / Arch :LATEST [Clang]

no member named 'assign' in 'std::array<pollfd, 11>'

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / Arch :LATEST [GCC]

‘struct std::array<pollfd, 11>’ has no member named ‘assign’

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / Fedora :LATEST [GCC -m32]

'struct std::array<pollfd, 11>' has no member named 'assign'

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / wasm32

no member named 'assign' in 'std::array<pollfd, 11>'

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / Ubuntu 22.04 [Clang]

no member named 'assign' in 'std::array<pollfd, 11>'

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / Ubuntu 22.04 [GCC]

'struct std::array<pollfd, 11>' has no member named 'assign'

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / Ubuntu 24.04 [Clang]

no member named 'assign' in 'std::array<pollfd, 11>'

Check failure on line 65 in lib/netplay/tcp/poll_descriptor_set.h

View workflow job for this annotation

GitHub Actions / Ubuntu 20.04 [GCC]

'struct std::array<pollfd, 11>' has no member named 'assign'
size_ = 0;
}

virtual int pollImpl(std::chrono::milliseconds timeout) override
{
#ifdef WZ_OS_WIN
return WSAPoll(fds_.data(), size_, timeout.count());
#else
return poll(fds_.data(), size_, timeout.count());
#endif
}

virtual bool isSet(SOCKET fd) const override
{
constexpr short evt = EventType == PollEventType::READABLE ? POLLIN : POLLOUT;

const auto it = std::find_if(fds_.begin(), fds_.end(), [fd](const pollfd& pfd) { return pfd.fd == fd; });
return it != fds_.end() && (it->revents & evt);
}

private:

std::array<pollfd, MAX_PLAYERS> fds_;
size_t size_ = 0;
};

} // namespace tcp
48 changes: 48 additions & 0 deletions lib/netplay/tcp/polling_algo_impl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: GPL-2.0-or-later

/*
This file is part of Warzone 2100.
Copyright (C) 2025 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 "polling_algo_impl.h"
#include "descriptor_set.h"

#ifndef WZ_OS_WIN
static const int SOCKET_ERROR = -1;
#else
# include <winsock2.h> // for SOCKET_ERROR
#endif

namespace tcp
{

int pollImpl(IDescriptorSet& descriptorSet, std::chrono::milliseconds timeout)
{
int ret;
do
{
ret = descriptorSet.pollImpl(timeout);
if (ret == SOCKET_ERROR)
{
return SOCKET_ERROR;
}
} while (errno == EINTR || errno == EAGAIN);

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Fedora :LATEST [GCC]

'errno' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Fedora :LATEST [GCC]

'EINTR' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Fedora :LATEST [GCC]

'EAGAIN' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Alpine :LATEST [GCC]

'errno' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Alpine :LATEST [GCC]

'EINTR' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Alpine :LATEST [GCC]

'EAGAIN' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Arch :LATEST [Clang]

use of undeclared identifier 'errno'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Arch :LATEST [Clang]

use of undeclared identifier 'EINTR'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Arch :LATEST [Clang]

use of undeclared identifier 'errno'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Arch :LATEST [Clang]

use of undeclared identifier 'EAGAIN'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Arch :LATEST [GCC]

‘errno’ was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Arch :LATEST [GCC]

‘EINTR’ was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Arch :LATEST [GCC]

‘EAGAIN’ was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Fedora :LATEST [GCC -m32]

'errno' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Fedora :LATEST [GCC -m32]

'EINTR' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Fedora :LATEST [GCC -m32]

'EAGAIN' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 22.04 [Clang]

use of undeclared identifier 'errno'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 22.04 [Clang]

use of undeclared identifier 'EINTR'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 22.04 [Clang]

use of undeclared identifier 'errno'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 22.04 [Clang]

use of undeclared identifier 'EAGAIN'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 22.04 [GCC]

'errno' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 22.04 [GCC]

'EINTR' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 22.04 [GCC]

'EAGAIN' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 24.04 [Clang]

use of undeclared identifier 'errno'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 24.04 [Clang]

use of undeclared identifier 'EINTR'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 24.04 [Clang]

use of undeclared identifier 'errno'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 24.04 [Clang]

use of undeclared identifier 'EAGAIN'

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 20.04 [GCC]

'errno' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 20.04 [GCC]

'EINTR' was not declared in this scope

Check failure on line 44 in lib/netplay/tcp/polling_algo_impl.cpp

View workflow job for this annotation

GitHub Actions / Ubuntu 20.04 [GCC]

'EAGAIN' was not declared in this scope
return ret;
}

} // namespace tcp
42 changes: 42 additions & 0 deletions lib/netplay/tcp/polling_algo_impl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-2.0-or-later

/*
This file is part of Warzone 2100.
Copyright (C) 2025 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 "descriptor_set.h"

#include <chrono>

namespace tcp
{

class IDescriptorImpl;

/// <summary>
/// Helper function to call into the internal polling API via given descriptor set.
/// Automatically retries upon encountering `EAGAIN` and `EINTR` signals.
/// </summary>
/// <param name="descriptorSet"></param>
/// <param name="timeout"></param>
/// <returns></returns>
int pollImpl(IDescriptorSet& descriptorSet, std::chrono::milliseconds timeout);

} // namespace tcp
Loading

0 comments on commit 01d9051

Please sign in to comment.