Skip to content

Commit e9c46b3

Browse files
committed
fix(TCPServer): use poll() instead of select() to remove FD_SETSIZE limit
select() cannot watch file descriptors whose number is >= FD_SETSIZE (1024 on glibc). When the hosting process holds many descriptors (e.g. a JVM such as MATLAB's), accepted socket FDs exceed that limit; the previous code either rejected the connection (the set_size_exceeded guard added for select()) or risked the "bit out of range" fd_set crash. Replace the select()/fd_set machinery in TCPServer with poll() (WSAPoll() on Windows), which has no FD_SETSIZE limitation. The pollfd set is rebuilt each spin() from the listen socket plus client_fds_ (already tracked), so the masterfds_/tempfds_/maxfd_ members and the FD_SET/FD_CLR/FD_ZERO/FD_ISSET and set_size_exceeded code are removed entirely. tests: add two TCPServer regression tests - services_client_with_high_fd_number (POSIX): consumes low FDs so the accepted client FD exceeds FD_SETSIZE, then asserts connect, bidirectional data and disconnect all work. Fails on select(), passes on poll(). - receives_from_many_concurrent_clients: many clients send simultaneously; asserts the server observes activity on every client FD (guards the poll() revents loop).
1 parent b940273 commit e9c46b3

3 files changed

Lines changed: 168 additions & 59 deletions

File tree

include/ur_client_library/comm/tcp_server.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,6 @@ class TCPServer
223223
std::atomic<socket_t> listen_fd_;
224224
int port_;
225225

226-
socket_t maxfd_;
227-
fd_set masterfds_;
228-
fd_set tempfds_;
229-
230226
uint32_t max_clients_allowed_;
231227
std::vector<socket_t> client_fds_;
232228
std::mutex clients_mutex_;

src/comm/tcp_server.cpp

Lines changed: 34 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,19 @@
3434

3535
#include <sstream>
3636
#include <cstring>
37+
#include <vector>
3738
#include "ur_client_library/comm/socket_t.h"
3839
#include <fcntl.h>
40+
#ifndef _WIN32
41+
# include <poll.h>
42+
#endif
3943

4044
namespace urcl
4145
{
4246
namespace comm
4347
{
4448
TCPServer::TCPServer(const int port, const size_t max_num_tries, const std::chrono::milliseconds reconnection_time)
45-
: port_(port), maxfd_(0), max_clients_allowed_(0)
49+
: port_(port), max_clients_allowed_(0)
4650
{
4751
#ifdef _WIN32
4852
WSAData data;
@@ -74,9 +78,6 @@ void TCPServer::init()
7478
ur_setsockopt(listen_fd_, SOL_SOCKET, SO_KEEPALIVE, &flag, sizeof(int));
7579

7680
URCL_LOG_DEBUG("Created socket with FD %d", (int)listen_fd_);
77-
78-
FD_ZERO(&masterfds_);
79-
FD_ZERO(&tempfds_);
8081
}
8182

8283
void TCPServer::shutdown()
@@ -179,9 +180,6 @@ void TCPServer::bind(const size_t max_num_tries, const std::chrono::milliseconds
179180
} while (err == -1 && (connection_counter <= max_num_tries || max_num_tries == 0));
180181

181182
URCL_LOG_DEBUG("Bound %d:%d to FD %d", server_addr.sin_addr.s_addr, port_, (int)listen_fd_);
182-
183-
FD_SET(listen_fd_, &masterfds_);
184-
maxfd_ = listen_fd_;
185183
}
186184

187185
void TCPServer::startListen()
@@ -220,33 +218,13 @@ void TCPServer::handleConnect()
220218
return;
221219
}
222220

223-
#ifdef _WIN32
224-
bool set_size_exceeded = client_fds_.size() >= FD_SETSIZE - 1; // -1 because listen_fd_ also occupies one
225-
// slot in masterfds_
226-
#else
227-
bool set_size_exceeded = client_fd >= FD_SETSIZE; // On Unix-like systems, the client FD itself must be less than
228-
// FD_SETSIZE, otherwise it cannot be added to the fd_set.
229-
#endif
230-
231-
if (set_size_exceeded)
232-
{
233-
URCL_LOG_ERROR("Accepted client FD %d exceeds FD_SETSIZE (%d). Closing connection.", (int)client_fd, FD_SETSIZE);
234-
ur_close(client_fd);
235-
return;
236-
}
237-
238221
bool accepted = false;
239222

240223
{
241224
std::lock_guard<std::mutex> lk(clients_mutex_);
242225
if (client_fds_.size() < max_clients_allowed_ || max_clients_allowed_ == 0)
243226
{
244227
client_fds_.push_back(client_fd);
245-
FD_SET(client_fd, &masterfds_);
246-
if (client_fd > maxfd_)
247-
{
248-
maxfd_ = client_fd;
249-
}
250228
accepted = true;
251229
}
252230
else
@@ -268,27 +246,40 @@ void TCPServer::handleConnect()
268246

269247
void TCPServer::spin()
270248
{
271-
tempfds_ = masterfds_;
272-
273-
timeval timeout;
274-
timeout.tv_sec = 1;
275-
timeout.tv_usec = 0;
249+
// Build the poll set fresh each iteration from the listen socket plus all currently connected
250+
// clients. poll() is used on both platforms (WSAPoll() on Windows) because it has no
251+
// FD_SETSIZE limit on file descriptor numbers, unlike select(). This matters when the hosting
252+
// process holds many file descriptors (e.g. a JVM), pushing socket FDs past FD_SETSIZE (1024).
253+
std::vector<struct pollfd> pollfds;
254+
pollfds.push_back({ static_cast<socket_t>(listen_fd_), POLLIN, 0 });
255+
{
256+
std::lock_guard<std::mutex> lk(clients_mutex_);
257+
for (const auto& client_fd : client_fds_)
258+
{
259+
pollfds.push_back({ client_fd, POLLIN, 0 });
260+
}
261+
}
276262

277-
// blocks until activity on any socket from tempfds
278-
int sel = select(static_cast<int>(maxfd_ + 1), &tempfds_, NULL, NULL, &timeout);
279-
if (sel < 0)
263+
// Block for up to 1 s waiting for activity on any socket. A shutdown wakes this immediately by
264+
// connecting to the listen socket (see shutdown()).
265+
#ifdef _WIN32
266+
int ready = ::WSAPoll(pollfds.data(), static_cast<ULONG>(pollfds.size()), 1000);
267+
#else
268+
int ready = ::poll(pollfds.data(), pollfds.size(), 1000);
269+
#endif
270+
if (ready < 0)
280271
{
281-
URCL_LOG_ERROR("select() failed. Shutting down socket event handler.");
272+
URCL_LOG_ERROR("poll() failed. Shutting down socket event handler.");
282273
keep_running_ = false;
283274
return;
284275
}
285276

286-
if (!keep_running_ || sel == 0)
277+
if (!keep_running_ || ready == 0)
287278
{
288279
return;
289280
}
290281

291-
if (FD_ISSET(listen_fd_, &tempfds_))
282+
if (pollfds[0].revents & POLLIN)
292283
{
293284
URCL_LOG_DEBUG("Activity on listen FD %d", (int)listen_fd_);
294285
handleConnect();
@@ -297,15 +288,13 @@ void TCPServer::spin()
297288
std::vector<socket_t> disconnected_clients;
298289
std::vector<socket_t> client_fds_with_activity;
299290

291+
// pollfds[0] is the listen socket; client entries start at index 1.
292+
for (size_t i = 1; i < pollfds.size(); ++i)
300293
{
301-
std::lock_guard<std::mutex> lk(clients_mutex_);
302-
for (const auto& client_fd : client_fds_)
294+
if (pollfds[i].revents & (POLLIN | POLLHUP | POLLERR))
303295
{
304-
if (FD_ISSET(client_fd, &tempfds_))
305-
{
306-
URCL_LOG_DEBUG("Activity on client FD %d", (int)client_fd);
307-
client_fds_with_activity.push_back(client_fd);
308-
}
296+
URCL_LOG_DEBUG("Activity on client FD %d", (int)pollfds[i].fd);
297+
client_fds_with_activity.push_back(static_cast<socket_t>(pollfds[i].fd));
309298
}
310299
}
311300
// We handle client activity outside the clients_mutex_ lock to avoid holding it during potentially slow I/O and
@@ -331,7 +320,6 @@ void TCPServer::handleDisconnect(const socket_t fd)
331320
{
332321
std::lock_guard<std::mutex> lk(clients_mutex_);
333322
ur_close(fd);
334-
FD_CLR(fd, &masterfds_);
335323

336324
for (size_t i = 0; i < client_fds_.size(); ++i)
337325
{
@@ -341,15 +329,6 @@ void TCPServer::handleDisconnect(const socket_t fd)
341329
break;
342330
}
343331
}
344-
345-
maxfd_ = listen_fd_;
346-
for (const auto& client_fd : client_fds_)
347-
{
348-
if (client_fd > maxfd_)
349-
{
350-
maxfd_ = client_fd;
351-
}
352-
}
353332
}
354333

355334
{

tests/test_tcp_server.cpp

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@
3434
#include <condition_variable>
3535
#include <chrono>
3636
#include <memory>
37+
#include <mutex>
3738
#include <thread>
3839
#include <vector>
40+
#ifndef _WIN32
41+
# include <fcntl.h>
42+
# include <unistd.h>
43+
# include <sys/resource.h>
44+
#endif
3945
#include "test_utils.h"
4046

4147
#include <ur_client_library/comm/tcp_server.h>
@@ -497,6 +503,134 @@ TEST_F(TCPServerTest, shutdown_during_active_writes)
497503
writer.join();
498504
}
499505

506+
// Verifies that the server receives data from many clients that all send simultaneously. This
507+
// exercises the poll() revents loop across many client file descriptors and guards against
508+
// missed read events when several sockets are readable at once.
509+
TEST_F(TCPServerTest, receives_from_many_concurrent_clients)
510+
{
511+
comm::TCPServer server(0);
512+
513+
std::mutex mtx;
514+
std::condition_variable cv;
515+
std::atomic<int> message_count{ 0 };
516+
517+
server.setMessageCallback([&](const socket_t, char*, int) {
518+
message_count.fetch_add(1);
519+
std::lock_guard<std::mutex> lk(mtx);
520+
cv.notify_all();
521+
});
522+
server.start();
523+
524+
#ifdef _WIN32
525+
// Windows allows a maximum of 64 sockets per process by default.
526+
constexpr int num_clients = 50;
527+
#else
528+
constexpr int num_clients = 100;
529+
#endif
530+
531+
std::vector<std::unique_ptr<Client>> clients;
532+
for (int i = 0; i < num_clients; ++i)
533+
{
534+
clients.push_back(std::make_unique<Client>(server.getPort()));
535+
}
536+
537+
// Every client sends a single message concurrently.
538+
std::vector<std::thread> senders;
539+
for (auto& client : clients)
540+
{
541+
senders.emplace_back([&client]() { client->send("ping\n"); });
542+
}
543+
for (auto& t : senders)
544+
{
545+
t.join();
546+
}
547+
548+
// The server's poll() loop must observe activity on every client FD and deliver all messages.
549+
std::unique_lock<std::mutex> lk(mtx);
550+
EXPECT_TRUE(cv.wait_for(lk, std::chrono::seconds(5), [&]() { return message_count.load() >= num_clients; }));
551+
EXPECT_EQ(message_count.load(), num_clients);
552+
}
553+
554+
#ifndef _WIN32
555+
// Regression test for the FD_SETSIZE limitation of select(): a client whose accepted socket file
556+
// descriptor number is >= FD_SETSIZE (1024) must still be serviced normally. This is the exact
557+
// scenario that occurs when the hosting process (e.g. a JVM) holds many file descriptors. The old
558+
// select()-based implementation rejected/crashed on such descriptors; poll() handles them.
559+
TEST_F(TCPServerTest, services_client_with_high_fd_number)
560+
{
561+
// Make sure we are allowed to open more than FD_SETSIZE descriptors; raise the soft limit if
562+
// needed and skip the test if the hard limit does not allow it.
563+
struct rlimit rl;
564+
ASSERT_EQ(getrlimit(RLIMIT_NOFILE, &rl), 0);
565+
const rlim_t needed = static_cast<rlim_t>(FD_SETSIZE) + 64;
566+
if (rl.rlim_cur < needed)
567+
{
568+
rl.rlim_cur = std::min<rlim_t>(needed, rl.rlim_max);
569+
if (setrlimit(RLIMIT_NOFILE, &rl) != 0 || rl.rlim_cur < needed)
570+
{
571+
GTEST_SKIP() << "Cannot raise RLIMIT_NOFILE above FD_SETSIZE; skipping high-fd test.";
572+
}
573+
}
574+
575+
// Consume the low-numbered descriptors so that subsequently created sockets are assigned fd
576+
// numbers beyond FD_SETSIZE.
577+
std::vector<int> fd_hogs;
578+
while (true)
579+
{
580+
int fd = ::open("/dev/null", O_RDONLY);
581+
if (fd < 0)
582+
{
583+
break;
584+
}
585+
fd_hogs.push_back(fd);
586+
if (fd > static_cast<int>(FD_SETSIZE) + 8)
587+
{
588+
break;
589+
}
590+
}
591+
const bool pushed_past_limit = !fd_hogs.empty() && fd_hogs.back() > static_cast<int>(FD_SETSIZE);
592+
if (!pushed_past_limit)
593+
{
594+
for (int fd : fd_hogs)
595+
{
596+
::close(fd);
597+
}
598+
GTEST_SKIP() << "Could not allocate descriptors beyond FD_SETSIZE; skipping.";
599+
}
600+
601+
TestableTcpServer server(port_);
602+
server.start();
603+
604+
Client client(port_);
605+
EXPECT_TRUE(server.waitForConnectionCallback(2000));
606+
607+
// The server-side accepted client FD should exceed FD_SETSIZE -- the case that breaks select().
608+
auto client_fds = server.getClientFDs();
609+
ASSERT_FALSE(client_fds.empty());
610+
EXPECT_GT(client_fds.back(), static_cast<socket_t>(FD_SETSIZE));
611+
612+
// Data must flow both ways on the high-numbered descriptor.
613+
const std::string message = "high fd message\n";
614+
client.send(message);
615+
EXPECT_TRUE(server.waitForMessageCallback(2000));
616+
EXPECT_EQ(server.getReceivedMessage(), message);
617+
618+
size_t written;
619+
const auto* data = reinterpret_cast<const uint8_t*>(message.c_str());
620+
ASSERT_TRUE(server.write(data, message.size(), written));
621+
EXPECT_EQ(client.recv(), message);
622+
623+
// Disconnect must also be detected on the high-numbered descriptor.
624+
client.close();
625+
EXPECT_TRUE(server.waitForDisconnectionCallback(2000));
626+
627+
for (int fd : fd_hogs)
628+
{
629+
::close(fd);
630+
}
631+
}
632+
#endif
633+
500634
int main(int argc, char* argv[])
501635
{
502636
::testing::InitGoogleTest(&argc, argv);

0 commit comments

Comments
 (0)