├── CMakeLists.txt ├── README.md ├── html_parser.cpp ├── html_parser.h ├── main.cpp └── main.html /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(Basic-Http-Server-cpp) 2 | 3 | find_package( Threads REQUIRED ) 4 | 5 | add_executable(server 6 | ${CMAKE_CURRENT_SOURCE_DIR}/html_parser.cpp 7 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 8 | ) 9 | 10 | if(WIN32) 11 | target_link_libraries(server ws2_32) 12 | endif() 13 | 14 | if(UNIX) 15 | #target_compile_options(server PUBLIC "-pthread") Replaced with 16 | endif() 17 | 18 | target_link_libraries(server Threads::Threads) 19 | 20 | file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/main.html 21 | DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basic-Http-Server-Cpp 2 | HTTP server implemented in C++ without any third party library. 3 | 4 | * Only used standard libraries 5 | 6 | ## Features 7 | 8 | * Implemented only using GNU C/C++ Library 9 | * Handles GET/POST requests 10 | * Asynchronous request handling 11 | * Multithreaded/Thread Pooling 12 | * Handles over 20,000 concurrent connections. 13 | * Serves over 100,000 requests per second on a modern personal computer 14 | ## Compile and Run 15 | 16 | * Compile with a C++11 compliant compiler: 17 | ```sh 18 | make 19 | ./server 20 | ``` 21 | ### Run 22 | It is basic dictionary app. With 2 functions add word to dictionary and check if word contains in the dictionary 23 | 24 | * Check word if it is in the dictionary with Get Request 25 | ```sh 26 | http://127.0.0.1/check?name=EXAMPLE_WORD 27 | ``` 28 | * Add word to the dictionary with Post Request 29 | ```sh 30 | http://127.0.0.1/add 31 | Body should contain raw text => "name=test_word" 32 | ``` 33 | 34 | ![](https://i.imgur.com/9H5LdpH.png) 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ## Perfonmance Results 43 | 44 | Tested in Ubuntu 18.04 , Intel Core m-5y70 2.6GHZ-8GB-256SSD 45 | * Benchmarking http://127.0.0.1/ 46 | ``` 47 | kayacan@therion:/home/kayacan# ab -c 10000 -n 100000 http://127.0.0.1/ 48 | This is ApacheBench, Version 2.3 <$Revision: 1807734 $> 49 | 50 | Benchmarking 127.0.0.1 (be patient) 51 | 52 | Server Hostname: 127.0.0.1 53 | Server Port: 80 54 | 55 | Document Path: / 56 | Document Length: 468 bytes 57 | 58 | Concurrency Level: 10000 59 | Time taken for tests: 3.385 seconds 60 | Complete requests: 100000 61 | Failed requests: 0 62 | Total transferred: 53500000 bytes 63 | HTML transferred: 46800000 bytes 64 | Requests per second: 29545.55 [#/sec] (mean) 65 | Time per request: 338.461 [ms] (mean) 66 | Time per request: 0.034 [ms] (mean, across all concurrent requests) 67 | Transfer rate: 15436.39 [Kbytes/sec] received 68 | 69 | Connection Times (ms) 70 | min mean[+/-sd] median max 71 | Connect: 0 115 385.7 39 3121 72 | Processing: 1 39 46.1 42 327 73 | Waiting: 0 30 40.9 31 309 74 | Total: 3 154 400.0 83 3248 75 | 76 | ``` 77 | 78 | 79 | * Benchmarking Get Request http://127.0.0.1/check?name=EXAMPLE 80 | ``` 81 | kayacan@therion:/home/kayacan# ab -c 10000 -n 100000 localhost/check?name=EXAMPLE 82 | This is ApacheBench, Version 2.3 <$Revision: 1807734 $> 83 | 84 | 85 | Server Software: 86 | Server Hostname: localhost 87 | Server Port: 80 88 | 89 | Document Path: /check?name=EXAMPLE 90 | Document Length: 498 bytes 91 | 92 | Concurrency Level: 10000 93 | Time taken for tests: 3.059 seconds 94 | Complete requests: 100000 95 | Failed requests: 0 96 | Total transferred: 56500000 bytes 97 | HTML transferred: 49800000 bytes 98 | Requests per second: 32691.55 [#/sec] (mean) 99 | Time per request: 305.889 [ms] (mean) 100 | Time per request: 0.031 [ms] (mean, across all concurrent requests) 101 | Transfer rate: 18037.82 [Kbytes/sec] received 102 | 103 | Connection Times (ms) 104 | min mean[+/-sd] median max 105 | Connect: 35 122 183.8 84 1148 106 | Processing: 37 100 37.0 93 429 107 | Waiting: 10 52 41.3 41 395 108 | Total: 125 222 198.9 216 1480 109 | ``` 110 | 111 | 112 | 113 | * Benchmarking Post Request http://127.0.0.1/add (body="Add_Keyword") #I did not try to add different words yet 114 | ``` 115 | kayacan@therion:/home/kayacan# ab -c 10000 -p t -n 100000 http://127.0.0.1/add 116 | This is ApacheBench, Version 2.3 <$Revision: 1807734 $> 117 | 118 | Server Software: 119 | Server Hostname: 127.0.0.1 120 | Server Port: 80 121 | 122 | Document Path: /add 123 | Document Length: 468 bytes 124 | 125 | Concurrency Level: 10000 126 | Time taken for tests: 3.093 seconds 127 | Complete requests: 100000 128 | Failed requests: 0 129 | Total transferred: 53500000 bytes 130 | Total body sent: 13100000 131 | HTML transferred: 46800000 bytes 132 | Requests per second: 32332.34 [#/sec] (mean) 133 | Time per request: 309.288 [ms] (mean) 134 | Time per request: 0.031 [ms] (mean, across all concurrent requests) 135 | Transfer rate: 16892.38 [Kbytes/sec] received 136 | 4136.27 kb/s sent 137 | 21028.65 kb/s total 138 | 139 | Connection Times (ms) 140 | min mean[+/-sd] median max 141 | Connect: 6 87 216.7 36 1100 142 | Processing: 7 43 24.0 40 265 143 | Waiting: 4 26 19.4 24 253 144 | Total: 23 130 220.5 75 1172 145 | 146 | ``` 147 | -------------------------------------------------------------------------------- /html_parser.cpp: -------------------------------------------------------------------------------- 1 | #include "html_parser.h" 2 | 3 | 4 | html_parser::html_parser(char * buffer,int buffer_length) 5 | { 6 | if(buffer_length==0) return ; 7 | std::vector lines; 8 | int header_ends=0; 9 | for (int i = 0; i < buffer_length; i++) 10 | { 11 | std::string a; 12 | while(i=0 && buffer[t]!='\n' && buffer[t]!='=') 47 | t--; 48 | t++; 49 | while(t 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | class html_parser{ 8 | 9 | private: 10 | int request_type; // get => 0 , post => 1 , put=> 2 11 | std::string url; 12 | std::string text; 13 | std::map request_inputs; 14 | void url_parser(std::string url); 15 | public: 16 | int get_request_type(); 17 | std::string get_input(std::string a); 18 | std::string get_text(); 19 | html_parser(char * buffer,int buffer_length); 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | //Unix 16 | #ifdef _unix 17 | #include 18 | #include 19 | #include 20 | // #include // replaced by 21 | #endif //_unix_ 22 | 23 | //Windows 24 | #ifdef _WIN32 25 | #include 26 | #endif //WIN32 27 | 28 | #include "html_parser.h" 29 | 30 | //Thread Queue Lock 31 | std::mutex QueueLock; 32 | std::queue < int > event_queue; // Events are Socket Numbers 33 | 34 | //Can be upgraded by letting multiple reading using semaphore 35 | std::mutex DictionaryLock; // Mutex For Reading-Writing of dictionary.txt 36 | 37 | std::mutex CoutLock; 38 | 39 | class website_handler { 40 | private: 41 | std::set < std::string > dictionary; // Current dictionary set 42 | 43 | std::map < std::string, char * > page; // "page name" -> page_contents 44 | 45 | char * readFile(const char * fileName) // Read File and return the output as a char pointer 46 | { 47 | FILE * file = fopen(fileName, "r"); 48 | char * code; 49 | size_t n = 0; 50 | int c; 51 | if (file == NULL) 52 | return NULL; 53 | code = (char * ) malloc(50000 * sizeof(char)); 54 | while ((c = fgetc(file)) != EOF) 55 | code[n++] = (char) c; 56 | code[n] = '\0'; 57 | return code; 58 | } 59 | public: 60 | website_handler() { 61 | // init_dictionary(); 62 | } 63 | void load(const char * filename) // Loading html file to proccess 64 | { 65 | page[filename] = readFile(filename); 66 | } 67 | 68 | /** 69 | * @brief Returns the wanted page for given params 70 | * @param *filename: 71 | * @param request_type: get => 0 , post => 1 , put=> 2 72 | * @param input: Add Get request input 73 | * @param text: Add Body of request(for POST Request) 74 | * @retval Char Pointer 75 | */ 76 | char * get_page(const char * filename, int request_type, std::string input, std::string text) { 77 | if(std::fstream(filename).fail()){ // Check if the file exists 78 | std::cerr << filename << " could not be opened! Check if the path is correct\n"; 79 | return ""; 80 | } 81 | std::string str = "HTTP/1.1 200 Okay\r\nContent-Type: text/html; charset=ISO-8859-4 \r\n\r\n" + std::string(page[filename]); 82 | if (request_type == 1) { 83 | add_dictionary(text); 84 | } else if (request_type == 0 && input != "") { 85 | int is_contains = check_dictionary(input); 86 | if (is_contains) { 87 | input += " is found in your Dictionary"; 88 | str.replace(str.find(""), 9, input.c_str()); 89 | } else { 90 | input += " is NOT found in your Dictionary"; 91 | str.replace(str.find(""), 9, input.c_str()); 92 | } 93 | } 94 | char * cstr = new char[str.length() + 1]; 95 | strcpy(cstr, str.c_str()); 96 | return cstr; 97 | } 98 | 99 | /** 100 | * @brief Add Word To Dictionary (Both Set and File) 101 | * @note 102 | * @param word: Word To be added, c++ stl format 103 | */ 104 | void add_dictionary(std::string word) // 105 | { 106 | if(word.empty()) return; // Don't write empty words 107 | DictionaryLock.lock(); 108 | if (dictionary.count(word) == 0) { 109 | std::ofstream fDictionary("dictionary.txt" , std::ios::app); 110 | if(!fDictionary.good()){ 111 | std::cerr << "Failed to open file\n"; 112 | fDictionary.close(); 113 | } 114 | fDictionary << word.c_str() << "\n"; 115 | fDictionary.close(); 116 | dictionary.insert(word); 117 | } 118 | DictionaryLock.unlock(); 119 | } 120 | /** 121 | * @brief Add Word To Dictionary (Both Set and File) 122 | * @note 123 | * @param word: Word To be added, c++ stl format 124 | */ 125 | int check_dictionary(std::string word) { 126 | DictionaryLock.lock(); 127 | int return_value = dictionary.count(word); 128 | DictionaryLock.unlock(); 129 | return return_value; 130 | } 131 | /** 132 | * @brief Read dictionary.txt and add all to dictionary set for fast access 133 | */ 134 | void init_dictionary() { 135 | DictionaryLock.lock(); 136 | dictionary.insert(""); 137 | std::ifstream fp("dictionary.txt"); 138 | if (!fp.good()) //if file does not exist, create it 139 | { 140 | std::ofstream created("dictionary.txt"); 141 | created.close(); 142 | fp.close(); 143 | DictionaryLock.unlock(); 144 | return; 145 | } 146 | std::string word; 147 | char c; 148 | while ((c = fp.get()) != EOF) { 149 | if (c == '\n') { 150 | dictionary.insert(word); 151 | word.clear(); 152 | } else word.push_back(c); 153 | } 154 | if (word != "") 155 | dictionary.insert(word); 156 | fp.close(); 157 | DictionaryLock.unlock(); 158 | } 159 | }; 160 | 161 | website_handler website; 162 | 163 | class server { 164 | private: 165 | 166 | int file_descriptor; 167 | int sizeof_address; 168 | int THREAD_COUNT; 169 | 170 | struct sockaddr_in address; 171 | int server_up; 172 | int new_socket() // New socket for listen 173 | { 174 | file_descriptor = socket(AF_INET, SOCK_STREAM, 0); //! Fails 175 | int error = WSAGetLastError(); 176 | if (file_descriptor == INVALID_SOCKET) { 177 | std::cerr << "ERROR Invalid Socket: " << error ; 178 | return -1; 179 | } 180 | return 0; 181 | } 182 | int bind_address() // Bind address to socket 183 | { 184 | int return_value = bind(file_descriptor, (struct sockaddr * ) & address, sizeof(address)); 185 | if (return_value < 0) { 186 | perror("ERROR: Couldn't bind\n"); 187 | return -1; 188 | } 189 | return 0; 190 | } 191 | int start_listen(int k = 100000) // k is the max size of the queue 192 | { 193 | int return_value = listen(file_descriptor, k); 194 | if (return_value < 0) { 195 | perror("ERROR: Couldn't listen\n"); 196 | return -1; 197 | } 198 | return 0; 199 | } 200 | /** 201 | * @brief accepts new requests from file_descriptor, 202 | * @retval int, the value of the connection socket 203 | */ 204 | int accept_connection() { 205 | int connection_value = accept(file_descriptor, (struct sockaddr * ) & address, (int * ) & sizeof_address); 206 | if (connection_value < 0) { 207 | perror("ERROR: Connection Accept Failure\n"); 208 | return -1; 209 | } 210 | return connection_value; 211 | } 212 | public: 213 | 214 | /** 215 | * @brief Server Constructer 216 | * @param internet_address: internet address 217 | * @param port_number: port number, Default:80 218 | * @param THREAD_COUNT: Number Of Thread Count for a proccess 219 | * @retval 220 | */ 221 | server(int internet_address, int port_number = 80, int THREAD_COUNT = 10) // 80 for http 222 | { 223 | this -> THREAD_COUNT = THREAD_COUNT; 224 | server_up = 0; 225 | sizeof_address = sizeof(address); 226 | address.sin_family = AF_INET; // Internet Based 227 | if (internet_address == 0) 228 | address.sin_addr.s_addr = INADDR_ANY; // accept any incoming 229 | else 230 | address.sin_addr.s_addr = internet_address; 231 | address.sin_port = htons(80); // htons(port_number) 232 | memset(address.sin_zero, '\0', sizeof address.sin_zero); 233 | if (new_socket() == -1) 234 | return; 235 | if (bind_address() == -1) 236 | return; 237 | if (start_listen() == -1) 238 | return; 239 | server_up = 1; 240 | }~server() { 241 | shutdown(file_descriptor, 2); 242 | } 243 | /** 244 | * 245 | */ 246 | static void * connection_thread(void * argv) { 247 | while (true) { 248 | int socket_num; 249 | QueueLock.lock(); 250 | if (event_queue.empty() == false) { 251 | socket_num = event_queue.front(); 252 | event_queue.pop(); 253 | QueueLock.unlock(); 254 | } else { 255 | QueueLock.unlock(); 256 | continue; 257 | } 258 | 259 | char buffer[1000] = { 260 | 0 261 | }; 262 | memset(buffer, 0 , 1000); 263 | int buffer_length = recv(socket_num, buffer, 1000, 0); //! Returning wrong lnegth 264 | if (buffer_length < 0) { 265 | perror("ERROR: Receiving Failure\n"); 266 | return NULL; 267 | } 268 | html_parser request(buffer, buffer_length); 269 | /* CoutLock.lock(); 270 | std::cout << "Raw request: " << (char*)buffer; 271 | CoutLock.unlock(); */ 272 | 273 | char * message = website.get_page("main.html", request.get_request_type(), request.get_input("name"), request.get_text()); 274 | 275 | int length = strlen(message); 276 | int send_value = send(socket_num, message, length, 0); 277 | if (send_value < 0) { 278 | perror("ERROR: Sending Failure\n"); 279 | return NULL; 280 | } 281 | closesocket(socket_num); 282 | } 283 | } 284 | void start() { 285 | if (server_up == 0){ 286 | std::cerr << "Server failed to start!\n"; 287 | return; 288 | } 289 | //unix 290 | #ifdef _unix_ 291 | pthread_t ptid[THREAD_COUNT]; 292 | for (int i = 0; i < THREAD_COUNT; i++) { 293 | int return_value = pthread_create( & ptid[i], NULL, connection_thread, (void * ) NULL); 294 | if (return_value < 0) { 295 | perror("ERROR: Couldn't create thread\n"); 296 | exit(1); 297 | } 298 | } 299 | #endif// unix 300 | 301 | #ifdef _WIN32 302 | std::vector ptid; 303 | for (int i = 0; i < THREAD_COUNT; i++) { 304 | ptid.push_back(std::thread(connection_thread , (void*)NULL)); 305 | } 306 | #endif //WIN32 307 | while (1) { 308 | int socket_num = accept_connection(); 309 | QueueLock.lock(); 310 | event_queue.push(socket_num); 311 | QueueLock.unlock(); 312 | } 313 | } 314 | }; 315 | 316 | int main(int argc, char** argv) { 317 | #ifdef _WIN32 318 | WSADATA data; 319 | WSAStartup(MAKEWORD(2 , 2) , &data); 320 | #endif 321 | website.init_dictionary(); 322 | website.load("main.html"); 323 | server basic_server(0, 80, 10); 324 | basic_server.start(); 325 | return 0; 326 | } 327 | -------------------------------------------------------------------------------- /main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Basic Dictionary | Demo

5 | 6 |
7 |
8 | 9 |
10 | 11 |
12 |

13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |

Result:

22 | 23 | 24 | 25 | --------------------------------------------------------------------------------