-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.cpp
More file actions
827 lines (730 loc) · 29.1 KB
/
main.cpp
File metadata and controls
827 lines (730 loc) · 29.1 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
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
/**
* @file main.cpp
* @brief Multi-threaded HTTP server built with WinSock2
*
* Features:
* - Multi-threaded client handling (one thread per connection)
* - Static file serving with MIME type detection
* - Configuration via INI file
* - Structured logging to console and file
* - Graceful shutdown on Ctrl+C
* - Request size limiting and socket timeouts
* - Active connection limiting (503 when overloaded)
* - Basic path traversal protection
*/
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "ws2_32.lib")
#include <WinSock2.h>
#include <string>
#include <iostream>
#include <thread>
#include <mutex>
#include <fstream>
#include <sstream>
#include <map>
#include <vector>
#include <windows.h>
#include <ctime>
#include <iomanip>
#include <algorithm>
#include <atomic>
#include <csignal>
// ============================================================================
// CONFIGURATION STRUCT
// ============================================================================
/**
* @struct Config
* @brief Server configuration loaded from INI file
*
* All values have sensible defaults. If config.ini is missing or incomplete,
* the server falls back to these defaults.
*/
struct Config {
std::string host = "127.0.0.1"; // Server bind address
int port = 8080; // Server listen port
std::string www_root = "static"; // Root directory for static files
int buffer_size = 37020; // Internal buffer size for I/O
int max_connections = 20; // Backlog for listen()
int max_request_size = 8192; // Max HTTP request header size (bytes)
int recv_timeout_ms = 5000; // Socket receive timeout (milliseconds)
int send_timeout_ms = 5000; // Socket send timeout (milliseconds)
int max_active_clients = 100; // Max concurrent client threads
bool log_to_file = true; // Enable file logging
std::string log_level = "INFO"; // Minimum log level: DEBUG/INFO/WARN/ERROR
std::string log_dir = "logs"; // Directory for log files
};
// ============================================================================
// GLOBAL STATE
// ============================================================================
Config g_config; // Global server configuration
std::atomic<bool> g_running(true); // Main server loop control flag
SOCKET g_server_socket = INVALID_SOCKET; // Listening socket (global for shutdown access)
std::atomic<int> g_active_clients(0); // Current number of active client threads
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* @brief Remove leading and trailing whitespace from a string
* @param s Input string
* @return Trimmed string
*/
std::string trim(const std::string& s) {
size_t start = s.find_first_not_of(" \t\r\n");
if (start == std::string::npos) return "";
size_t end = s.find_last_not_of(" \t\r\n");
return s.substr(start, end - start + 1);
}
/**
* @brief Get the directory containing the current executable
* @return Absolute path to the executable's directory
*
* Used to locate config.ini and static files relative to the binary,
* making the server portable (works regardless of working directory).
*/
std::string getExecutableDir() {
char buffer[MAX_PATH];
GetModuleFileNameA(NULL, buffer, MAX_PATH);
std::string path(buffer);
size_t last_slash = path.find_last_of("\\/");
return path.substr(0, last_slash);
}
// ============================================================================
// INI FILE PARSER
// ============================================================================
/**
* @class IniParser
* @brief Simple INI file parser with section support
*
* Supports:
* - [Section] headers
* - key = value pairs
* - Comments starting with ; or #
* - Automatic trimming of whitespace
*
* Example INI:
* @code
* [server]
* port = 8080
* host = 127.0.0.1
*
* [logging]
* log_level = INFO
* @endcode
*/
class IniParser {
public:
// Nested map: section -> (key -> value)
std::map<std::string, std::map<std::string, std::string>> data;
/**
* @brief Load and parse an INI file
* @param filename Path to the INI file
* @return true if loaded successfully, false if file not found
*/
bool load(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
std::cerr << "Warning: Cannot open " << filename << ", using defaults" << std::endl;
return false;
}
std::string line, current_section;
while (std::getline(file, line)) {
line = trim(line);
// Skip empty lines and comments
if (line.empty() || line[0] == ';' || line[0] == '#') continue;
// Section header: [section_name]
if (line[0] == '[' && line.back() == ']') {
current_section = line.substr(1, line.size() - 2);
} else {
// Key-value pair: key = value
size_t eq = line.find('=');
if (eq != std::string::npos) {
std::string key = trim(line.substr(0, eq));
std::string val = trim(line.substr(eq + 1));
data[current_section][key] = val;
}
}
}
return true;
}
/**
* @brief Get string value from INI
* @param section Section name
* @param key Key name
* @param default_val Default value if key not found
* @return Value or default
*/
std::string get(const std::string& section, const std::string& key,
const std::string& default_val = "") {
auto sit = data.find(section);
if (sit != data.end()) {
auto kit = sit->second.find(key);
if (kit != sit->second.end()) return kit->second;
}
return default_val;
}
/**
* @brief Get integer value from INI
* @param section Section name
* @param key Key name
* @param default_val Default value if key not found or invalid
* @return Integer value or default
*/
int getInt(const std::string& section, const std::string& key, int default_val = 0) {
std::string val = get(section, key, std::to_string(default_val));
try { return std::stoi(val); } catch (...) { return default_val; }
}
/**
* @brief Get boolean value from INI
* @param section Section name
* @param key Key name
* @param default_val Default value if key not found
* @return true for "true"/"1"/"yes", false otherwise
*/
bool getBool(const std::string& section, const std::string& key, bool default_val = false) {
std::string val = get(section, key, default_val ? "true" : "false");
std::transform(val.begin(), val.end(), val.begin(), ::tolower);
return (val == "true" || val == "1" || val == "yes");
}
};
// ============================================================================
// LOGGER
// ============================================================================
/**
* @enum LogLevel
* @brief Severity levels for log messages
*
* Note: Named ERR instead of ERROR to avoid conflict with Windows macro.
*/
enum class LogLevel { DEBUG, INFO, WARN, ERR };
/**
* @class Logger
* @brief Thread-safe logger with console and file output
*
* Features:
* - Timestamped messages
* - Configurable minimum log level
* - Thread-safe via mutex
* - Daily log file rotation (filename includes date)
* - Automatic log directory creation
*/
class Logger {
private:
std::mutex mutex_; // Protects concurrent access
std::ofstream file_; // Log file stream
LogLevel min_level_; // Minimum level to log
bool log_to_console_ = true; // Also print to stdout
/**
* @brief Convert log level to display string
*/
std::string levelToString(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO ";
case LogLevel::WARN: return "WARN ";
case LogLevel::ERR: return "ERROR";
default: return "UNKNOWN";
}
}
/**
* @brief Get current timestamp as string: YYYY-MM-DD HH:MM:SS
*/
std::string getTimestamp() {
auto now = std::time(nullptr);
auto tm = std::localtime(&now);
std::ostringstream ss;
ss << std::put_time(tm, "%Y-%m-%d %H:%M:%S");
return ss.str();
}
/**
* @brief Get current date for log filename: YYYY-MM-DD
*/
std::string getDate() {
auto now = std::time(nullptr);
auto tm = std::localtime(&now);
std::ostringstream ss;
ss << std::put_time(tm, "%Y-%m-%d");
return ss.str();
}
/**
* @brief Parse log level from string (case-insensitive)
*/
LogLevel stringToLevel(const std::string& s) {
std::string upper = s;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
if (upper == "DEBUG") return LogLevel::DEBUG;
if (upper == "WARN") return LogLevel::WARN;
if (upper == "ERROR") return LogLevel::ERR;
return LogLevel::INFO;
}
public:
Logger() : min_level_(LogLevel::INFO) {}
~Logger() {
if (file_.is_open()) file_.close();
}
/**
* @brief Initialize the logger
* @param log_dir Directory for log files (created if missing)
* @param level_str Minimum log level as string
* @param log_to_file Enable file output
* @return true if initialization successful
*/
bool init(const std::string& log_dir, const std::string& level_str, bool log_to_file) {
min_level_ = stringToLevel(level_str);
log_to_console_ = true;
if (log_to_file) {
// Create log directory if it doesn't exist
CreateDirectoryA(log_dir.c_str(), NULL);
std::string filename = log_dir + "\\server_" + getDate() + ".log";
file_.open(filename, std::ios::app);
if (!file_.is_open()) {
std::cerr << "Failed to open log file: " << filename << std::endl;
return false;
}
}
return true;
}
/**
* @brief Log a message with specified level
* @param level Severity level
* @param msg Message content
*
* Messages below min_level_ are silently dropped.
*/
void log(LogLevel level, const std::string& msg) {
if (level < min_level_) return;
std::string timestamp = getTimestamp();
std::string level_str = levelToString(level);
std::string formatted = "[" + timestamp + "] [" + level_str + "] " + msg;
std::lock_guard<std::mutex> lock(mutex_);
if (log_to_console_) {
std::cout << formatted << std::endl;
}
if (file_.is_open()) {
file_ << formatted << std::endl;
file_.flush(); // Ensure immediate write for crash safety
}
}
// Convenience methods
void debug(const std::string& msg) { log(LogLevel::DEBUG, msg); }
void info(const std::string& msg) { log(LogLevel::INFO, msg); }
void warn(const std::string& msg) { log(LogLevel::WARN, msg); }
void error(const std::string& msg) { log(LogLevel::ERR, msg); }
};
Logger g_logger; // Global logger instance
// ============================================================================
// MIME TYPE HANDLING
// ============================================================================
/**
* @brief Map of file extensions to MIME types
*
* Used to set the correct Content-Type header when serving static files.
* Add more types as needed.
*/
std::map<std::string, std::string> mime_types = {
{".html", "text/html"},
{".htm", "text/html"},
{".css", "text/css"},
{".js", "application/javascript"},
{".png", "image/png"},
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".gif", "image/gif"},
{".ico", "image/x-icon"},
{".txt", "text/plain"},
{".json", "application/json"}
};
// ============================================================================
// FILE OPERATIONS
// ============================================================================
/**
* @brief Determine MIME type from file extension
* @param path File path
* @return MIME type string, or application/octet-stream if unknown
*/
std::string getMimeType(const std::string& path) {
size_t dot = path.find_last_of('.');
if (dot != std::string::npos) {
auto it = mime_types.find(path.substr(dot));
if (it != mime_types.end()) return it->second;
}
return "application/octet-stream";
}
/**
* @brief Read entire file into memory
* @param path Absolute or relative file path
* @param content Output string (binary-safe)
* @return true if file was read successfully
*/
bool readFile(const std::string& path, std::string& content) {
std::ifstream file(path, std::ios::binary);
if (!file.is_open()) return false;
std::ostringstream ss;
ss << file.rdbuf();
content = ss.str();
return true;
}
// ============================================================================
// HTTP PROTOCOL HANDLING
// ============================================================================
/**
* @brief Extract the request path from an HTTP request line
* @param request Raw HTTP request string
* @return Request path (e.g., "/index.html"), defaults to "/index.html"
*
* Parses the first line: "GET /path HTTP/1.1"
*/
std::string parsePath(const std::string& request) {
size_t first = request.find(' ');
if (first == std::string::npos) return "/";
size_t second = request.find(' ', first + 1);
if (second == std::string::npos) return "/";
std::string path = request.substr(first + 1, second - first - 1);
// Serve index.html for root path
if (path == "/" || path.empty()) path = "/index.html";
return path;
}
/**
* @brief Send an HTTP response to the client
* @param client_socket Connected client socket
* @param status HTTP status code (200, 404, etc.)
* @param content_type MIME type of the body
* @param body Response body content
*
* Automatically sets Content-Length and Connection: close headers.
* Handles partial sends by looping until all bytes are transmitted.
*/
void sendResponse(SOCKET client_socket, int status,
const std::string& content_type,
const std::string& body) {
// Map status codes to reason phrases
std::string status_text = (status == 200) ? "OK" :
(status == 403) ? "Forbidden" :
(status == 413) ? "Payload Too Large" :
(status == 503) ? "Service Unavailable" : "Not Found";
// Build HTTP response
std::string response =
"HTTP/1.1 " + std::to_string(status) + " " + status_text + "\r\n"
"Content-Type: " + content_type + "\r\n"
"Content-Length: " + std::to_string(body.size()) + "\r\n"
"Connection: close\r\n" // We close after each request
"\r\n" + body;
// Send with retry on partial writes
int total = 0;
while (total < (int)response.size()) {
int sent = send(client_socket, response.c_str() + total,
response.size() - total, 0);
if (sent <= 0) break; // Connection broken
total += sent;
}
}
// ============================================================================
// SOCKET CONFIGURATION
// ============================================================================
/**
* @brief Set receive and send timeouts on a socket
* @param sock Socket to configure
*
* Prevents hung connections from occupying threads indefinitely.
* Timeouts are read from g_config.
*/
void setSocketTimeouts(SOCKET sock) {
int recv_timeout = g_config.recv_timeout_ms;
int send_timeout = g_config.send_timeout_ms;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&recv_timeout, sizeof(recv_timeout));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&send_timeout, sizeof(send_timeout));
}
// ============================================================================
// CONFIGURATION INITIALIZATION
// ============================================================================
/**
* @brief Load configuration from INI file
*
* Searches for config.ini in this order:
* 1. Same directory as executable
* 2. Parent directory (for CLion/IDE builds where EXE is in cmake-build-debug)
*
* Falls back to hardcoded defaults if file not found.
* Also resolves the absolute path to www_root.
*/
void initConfig() {
std::string exeDir = getExecutableDir();
std::string configPath = exeDir + "\\config.ini";
// Try parent directory if not found next to EXE (CLion compatibility)
if (GetFileAttributesA(configPath.c_str()) == INVALID_FILE_ATTRIBUTES) {
std::string parent = exeDir.substr(0, exeDir.find_last_of("\\/"));
configPath = parent + "\\config.ini";
}
IniParser parser;
parser.load(configPath);
// Load server settings
g_config.host = parser.get("server", "host", "127.0.0.1");
g_config.port = parser.getInt("server", "port", 8080);
g_config.www_root = parser.get("server", "www_root", "static");
g_config.buffer_size = parser.getInt("server", "buffer_size", 37020);
g_config.max_connections = parser.getInt("server", "max_connections", 20);
g_config.max_request_size = parser.getInt("server", "max_request_size", 8192);
g_config.recv_timeout_ms = parser.getInt("server", "recv_timeout_ms", 5000);
g_config.send_timeout_ms = parser.getInt("server", "send_timeout_ms", 5000);
g_config.max_active_clients = parser.getInt("server", "max_active_clients", 100);
// Load logging settings
g_config.log_to_file = parser.getBool("logging", "log_to_file", true);
g_config.log_level = parser.get("logging", "log_level", "INFO");
g_config.log_dir = parser.get("logging", "log_dir", "logs");
// Resolve www_root to absolute path
std::string nearExe = exeDir + "\\" + g_config.www_root;
std::string parentDir = exeDir.substr(0, exeDir.find_last_of("\\/"));
std::string nearProject = parentDir + "\\" + g_config.www_root;
DWORD attr1 = GetFileAttributesA(nearExe.c_str());
DWORD attr2 = GetFileAttributesA(nearProject.c_str());
if (attr1 != INVALID_FILE_ATTRIBUTES && (attr1 & FILE_ATTRIBUTE_DIRECTORY)) {
g_config.www_root = nearExe;
} else if (attr2 != INVALID_FILE_ATTRIBUTES && (attr2 & FILE_ATTRIBUTE_DIRECTORY)) {
g_config.www_root = nearProject;
}
}
// ============================================================================
// CLIENT HANDLER (runs in separate thread)
// ============================================================================
/**
* @brief Handle a single client connection
* @param client_socket Connected client socket
* @param client_addr Client address information
*
* This function runs in a detached thread. It:
* 1. Checks active connection limit (returns 503 if exceeded)
* 2. Sets socket timeouts
* 3. Reads HTTP request with size limiting
* 4. Parses requested path
* 5. Serves static file or returns error
* 6. Closes socket and decrements active counter
*/
void handleClient(SOCKET client_socket, sockaddr_in client_addr) {
char* client_ip = inet_ntoa(client_addr.sin_addr);
// Increment active client counter
g_active_clients++;
int current_active = g_active_clients.load();
g_logger.info("Connected: " + std::string(client_ip) +
" (active: " + std::to_string(current_active) + "/" +
std::to_string(g_config.max_active_clients) + ")");
// Check connection limit - return 503 if server is overloaded
if (current_active > g_config.max_active_clients) {
g_logger.warn("Too many clients (" + std::to_string(current_active) +
"), rejecting " + std::string(client_ip));
sendResponse(client_socket, 503, "text/html",
"<html><h1>503 Service Unavailable</h1><p>Server overloaded</p></html>");
closesocket(client_socket);
g_active_clients--;
return;
}
// Apply socket timeouts to prevent hung connections
setSocketTimeouts(client_socket);
// Read HTTP request with size limiting
std::string request;
std::vector<char> buffer(g_config.buffer_size, 0);
int total_received = 0;
bool header_complete = false;
while (total_received < g_config.max_request_size) {
// Calculate remaining space in buffer and under limit
int space_in_buffer = g_config.buffer_size - total_received;
int space_under_limit = g_config.max_request_size - total_received;
int to_read = std::min(space_in_buffer, space_under_limit);
int bytes = recv(client_socket, buffer.data() + total_received, to_read, 0);
if (bytes <= 0) {
// Handle errors: timeout, reset, or other
int err = WSAGetLastError();
if (err == WSAETIMEDOUT) {
g_logger.warn("recv timeout for " + std::string(client_ip));
} else if (err == WSAECONNRESET) {
g_logger.warn("Connection reset by " + std::string(client_ip));
} else {
g_logger.error("recv failed for " + std::string(client_ip) +
" (code: " + std::to_string(err) + ")");
}
closesocket(client_socket);
g_active_clients--;
return;
}
total_received += bytes;
request.append(buffer.data(), bytes);
// Check if we have the complete HTTP header (ends with \r\n\r\n)
if (request.find("\r\n\r\n") != std::string::npos) {
header_complete = true;
break;
}
// Check if request exceeds maximum allowed size
if (total_received >= g_config.max_request_size) {
g_logger.warn("Request too large (" + std::to_string(total_received) +
" bytes) from " + std::string(client_ip));
sendResponse(client_socket, 413, "text/html",
"<html><h1>413 Payload Too Large</h1><p>Max: " +
std::to_string(g_config.max_request_size) + " bytes</p></html>");
closesocket(client_socket);
g_active_clients--;
return;
}
}
// Verify we got a complete header
if (!header_complete) {
g_logger.warn("Incomplete request from " + std::string(client_ip));
closesocket(client_socket);
g_active_clients--;
return;
}
// Parse the requested path
std::string path = parsePath(request);
g_logger.info(std::string(client_ip) + " requested: " + path +
" (" + std::to_string(total_received) + " bytes)");
// Build absolute file path
std::string file_path = g_config.www_root + path;
// Path traversal protection: reject paths containing ".."
if (file_path.find("..") != std::string::npos) {
g_logger.warn("Forbidden path: " + path);
sendResponse(client_socket, 403, "text/html",
"<html><h1>403 Forbidden</h1></html>");
closesocket(client_socket);
g_active_clients--;
return;
}
// Attempt to read and serve the requested file
std::string content;
if (readFile(file_path, content)) {
g_logger.info("Serving: " + file_path + " (" + std::to_string(content.size()) + " bytes)");
sendResponse(client_socket, 200, getMimeType(path), content);
} else {
g_logger.warn("Not found: " + file_path);
std::string body = "<html><h1>404 Not Found</h1><p>" + path + "</p></html>";
sendResponse(client_socket, 404, "text/html", body);
}
// Cleanup
closesocket(client_socket);
g_active_clients--;
g_logger.info("Disconnected: " + std::string(client_ip) +
" (active: " + std::to_string(g_active_clients.load()) + ")");
}
// ============================================================================
// SIGNAL HANDLING (Graceful Shutdown)
// ============================================================================
/**
* @brief Windows console control handler for graceful shutdown
* @param signal Control signal type (CTRL_C_EVENT, CTRL_BREAK_EVENT, etc.)
* @return TRUE if handled, FALSE to pass to next handler
*
* On Ctrl+C:
* 1. Sets g_running to false (stops accept loop)
* 2. Closes listening socket (unblocks accept())
* 3. Logs shutdown message
*/
BOOL WINAPI consoleHandler(DWORD signal) {
if (signal == CTRL_C_EVENT || signal == CTRL_BREAK_EVENT) {
g_logger.info("Shutdown signal received, stopping server...");
g_running = false;
// Close listening socket to unblock accept()
if (g_server_socket != INVALID_SOCKET) {
closesocket(g_server_socket);
g_server_socket = INVALID_SOCKET;
}
return TRUE;
}
return FALSE;
}
// ============================================================================
// MAIN ENTRY POINT
// ============================================================================
/**
* @brief Main server entry point
*
* Startup sequence:
* 1. Load configuration from INI
* 2. Initialize logger
* 3. Register Ctrl+C handler
* 4. Initialize WinSock
* 5. Create, bind, and listen on socket
* 6. Accept loop: spawn thread per connection
* 7. Graceful cleanup on shutdown
*/
int main() {
// Step 1: Load configuration
initConfig();
// Step 2: Initialize logging
g_logger.init(g_config.log_dir, g_config.log_level, g_config.log_to_file);
// Log startup banner with configuration summary
g_logger.info("========================================");
g_logger.info("C++ HTTP Server starting...");
g_logger.info("Host: " + g_config.host);
g_logger.info("Port: " + std::to_string(g_config.port));
g_logger.info("WWW Root: " + g_config.www_root);
g_logger.info("Buffer Size: " + std::to_string(g_config.buffer_size));
g_logger.info("Max Connections: " + std::to_string(g_config.max_connections));
g_logger.info("Max Request Size: " + std::to_string(g_config.max_request_size) + " bytes");
g_logger.info("Recv Timeout: " + std::to_string(g_config.recv_timeout_ms) + " ms");
g_logger.info("Send Timeout: " + std::to_string(g_config.send_timeout_ms) + " ms");
g_logger.info("Max Active Clients: " + std::to_string(g_config.max_active_clients));
g_logger.info("========================================");
// Step 3: Register Ctrl+C handler for graceful shutdown
SetConsoleCtrlHandler(consoleHandler, TRUE);
g_logger.info("Press Ctrl+C to stop gracefully");
// Step 4: Initialize Windows Sockets API
WSADATA wsadata;
if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) {
g_logger.error("WSAStartup failed");
return 1;
}
// Step 5: Create listening socket
g_server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (g_server_socket == INVALID_SOCKET) {
g_logger.error("Socket creation failed");
WSACleanup();
return 1;
}
// Configure server address
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = inet_addr(g_config.host.c_str());
address.sin_port = htons(g_config.port);
// Bind socket to address and port
if (bind(g_server_socket, (SOCKADDR*)&address, sizeof(address)) != 0) {
g_logger.error("Bind failed on " + g_config.host + ":" + std::to_string(g_config.port));
closesocket(g_server_socket);
WSACleanup();
return 1;
}
// Start listening for incoming connections
if (listen(g_server_socket, g_config.max_connections) != 0) {
g_logger.error("Listen failed");
closesocket(g_server_socket);
WSACleanup();
return 1;
}
g_logger.info("Server listening on http://" + g_config.host + ":" + std::to_string(g_config.port));
// Step 6: Main accept loop
while (g_running) {
struct sockaddr_in client_addr;
int len = sizeof(client_addr);
// Block until client connects or shutdown signal received
SOCKET new_socket = accept(g_server_socket, (SOCKADDR*)&client_addr, &len);
// Check if accept was interrupted by shutdown
if (new_socket == INVALID_SOCKET) {
if (!g_running) {
g_logger.info("Accept interrupted by shutdown");
break;
}
g_logger.error("Accept failed, code: " + std::to_string(WSAGetLastError()));
continue;
}
// Spawn detached thread to handle client
std::thread(handleClient, new_socket, client_addr).detach();
}
// Step 7: Graceful shutdown
g_logger.info("Server shutting down...");
g_logger.info("Waiting for " + std::to_string(g_active_clients.load()) + " active clients...");
// Brief pause to let active threads finish naturally
// Note: In production, consider using a thread pool with join()
Sleep(1000);
// Cleanup resources
if (g_server_socket != INVALID_SOCKET) {
closesocket(g_server_socket);
}
WSACleanup();
g_logger.info("Server stopped. Goodbye!");
return 0;
}