-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.cpp
More file actions
519 lines (438 loc) · 20.5 KB
/
Copy pathserver.cpp
File metadata and controls
519 lines (438 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
/*
// server.cpp – Kayles game server.
//
// Single-process, single-threaded UDP state machine driven by incoming
// datagrams.
//
// Build: see Makefile
// Usage: kayles_server -r pawn_row -a address -p port -t server_timeout
*/
#include "protocol.h"
#include "helpers.h"
#include "server_config.h"
#include "pawn_row.h"
#include <arpa/inet.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
// ────────────────────────────────────────────────────────────────────────────
// Extended game entry stored by the server
// ────────────────────────────────────────────────────────────────────────────
struct GameEntry {
GameState state;
/// Timestamps of last valid message from each player (for timeout).
time_t last_msg_a {0};
time_t last_msg_b {0};
};
// ────────────────────────────────────────────────────────────────────────────
// Server state
// ────────────────────────────────────────────────────────────────────────────
struct Server {
int sockfd {-1};
int timeout {0};
/// Pawn row template for new games (values constant per server run).
uint8_t max_pawn {0};
std::array<uint8_t, MAX_PAWN_ROW_SIZE> pawn_row_template {};
/// All active + recently finished games, keyed by game_id.
std::unordered_map<game_id_t, GameEntry> games;
/// game_id of the current WAITING_FOR_OPPONENT game, if any.
std::optional<game_id_t> waiting_game_id;
/// Monotonically increasing counter for generating unique game IDs.
/// Never reuse an ID.
game_id_t next_game_id {0};
};
// ────────────────────────────────────────────────────────────────────────────
// Sending helpers
// ────────────────────────────────────────────────────────────────────────────
static void send_game_state(int sockfd,
const GameState &gs,
const sockaddr_in &dest) {
uint8_t buf[MAX_SERVER_MSG];
const size_t len = serialize_game_state(gs, buf);
if (sendto(sockfd, buf, len, 0,
reinterpret_cast<const sockaddr *>(&dest),
sizeof(dest)) < 0) {
std::perror("sendto (game_state)");
}
}
static void send_wrong_msg(int sockfd,
const uint8_t *client_buf, size_t client_len,
uint8_t error_index,
const sockaddr_in &dest) {
uint8_t buf[WRONG_MSG_SIZE];
serialize_wrong_msg(client_buf, client_len, error_index, buf);
if (sendto(sockfd, buf, WRONG_MSG_SIZE, 0,
reinterpret_cast<const sockaddr *>(&dest),
sizeof(dest)) < 0) {
std::perror("sendto (wrong_msg)");
}
}
// ────────────────────────────────────────────────────────────────────────────
// Timeout / cleanup
// ────────────────────────────────────────────────────────────────────────────
/// For a game in WAITING_FOR_OPPONENT: returns true if it should be removed
/// (no keep-alive from player A, no JOIN from player B within server_timeout).
static bool is_waiting_expired(const GameEntry &e, int timeout_sec) {
return (serverhelper::now() - e.last_msg_a) >= timeout_sec;
}
/// For an active or finished game: returns true if it should be removed.
/// The deadline is server_timeout after the latest valid message from either player.
static bool is_active_expired(const GameEntry &e, int timeout_sec) {
const time_t last = std::max(e.last_msg_a, e.last_msg_b);
return (serverhelper::now() - last) >= timeout_sec;
}
/// Removes expired games and clears the waiting_game_id if needed.
static void expire_games(Server &srv) {
auto it = srv.games.begin();
while (it != srv.games.end()) {
const GameEntry &e = it->second;
bool expired = false;
if (e.state.status == GameStatus::waiting_for_opponent) {
expired = is_waiting_expired(e, srv.timeout);
} else {
expired = is_active_expired(e, srv.timeout);
}
if (expired) {
if (srv.waiting_game_id && *srv.waiting_game_id == it->first) {
srv.waiting_game_id.reset();
}
it = srv.games.erase(it);
} else {
++it;
}
}
}
// ────────────────────────────────────────────────────────────────────────────
// Determine which player timed out in an active game and mark the result.
// ────────────────────────────────────────────────────────────────────────────
static void check_active_timeout(GameEntry &e, int timeout_sec) {
if (e.state.status != GameStatus::turn_a &&
e.state.status != GameStatus::turn_b) {
return; // already finished or waiting
}
const time_t t = serverhelper::now();
const bool a_dead = (t - e.last_msg_a) >= timeout_sec;
const bool b_dead = (t - e.last_msg_b) >= timeout_sec;
if (a_dead)
e.state.status = GameStatus::win_b;
else if (b_dead)
e.state.status = GameStatus::win_a;
}
// ────────────────────────────────────────────────────────────────────────────
// Game logic helpers
// ────────────────────────────────────────────────────────────────────────────
/// Returns true if the move "remove pawn p" is legal in state gs.
static bool is_move_one_legal(const GameState &gs, uint8_t p) {
if (p > gs.max_pawn) return false;
return gs.pawn_present(p);
}
/// Returns true if the move "remove pawns p and p+1" is legal in state gs.
static bool is_move_two_legal(const GameState &gs, uint8_t p) {
if (p >= gs.max_pawn) return false; // p+1 would exceed max_pawn
return gs.pawn_present(p) && gs.pawn_present(p + 1);
}
/// Switches turn (TURN_A <-> TURN_B) and checks for win.
static void finalize_move(GameState &gs, bool was_turn_a) {
if (!gs.any_pawn_present()) {
gs.status = was_turn_a ? GameStatus::win_a : GameStatus::win_b;
} else {
gs.status = was_turn_a ? GameStatus::turn_b : GameStatus::turn_a;
}
}
// ────────────────────────────────────────────────────────────────────────────
// Message dispatch
// ────────────────────────────────────────────────────────────────────────────
static void handle_join(Server &srv,
const uint8_t *buf, size_t len,
const sockaddr_in &src) {
// Minimum size check: msg_type(1) + player_id(4) = 5 bytes.
if (len < sizeof(MsgJoin)) {
const uint8_t err = static_cast<uint8_t>(len); // first missing byte
send_wrong_msg(srv.sockfd, buf, len, err, src);
return;
} else if (len > sizeof(MsgJoin)) {
// Extra bytes after the fixed-size struct: error at byte sizeof(MsgJoin).
// We treat this as an error with error_index = 5,
// i.e. the first unexpected byte after the fixed-size struct.
send_wrong_msg(srv.sockfd, buf, len, 5, src);
return;
}
MsgJoin msg;
std::memcpy(&msg, buf, sizeof(msg));
const player_id_t pid = ntohl(msg.player_id);
if (pid == 0) {
send_wrong_msg(srv.sockfd, buf, len, 1, src);
return;
}
// ── Run lazy cleanup ──────────────────────────────────────────────────
expire_games(srv);
// ── No waiting game: create one ───────────────────────────────────────
if (!srv.waiting_game_id) {
// Check ID exhaustion: if next_game_id is already taken (after 2^32
// games) we cannot allocate; silently ignore per spec.
if (srv.games.count(srv.next_game_id) != 0) {
return;
}
GameEntry entry;
entry.state.game_id = srv.next_game_id;
entry.state.player_a = pid;
entry.state.player_b = 0;
entry.state.status = GameStatus::waiting_for_opponent;
entry.state.max_pawn = srv.max_pawn;
entry.state.pawn_row = srv.pawn_row_template;
entry.last_msg_a = serverhelper::now();
entry.last_msg_b = 0;
srv.waiting_game_id = srv.next_game_id;
srv.games.emplace(srv.next_game_id, entry);
++srv.next_game_id;
send_game_state(srv.sockfd, entry.state, src);
return;
}
// ── Waiting game exists: join as player B ─────────────────────────────
GameEntry &entry = srv.games.at(*srv.waiting_game_id);
entry.state.player_b = pid;
entry.state.status = GameStatus::turn_b;
entry.last_msg_b = serverhelper::now();
srv.waiting_game_id.reset();
send_game_state(srv.sockfd, entry.state, src);
}
// Helper: validate player_id + game_id for non-JOIN messages.
// Returns pointer to GameEntry on success, nullptr if an error was sent.
static GameEntry *validate_common(Server &srv,
const uint8_t *buf, size_t len,
size_t expected_len,
const sockaddr_in &src,
player_id_t &out_pid,
game_id_t &out_gid) {
// Length check (msg_type already validated by caller).
if (len < expected_len) {
const uint8_t err = static_cast<uint8_t>(len);
send_wrong_msg(srv.sockfd, buf, len, err, src);
return nullptr;
} else if (len > expected_len) {
const uint8_t err = static_cast<uint8_t>(expected_len);
send_wrong_msg(srv.sockfd, buf, len, err, src);
return nullptr;
}
// player_id at offset 1 (4 bytes).
uint32_t pid_net;
std::memcpy(&pid_net, buf + 1, 4);
out_pid = ntohl(pid_net);
if (out_pid == 0) {
send_wrong_msg(srv.sockfd, buf, len, 1, src);
return nullptr;
}
// game_id at offset 5 (4 bytes).
uint32_t gid_net;
std::memcpy(&gid_net, buf + 5, 4);
out_gid = ntohl(gid_net);
auto it = srv.games.find(out_gid);
if (it == srv.games.end()) {
// game_id byte starts at offset 5.
send_wrong_msg(srv.sockfd, buf, len, 5, src);
return nullptr;
}
GameEntry &e = it->second;
if (e.state.player_a != out_pid && e.state.player_b != out_pid) {
// player_id is not a participant.
send_wrong_msg(srv.sockfd, buf, len, 1, src);
return nullptr;
}
return &e;
}
static void handle_move(Server &srv,
const uint8_t *buf, size_t len,
bool is_two,
const sockaddr_in &src) {
const size_t expected = is_two ? sizeof(MsgMoveTwo) : sizeof(MsgMoveOne);
player_id_t pid; game_id_t gid;
GameEntry *ep = validate_common(srv, buf, len, expected, src, pid, gid);
if (!ep) return;
GameEntry &e = *ep;
// Lazily update timeout state.
check_active_timeout(e, srv.timeout);
const uint8_t pawn = buf[expected - 1]; // last byte of either struct
// Determine which player is moving.
const bool is_a = (e.state.player_a == pid);
// Update last-heard timestamp regardless of move legality.
if (is_a) e.last_msg_a = serverhelper::now();
else e.last_msg_b = serverhelper::now();
// Check: is the game in a state where moves are accepted?
if (e.state.status != GameStatus::turn_a &&
e.state.status != GameStatus::turn_b) {
// Game is over; the move is illegal (state unchanged).
send_game_state(srv.sockfd, e.state, src);
return;
}
// Check whose turn it is.
const bool expected_a = (e.state.status == GameStatus::turn_a);
if (expected_a != is_a) {
// Wrong player's turn – illegal move.
send_game_state(srv.sockfd, e.state, src);
return;
}
// Check move legality and apply.
if (!is_two) {
if (is_move_one_legal(e.state, pawn)) {
e.state.set_pawn(pawn, false);
finalize_move(e.state, is_a);
}
// else: illegal pawn value - state unchanged (still valid message).
} else {
if (is_move_two_legal(e.state, pawn)) {
e.state.set_pawn(pawn, false);
e.state.set_pawn(pawn + 1, false);
finalize_move(e.state, is_a);
}
// else: illegal - state unchanged.
}
send_game_state(srv.sockfd, e.state, src);
}
static void handle_keep_alive(Server &srv,
const uint8_t *buf, size_t len,
const sockaddr_in &src) {
player_id_t pid; game_id_t gid;
GameEntry *ep = validate_common(srv, buf, len,
sizeof(MsgKeepAlive), src, pid, gid);
if (!ep) return;
GameEntry &e = *ep;
const bool is_a = (e.state.player_a == pid);
if (is_a) e.last_msg_a = serverhelper::now();
else e.last_msg_b = serverhelper::now();
check_active_timeout(e, srv.timeout);
send_game_state(srv.sockfd, e.state, src);
}
static void handle_give_up(Server &srv,
const uint8_t *buf, size_t len,
const sockaddr_in &src) {
player_id_t pid; game_id_t gid;
GameEntry *ep = validate_common(srv, buf, len,
sizeof(MsgGiveUp), src, pid, gid);
if (!ep) return;
GameEntry &e = *ep;
const bool is_a = (e.state.player_a == pid);
// Update last-heard regardless.
if (is_a) e.last_msg_a = serverhelper::now();
else e.last_msg_b = serverhelper::now();
check_active_timeout(e, srv.timeout);
// Give-up only legal when it is this player's turn.
const bool legal = (is_a && e.state.status == GameStatus::turn_a) ||
(!is_a && e.state.status == GameStatus::turn_b);
if (legal) {
e.state.status = is_a ? GameStatus::win_b : GameStatus::win_a;
}
// If not the player's turn, it's an illegal give-up; state unchanged.
send_game_state(srv.sockfd, e.state, src);
}
// ────────────────────────────────────────────────────────────────────────────
// Main receive / dispatch loop
// ────────────────────────────────────────────────────────────────────────────
static void run(Server &srv) {
uint8_t buf[MAX_CLIENT_MSG];
sockaddr_in src{};
socklen_t src_len = sizeof(src);
for (;;) {
src_len = sizeof(src);
const ssize_t n = recvfrom(srv.sockfd, buf, sizeof(buf), 0,
reinterpret_cast<sockaddr *>(&src), &src_len);
if (n < 0) {
if (errno == EINTR) continue;
std::perror("recvfrom");
continue;
}
const size_t len = static_cast<size_t>(n);
// Always run lazy cleanup on each received datagram.
expire_games(srv);
// Zero-length datagram: error_index = 0 (first missing byte = msg_type).
if (len == 0) {
send_wrong_msg(srv.sockfd, buf, 0, 0, src);
continue;
}
const auto msg_type = static_cast<MsgType>(buf[0]);
switch (msg_type) {
case MsgType::join:
handle_join(srv, buf, len, src);
break;
case MsgType::move_one:
handle_move(srv, buf, len, false, src);
break;
case MsgType::move_two:
handle_move(srv, buf, len, true, src);
break;
case MsgType::keep_alive:
handle_keep_alive(srv, buf, len, src);
break;
case MsgType::give_up:
handle_give_up(srv, buf, len, src);
break;
default:
// Unknown msg_type: error at byte 0.
send_wrong_msg(srv.sockfd, buf, len, 0, src);
break;
}
}
}
// ────────────────────────────────────────────────────────────────────────────
// main
// ────────────────────────────────────────────────────────────────────────────
int main(int argc, char *argv[]) {
const ServerConfig cfg = parse_args(argc, argv);
Server srv;
srv.timeout = cfg.server_timeout;
init_pawn_template(cfg.pawn_row_str, srv.max_pawn, srv.pawn_row_template);
// ── Create UDP socket ─────────────────────────────────────────────────
srv.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (srv.sockfd < 0)
serverhelper::fatal("socket");
// ── Resolve bind address ──────────────────────────────────────────────
sockaddr_in bind_addr{};
bind_addr.sin_family = AF_INET;
bind_addr.sin_port = htons(cfg.port);
if (cfg.address == "0.0.0.0") {
bind_addr.sin_addr.s_addr = INADDR_ANY;
} else {
if (inet_pton(AF_INET, cfg.address.c_str(),
&bind_addr.sin_addr) != 1) {
addrinfo hints{};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
addrinfo *res = nullptr;
const int rc = getaddrinfo(cfg.address.c_str(), nullptr, &hints, &res);
if (rc != 0)
kaylescommon::die("cannot resolve address '%s': %s",
cfg.address.c_str(), gai_strerror(rc));
bind_addr.sin_addr =
reinterpret_cast<sockaddr_in *>(res->ai_addr)->sin_addr;
freeaddrinfo(res);
}
}
if (bind(srv.sockfd,
reinterpret_cast<sockaddr *>(&bind_addr),
sizeof(bind_addr)) < 0)
serverhelper::fatal("bind");
// Report actual port if 0 was requested.
if (cfg.port == 0) {
sockaddr_in actual{};
socklen_t actual_len = sizeof(actual);
if (getsockname(srv.sockfd,
reinterpret_cast<sockaddr *>(&actual),
&actual_len) == 0) {
std::printf("Listening on port %u\n", ntohs(actual.sin_port));
}
}
run(srv);
// Unreachable.
close(srv.sockfd);
return 0;
}