Skip to content

Commit 9ecdf5c

Browse files
authored
Added timeout for TCP calls such as connect, send, recv (#3432)
* Now lets test on windows * I guess testing on windows passes. * Update tcp_client-windows.h Added default value to argument * Final edit * Update tcp_client-windows.h Changed improper misplaced includes.
1 parent 737347d commit 9ecdf5c

File tree

3 files changed

+181
-11
lines changed

3 files changed

+181
-11
lines changed

include/spdlog/details/tcp_client-windows.h

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,82 @@ class tcp_client {
5757
}
5858

5959
SOCKET fd() const { return socket_; }
60+
61+
int connect_socket_with_timeout(SOCKET sockfd,
62+
const struct sockaddr *addr,
63+
int addrlen,
64+
const timeval &tv) {
65+
// If no timeout requested, do a normal blocking connect.
66+
if (tv.tv_sec == 0 && tv.tv_usec == 0) {
67+
int rv = ::connect(sockfd, addr, addrlen);
68+
if (rv == SOCKET_ERROR && WSAGetLastError() == WSAEISCONN) {
69+
return 0;
70+
}
71+
return rv;
72+
}
73+
74+
// Switch to non‐blocking mode
75+
u_long mode = 1UL;
76+
if (::ioctlsocket(sockfd, FIONBIO, &mode) == SOCKET_ERROR) {
77+
return SOCKET_ERROR;
78+
}
79+
80+
int rv = ::connect(sockfd, addr, addrlen);
81+
int last_error = WSAGetLastError();
82+
if (rv == 0 || last_error == WSAEISCONN) {
83+
mode = 0UL;
84+
if (::ioctlsocket(sockfd, FIONBIO, &mode) == SOCKET_ERROR) {
85+
return SOCKET_ERROR;
86+
}
87+
return 0;
88+
}
89+
if (last_error != WSAEWOULDBLOCK) {
90+
// Real error
91+
mode = 0UL;
92+
if (::ioctlsocket(sockfd, FIONBIO, &mode)) {
93+
return SOCKET_ERROR;
94+
}
95+
return SOCKET_ERROR;
96+
}
97+
98+
// Wait until socket is writable or timeout expires
99+
fd_set wfds;
100+
FD_ZERO(&wfds);
101+
FD_SET(sockfd, &wfds);
102+
103+
rv = ::select(0, nullptr, &wfds, nullptr, const_cast<timeval *>(&tv));
104+
105+
// Restore blocking mode regardless of select result
106+
mode = 0UL;
107+
if (::ioctlsocket(sockfd, FIONBIO, &mode) == SOCKET_ERROR) {
108+
return SOCKET_ERROR;
109+
}
110+
111+
if (rv == 0) {
112+
WSASetLastError(WSAETIMEDOUT);
113+
return SOCKET_ERROR;
114+
}
115+
if (rv == SOCKET_ERROR) {
116+
return SOCKET_ERROR;
117+
}
118+
119+
int so_error = 0;
120+
int len = sizeof(so_error);
121+
if (::getsockopt(sockfd, SOL_SOCKET, SO_ERROR, reinterpret_cast<char *>(&so_error), &len) ==
122+
SOCKET_ERROR) {
123+
return SOCKET_ERROR;
124+
}
125+
if (so_error != 0 && so_error != WSAEISCONN) {
126+
// connection failed
127+
WSASetLastError(so_error);
128+
return SOCKET_ERROR;
129+
}
130+
131+
return 0; // success
132+
}
60133

61134
// try to connect or throw on failure
62-
void connect(const std::string &host, int port) {
135+
void connect(const std::string &host, int port, int timeout_ms = 0) {
63136
if (is_connected()) {
64137
close();
65138
}
@@ -71,6 +144,10 @@ class tcp_client {
71144
hints.ai_flags = AI_NUMERICSERV; // port passed as as numeric value
72145
hints.ai_protocol = 0;
73146

147+
timeval tv;
148+
tv.tv_sec = timeout_ms / 1000;
149+
tv.tv_usec = (timeout_ms % 1000) * 1000;
150+
74151
auto port_str = std::to_string(port);
75152
struct addrinfo *addrinfo_result;
76153
auto rv = ::getaddrinfo(host.c_str(), port_str.c_str(), &hints, &addrinfo_result);
@@ -82,26 +159,31 @@ class tcp_client {
82159
}
83160

84161
// Try each address until we successfully connect(2).
85-
86162
for (auto *rp = addrinfo_result; rp != nullptr; rp = rp->ai_next) {
87163
socket_ = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
88164
if (socket_ == INVALID_SOCKET) {
89165
last_error = ::WSAGetLastError();
90166
WSACleanup();
91167
continue;
92168
}
93-
if (::connect(socket_, rp->ai_addr, (int)rp->ai_addrlen) == 0) {
169+
if (connect_socket_with_timeout(socket_, rp->ai_addr, (int)rp->ai_addrlen, tv) == 0) {
170+
last_error = 0;
94171
break;
95-
} else {
96-
last_error = ::WSAGetLastError();
97-
close();
98172
}
173+
last_error = WSAGetLastError();
174+
::closesocket(socket_);
175+
socket_ = INVALID_SOCKET;
99176
}
100177
::freeaddrinfo(addrinfo_result);
101178
if (socket_ == INVALID_SOCKET) {
102179
WSACleanup();
103180
throw_winsock_error_("connect failed", last_error);
104181
}
182+
if (timeout_ms > 0) {
183+
DWORD tv = static_cast<DWORD>(timeout_ms);
184+
::setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv));
185+
::setsockopt(socket_, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof(tv));
186+
}
105187

106188
// set TCP_NODELAY
107189
int enable_flag = 1;

include/spdlog/details/tcp_client.h

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,72 @@ class tcp_client {
3939

4040
~tcp_client() { close(); }
4141

42+
int connect_socket_with_timeout(int sockfd,
43+
const struct sockaddr *addr,
44+
socklen_t addrlen,
45+
const timeval &tv) {
46+
// Blocking connect if timeout is zero
47+
if (tv.tv_sec == 0 && tv.tv_usec == 0) {
48+
int rv = ::connect(sockfd, addr, addrlen);
49+
if (rv < 0 && errno == EISCONN) {
50+
// already connected, treat as success
51+
return 0;
52+
}
53+
return rv;
54+
}
55+
56+
// Non-blocking path
57+
int orig_flags = ::fcntl(sockfd, F_GETFL, 0);
58+
if (orig_flags < 0) {
59+
return -1;
60+
}
61+
if (::fcntl(sockfd, F_SETFL, orig_flags | O_NONBLOCK) < 0) {
62+
return -1;
63+
}
64+
65+
int rv = ::connect(sockfd, addr, addrlen);
66+
if (rv == 0 || (rv < 0 && errno == EISCONN)) {
67+
// immediate connect or already connected
68+
::fcntl(sockfd, F_SETFL, orig_flags);
69+
return 0;
70+
}
71+
if (errno != EINPROGRESS) {
72+
::fcntl(sockfd, F_SETFL, orig_flags);
73+
return -1;
74+
}
75+
76+
// wait for writability
77+
fd_set wfds;
78+
FD_ZERO(&wfds);
79+
FD_SET(sockfd, &wfds);
80+
81+
struct timeval tv_copy = tv;
82+
rv = ::select(sockfd + 1, nullptr, &wfds, nullptr, &tv_copy);
83+
if (rv <= 0) {
84+
// timeout or error
85+
::fcntl(sockfd, F_SETFL, orig_flags);
86+
if (rv == 0) errno = ETIMEDOUT;
87+
return -1;
88+
}
89+
90+
// check socket error
91+
int so_error = 0;
92+
socklen_t len = sizeof(so_error);
93+
if (::getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_error, &len) < 0) {
94+
::fcntl(sockfd, F_SETFL, orig_flags);
95+
return -1;
96+
}
97+
::fcntl(sockfd, F_SETFL, orig_flags);
98+
if (so_error != 0 && so_error != EISCONN) {
99+
errno = so_error;
100+
return -1;
101+
}
102+
103+
return 0;
104+
}
105+
42106
// try to connect or throw on failure
43-
void connect(const std::string &host, int port) {
107+
void connect(const std::string &host, int port, int timeout_ms = 0) {
44108
close();
45109
struct addrinfo hints {};
46110
memset(&hints, 0, sizeof(struct addrinfo));
@@ -49,6 +113,10 @@ class tcp_client {
49113
hints.ai_flags = AI_NUMERICSERV; // port passed as as numeric value
50114
hints.ai_protocol = 0;
51115

116+
struct timeval tv;
117+
tv.tv_sec = timeout_ms / 1000;
118+
tv.tv_usec = (timeout_ms % 1000) * 1000;
119+
52120
auto port_str = std::to_string(port);
53121
struct addrinfo *addrinfo_result;
54122
auto rv = ::getaddrinfo(host.c_str(), port_str.c_str(), &hints, &addrinfo_result);
@@ -69,8 +137,9 @@ class tcp_client {
69137
last_errno = errno;
70138
continue;
71139
}
72-
rv = ::connect(socket_, rp->ai_addr, rp->ai_addrlen);
73-
if (rv == 0) {
140+
::fcntl(socket_, F_SETFD, FD_CLOEXEC);
141+
if (connect_socket_with_timeout(socket_, rp->ai_addr, rp->ai_addrlen, tv) == 0) {
142+
last_errno = 0;
74143
break;
75144
}
76145
last_errno = errno;
@@ -82,6 +151,12 @@ class tcp_client {
82151
throw_spdlog_ex("::connect failed", last_errno);
83152
}
84153

154+
if (timeout_ms > 0) {
155+
// Set timeouts for send and recv
156+
::setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv));
157+
::setsockopt(socket_, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof(tv));
158+
}
159+
85160
// set TCP_NODELAY
86161
int enable_flag = 1;
87162
::setsockopt(socket_, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char *>(&enable_flag),

include/spdlog/sinks/tcp_sink.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ namespace sinks {
3131
struct tcp_sink_config {
3232
std::string server_host;
3333
int server_port;
34+
int timeout_ms = 0; // The timeout for all 3 major socket operations that is connect, send, and recv
3435
bool lazy_connect = false; // if true connect on first log call instead of on construction
3536

3637
tcp_sink_config(std::string host, int port)
@@ -44,10 +45,22 @@ class tcp_sink : public spdlog::sinks::base_sink<Mutex> {
4445
// connect to tcp host/port or throw if failed
4546
// host can be hostname or ip address
4647

48+
explicit tcp_sink(const std::string &host,
49+
int port,
50+
int timeout_ms = 0,
51+
bool lazy_connect = false)
52+
: config_{host, port} {
53+
config_.timeout_ms = timeout_ms;
54+
config_.lazy_connect = lazy_connect;
55+
if (!config_.lazy_connect) {
56+
client_.connect(config_.server_host, config_.server_port, config_.timeout_ms);
57+
}
58+
}
59+
4760
explicit tcp_sink(tcp_sink_config sink_config)
4861
: config_{std::move(sink_config)} {
4962
if (!config_.lazy_connect) {
50-
this->client_.connect(config_.server_host, config_.server_port);
63+
client_.connect(config_.server_host, config_.server_port, config_.timeout_ms);
5164
}
5265
}
5366

@@ -58,7 +71,7 @@ class tcp_sink : public spdlog::sinks::base_sink<Mutex> {
5871
spdlog::memory_buf_t formatted;
5972
spdlog::sinks::base_sink<Mutex>::formatter_->format(msg, formatted);
6073
if (!client_.is_connected()) {
61-
client_.connect(config_.server_host, config_.server_port);
74+
client_.connect(config_.server_host, config_.server_port, config_.timeout_ms);
6275
}
6376
client_.send(formatted.data(), formatted.size());
6477
}

0 commit comments

Comments
 (0)