Skip to content
This repository was archived by the owner on Mar 29, 2026. It is now read-only.

Commit fb183d7

Browse files
authored
feat, fix: Added heartbeats (#22), improved servers, rewrote unit tests for servers (#23)
* feat: start of sending heartbeats to clients, along with other improvements * feat: Added heartbeats, fixed server being stuck in authentication, added some logging, rewrote unit test to properly test servers
1 parent 112a738 commit fb183d7

6 files changed

Lines changed: 206 additions & 115 deletions

File tree

include/rconpp/server.h

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
namespace rconpp {
2424

2525
struct connected_client {
26-
struct sockaddr_in sock_info{};
26+
sockaddr_in sock_info{};
2727
int socket{0};
2828
bool connected{false};
2929

3030
bool authenticated{false};
31+
32+
time_t last_heartbeat{0};
3133
};
3234

3335
struct client_command {
@@ -47,6 +49,7 @@ class RCONPP_EXPORT rcon_server {
4749
#endif
4850

4951
std::thread accept_connections_runner;
52+
std::mutex connected_clients_mutex;
5053

5154
public:
5255
bool online{false};
@@ -84,8 +87,9 @@ class RCONPP_EXPORT rcon_server {
8487
* @brief Disconnect a client from the server.
8588
*
8689
* @param client_socket The socket of the client to disconnect.
90+
* @param remove_after Should remove client from connected_clients after?
8791
*/
88-
void disconnect_client(const int client_socket);
92+
void disconnect_client(int client_socket, bool remove_after = true);
8993

9094
private:
9195

@@ -99,19 +103,20 @@ class RCONPP_EXPORT rcon_server {
99103
bool startup_server();
100104

101105
/**
102-
* @brief Ask to receive information from the server for a specified ID.
103-
*
104-
* @param id The ID that we should except the server to return, alongside information.
105-
* @param type The type of packet that we should expect.
106+
* @brief Gathers all the packet's content (based on the length returned by `read_packet_length`)
106107
*
107-
* @return Data given by the server.
108+
* @param client Client to read packet from.
108109
*/
109-
response receive_information(int32_t id, data_type type);
110+
void read_packet(connected_client& client);
110111

111112
/**
112-
* @brief Gathers all the packet's content (based on the length returned by `read_packet_length`)
113+
* @brief Sends a heartbeat to a client.
114+
*
115+
* @param client Client to send a heartbeat to.
116+
*
117+
* @returns bool, true is heartbeat was sent, otherwise false.
113118
*/
114-
void read_packet(rconpp::connected_client client);
119+
bool send_heartbeat(connected_client& client);
115120
};
116121

117122
} // namespace rconpp

include/rconpp/utilities.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ constexpr int DEFAULT_TIMEOUT = 4; // In Seconds.
1414
constexpr int MIN_PACKET_SIZE = 10;
1515
constexpr int MIN_PACKET_LENGTH = 14;
1616
constexpr int MAX_RETRIES_TO_RECEIVE_INFO = 500;
17+
constexpr int HEARTBEAT_TIME = 30;
18+
1719

1820
enum data_type {
1921
/**
@@ -99,6 +101,6 @@ RCONPP_EXPORT void report_error();
99101
*
100102
* @return The size (not length) of the packet.
101103
*/
102-
RCONPP_EXPORT int read_packet_size(int socket, const std::function<void(const std::string_view log)>& on_log);
104+
RCONPP_EXPORT int read_packet_size(int socket);
103105

104106
} // namespace rconpp

src/rconpp/client.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ rconpp::rcon_client::rcon_client(const std::string_view addr, const int _port, c
66
}
77

88
rconpp::rcon_client::~rcon_client() {
9+
if (on_log) {
10+
on_log("RCON client is shutting down.");
11+
}
912
// Set connected to false, meaning no requests can be attempted during shutdown.
1013
connected = false;
1114

@@ -142,7 +145,12 @@ rconpp::response rconpp::rcon_client::receive_information(int32_t id, rconpp::da
142145
}
143146

144147
rconpp::packet rconpp::rcon_client::read_packet() {
145-
const int packet_size = read_packet_size(static_cast<int>(sock), on_log);
148+
const int packet_size = read_packet_size(static_cast<int>(sock));
149+
150+
if (packet_size == -1) {
151+
on_log("Did not receive a packet in time. Did the server send a response?");
152+
return {};
153+
}
146154

147155
packet temp_packet{};
148156
temp_packet.length = packet_size + 4;
@@ -205,7 +213,7 @@ void rconpp::rcon_client::start(const bool return_after) {
205213

206214
// The server will send SERVERDATA_AUTH_RESPONSE once it's happy. If it's not -1, the server will have accepted us!
207215
// We use the _sync method here to do a blocking call.
208-
const response response = send_data_sync(password, 1, data_type::SERVERDATA_AUTH, true);
216+
const response response = send_data_sync(password, 1, SERVERDATA_AUTH, true);
209217

210218
if (!response.server_responded) {
211219
on_log("Login data was incorrect. RCON++ will now abort.");

src/rconpp/server.cpp

Lines changed: 110 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ rconpp::rcon_server::rcon_server(const std::string_view addr, const int _port, c
77
}
88

99
rconpp::rcon_server::~rcon_server() {
10+
if (on_log) {
11+
on_log("RCON server is shutting down.");
12+
}
13+
1014
// Set connected to false, meaning no requests can be attempted during shutdown.
1115
online = false;
1216

1317
terminating.notify_all();
1418

1519
// Safely disconnect all clients from server.
1620
for(const auto& client : connected_clients) {
17-
disconnect_client(client.first);
21+
disconnect_client(client.first, false);
1822
}
1923

2024
#ifdef _WIN32
@@ -80,90 +84,113 @@ bool rconpp::rcon_server::startup_server() {
8084
return true;
8185
}
8286

83-
void rconpp::rcon_server::disconnect_client(const int client_socket) {
87+
void rconpp::rcon_server::disconnect_client(const int client_socket, const bool remove_after /*= true*/) {
8488

8589
#ifdef _WIN32
8690
closesocket(client_socket);
8791
#else
8892
close(client_socket);
8993
#endif
9094

95+
std::lock_guard guard(connected_clients_mutex);
96+
9197
connected_clients.at(client_socket).connected = false;
9298

9399
if (request_handlers.at(client_socket).joinable()) {
94100
request_handlers.at(client_socket).join();
95101
}
96102

97-
connected_clients.erase(client_socket);
103+
if (remove_after) {
104+
connected_clients.erase(client_socket);
105+
}
98106
}
99107

100-
void rconpp::rcon_server::read_packet(rconpp::connected_client client) {
101-
while (client.connected) {
102-
const int packet_size = read_packet_size(static_cast<int>(sock), on_log);
108+
void rconpp::rcon_server::read_packet(connected_client& client) {
109+
const int packet_size = read_packet_size(static_cast<int>(client.socket));
103110

104-
if (packet_size <= MIN_PACKET_SIZE) {
105-
continue;
106-
}
111+
// Silently ignore packet size.
112+
if (packet_size < MIN_PACKET_SIZE) {
113+
return;
114+
}
107115

108-
std::vector<char> buffer{};
109-
buffer.resize(packet_size);
116+
std::vector<char> buffer{};
117+
buffer.resize(packet_size);
110118

111-
if (recv(client.socket, buffer.data(), packet_size, 0) == -1) {
112-
on_log("Failed to get a packet from client.");
113-
report_error();
114-
}
119+
if (recv(client.socket, buffer.data(), packet_size, 0) == -1) {
120+
on_log("Failed to get a packet from client.");
121+
report_error();
122+
return;
123+
}
115124

116-
std::string packet_data(&buffer[8], &buffer[buffer.size()-2]);
117-
int id = bit32_to_int(buffer);
118-
int type = type_to_int(buffer);
125+
// Client is talking to us, we don't need to send a heartbeat if we're being talked to.
126+
client.last_heartbeat = time(nullptr);
119127

120-
rconpp::packet packet_to_send{};
128+
std::string packet_data(&buffer[8], &buffer[buffer.size()-2]);
129+
int id = bit32_to_int(buffer);
130+
int type = type_to_int(buffer);
121131

122-
if (!client.authenticated) {
123-
if (packet_data == password) {
124-
packet_to_send = form_packet("", id, rconpp::data_type::SERVERDATA_AUTH_RESPONSE);
125-
client.authenticated = true;
126-
} else {
127-
packet_to_send = form_packet("", -1, rconpp::data_type::SERVERDATA_AUTH_RESPONSE);
128-
}
132+
packet packet_to_send{};
133+
134+
if (!client.authenticated) {
135+
on_log("Client not authenticated, handling authentication.");
136+
if (packet_data == password) {
137+
packet_to_send = form_packet("", id, SERVERDATA_AUTH_RESPONSE);
138+
client.authenticated = true;
139+
} else {
140+
packet_to_send = form_packet("", -1, SERVERDATA_AUTH_RESPONSE);
141+
}
142+
} else {
143+
if (type != SERVERDATA_EXECCOMMAND) {
144+
packet_to_send = form_packet("Invalid packet type (" + std::to_string(type) + "). Double check your packets.", id, SERVERDATA_RESPONSE_VALUE);
145+
on_log("Invalid packet type (" + std::to_string(type) + ") sent by [" + inet_ntoa(client.sock_info.sin_addr) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]. Double check your packets.");
129146
} else {
130-
if (type != rconpp::data_type::SERVERDATA_EXECCOMMAND) {
131-
packet_to_send = form_packet("Invalid packet type (" + std::to_string(type) + "). Double check your packets.", id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE);
132-
on_log("Invalid packet type (" + std::to_string(type) + ") sent by [" + inet_ntoa(client.sock_info.sin_addr) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]. Double check your packets.");
147+
on_log("Client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "] has asked to execute the command: \"" + packet_data + "\"");
148+
if (!on_command) {
149+
on_log("You have not set any response for on_command! The server will default to a blank response.");
150+
151+
/*
152+
* Whilst sending information about the server not responding would be nice,
153+
* we would end up with the possibility of clients thinking that is the response.
154+
* It's better to just send no information and let clients assume that meant
155+
* the server didn't like the command.
156+
*/
157+
packet_to_send = form_packet("", id, SERVERDATA_RESPONSE_VALUE);
133158
} else {
134-
on_log("Client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "] has asked to execute the command: \"" + packet_data + "\"");
135-
if (!on_command) {
136-
on_log("You have not set any response for on_command! The server will default to a blank response.");
137-
138-
/*
139-
* Whilst sending information about the server not responding would be nice,
140-
* we would end up with the possibility of clients thinking that is the response.
141-
* It's better to just send no information and let clients assume that meant
142-
* the server didn't like the command.
143-
*/
144-
packet_to_send = form_packet("", id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE);
145-
} else {
146-
client_command command{};
147-
command.command = packet_data;
148-
command.client = client;
149-
150-
std::string text_to_send = on_command(command);
151-
152-
on_log("Sending reply \"" + text_to_send + "\" to client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "].");
153-
154-
packet_to_send = form_packet(text_to_send, id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE);
155-
}
159+
client_command command{};
160+
command.command = packet_data;
161+
command.client = client;
162+
163+
std::string text_to_send = on_command(command);
164+
165+
on_log("Sending reply \"" + text_to_send + "\" to client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "].");
166+
167+
packet_to_send = form_packet(text_to_send, id, SERVERDATA_RESPONSE_VALUE);
156168
}
157169
}
170+
}
158171

159-
on_log("Sending...");
172+
on_log("Sending packet (of size: " + std::to_string(packet_to_send.length) + ") to client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]");
160173

161-
if (send(client.socket, packet_to_send.data.data(), packet_to_send.length, 0) < 0) {
162-
on_log("Sending failed!");
163-
report_error();
164-
continue;
165-
}
174+
if (send(client.socket, packet_to_send.data.data(), packet_to_send.length, 0) < 0) {
175+
on_log("Sending failed!");
176+
report_error();
177+
return;
178+
}
179+
}
180+
181+
bool rconpp::rcon_server::send_heartbeat(connected_client& client) {
182+
on_log("Sending heartbeat to client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]");
183+
184+
packet packet_to_send = form_packet("", -1, SERVERDATA_RESPONSE_VALUE);
185+
if (send(client.socket, packet_to_send.data.data(), packet_to_send.length, 0) < 0) {
186+
on_log("Failed to send a heartbeat to client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]");
187+
report_error();
188+
return false;
166189
}
190+
191+
client.last_heartbeat = time(nullptr);
192+
193+
return true;
167194
}
168195

169196
void rconpp::rcon_server::start(bool return_after) {
@@ -181,7 +208,7 @@ void rconpp::rcon_server::start(bool return_after) {
181208
on_log("Attempting to startup an RCON server...");
182209

183210
if (!startup_server()) {
184-
on_log("RCON++ is aborting as it failed to initiate server.");
211+
on_log("RCON server is aborting as it failed to initiate server.");
185212
return;
186213
}
187214

@@ -191,32 +218,52 @@ void rconpp::rcon_server::start(bool return_after) {
191218

192219
accept_connections_runner = std::thread([this]() {
193220
while (online) {
194-
connected_client client{};
195-
struct sockaddr_in client_info{};
221+
sockaddr_in client_info{};
196222

197223
socklen_t client_len = sizeof(client_info);
198224
int client_socket = accept(sock, reinterpret_cast<sockaddr*>(&client_info), &client_len);
199225

200226
if (client_socket == -1) {
201227
on_log("client with socket: \"" + std::to_string(client_socket) + "\" failed to connect.");
228+
report_error();
202229
continue;
203230
}
204231

205232
on_log("Client [" + std::string(inet_ntoa(client_info.sin_addr)) + ":" + std::to_string(ntohs(client_info.sin_port)) + "] has connected to the server.");
206233

234+
connected_client client{};
235+
207236
client.sock_info = client_info;
208237
client.socket = client_socket;
209238
client.connected = true;
210239

211-
std::thread client_thread([this, client]{
212-
read_packet(client);
240+
std::thread client_thread([this, &client]{
241+
while (client.connected) {
242+
read_packet(client);
243+
244+
const time_t current_time = time(nullptr);
245+
246+
if (client.authenticated) {
247+
if (client.last_heartbeat == 0 || current_time - client.last_heartbeat >= HEARTBEAT_TIME)
248+
{
249+
if (!send_heartbeat(client)) {
250+
disconnect_client(client.socket);
251+
}
252+
}
253+
}
254+
255+
// No need to let the server keep running this causing 100% usage on a thread, we can wait a bit between requests.
256+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
257+
}
213258
});
214259

215260
request_handlers.insert({ client_socket, std::move(client_thread) });
216261

217262
request_handlers.at(client_socket).detach();
218263

219-
connected_clients.insert({});
264+
std::lock_guard guard(connected_clients_mutex);
265+
266+
connected_clients.insert({ client_socket, client });
220267
}
221268
});
222269

src/rconpp/utilities.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ void rconpp::report_error() {
4646
#endif
4747
}
4848

49-
int rconpp::read_packet_size(int socket, const std::function<void(const std::string_view log)>& on_log) {
49+
int rconpp::read_packet_size(int socket) {
5050
std::vector<char> buffer{};
5151
buffer.resize(4);
5252

@@ -55,7 +55,6 @@ int rconpp::read_packet_size(int socket, const std::function<void(const std::str
5555
* We simply just want to read that and then return it.
5656
*/
5757
if (recv(socket, buffer.data(), 4, 0) == -1) {
58-
on_log("Did not receive a packet in time. Did the server send a response?");
5958
report_error();
6059
return -1;
6160
}

0 commit comments

Comments
 (0)