I'm working on a project (studying) and I have a lot of time for proof of concepts and write something from scratch.
Basically I'm creating an HTTP server (simple, but not too simple) in C++ using sockets and multi-threading.
There are two topics that I'm concerned about: the design pattern of my code structure and the efficiency of my thread implementation. Obs: I'm a little worried about C++ best practices, since I'm diving too fast into C++ (am I abusing of std items, since this requires a low-level implementation).
Server is the main file, that calls Routes, Request and Response. Request and Response contains Struct (that contains Status Code). And Routes contains Request and Response.
IMPORTANT: I'm using nlohmann/json and j-ulrich status-codes.
Server (Accepting Sockets and Multi-threading):
#pragma once
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <iostream>
#include <unordered_map>
#include <thread>
#include <mutex>
#include <condition_variable>
#include "Server/Request.h"
#include "Server/Response.h"
#include "Server/Struct.h"
#include "Server/Routes.h"
#include "Tools/Logger.h"
class Server {
public:
Server(unsigned int port, unsigned int max_connections = 64, unsigned int thread_count = 5);
~Server();
bool setRoute(std::string path, Struct::Methods method, void (*callback)(Request*, Response*));
bool doListen();
bool doStop();
private:
unsigned int _port;
unsigned int _max_connections;
unsigned int _thread_count;
std::mutex _mutex;
std::condition_variable _condition;
bool _signal;
std::vector<unsigned int> _queue;
std::thread* _thread_consume;
std::thread* _thread_process;
Routes* _routes;
int _socket;
struct sockaddr_in _address;
bool _listen;
bool _doStop();
bool _doCreateSocket(int& socket_in);
bool _doBindSocket(int file_descriptor);
void _doConsumeSocket();
void _doProcessSocket(int id);
bool _doProcessRequest(Request* request, Response* response);
};
#include "Server/Server.h"
Server::Server(unsigned int port, unsigned int max_connections, unsigned int thread_count) {
if (port > 65535) {
Logger::doSendMessage(Logger::TYPES::ERROR, "[Port must be something between 0 and 65535 on Server::Constructor.");
}
if (max_connections < 1) {
Logger::doSendMessage(Logger::TYPES::ERROR, "Max connections can't be lower than 1 on Server::Constructor.");
}
_port = port;
_max_connections = max_connections;
_thread_count = thread_count;
_routes = new Routes();
int status = _doCreateSocket(_socket);
if (!status) {
Logger::doSendMessage(Logger::TYPES::ERROR, "Failed to create socket on Server::Constructor.");
}
if (!_doBindSocket(_socket)) {
Logger::doSendMessage(Logger::TYPES::ERROR, "Failed to bind socket on Server::Constructor.");
};
_signal = false;
_listen = false;
}
Server::~Server() {
_doStop();
shutdown(_socket, SHUT_RD);
close(_socket);
try {
_thread_consume->join();
for (size_t i = 0; i < _thread_count; i++) {
_thread_process[i].join();
}
} catch (...) {}
delete _thread_consume;
delete[] _thread_process;
delete _routes;
}
bool Server::setRoute(std::string path, Struct::Methods method, void (*callback)(Request*, Response*)) {
return _routes->setRoute(path, method, callback);
}
bool Server::doListen() {
if (_listen) return false;
int status;
status = listen(_socket, _max_connections);
if (status < 0) return false;
Logger::doSendMessage(Logger::TYPES::INFO, "Server running with success at port " + std::to_string(_port) + ".");
_listen = true;
_thread_consume = new std::thread(&Server::_doConsumeSocket, this);
_thread_process = new std::thread[_thread_count];
for (size_t i = 0; i < _thread_count; i++) {
_thread_process[i] = std::thread(&Server::_doProcessSocket, this, i);
}
return true;
}
bool Server::doStop() {
return _doStop();
}
bool Server::_doStop() {
if (!_listen) return false;
{
std::lock_guard<std::mutex> lock(_mutex);
_listen = false;
}
_condition.notify_one();
return true;
}
bool Server::_doCreateSocket(int& socket_in) {
int file_descriptor = socket(AF_INET, SOCK_STREAM, 0);
if (file_descriptor == 0) return false;
int error;
int opt = 1;
error = setsockopt(file_descriptor, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
if (error) return false;
socket_in = file_descriptor;
return true;
}
bool Server::_doBindSocket(int file_descriptor) {
if (!file_descriptor) return false;
_address.sin_family = AF_INET;
_address.sin_addr.s_addr = INADDR_ANY;
_address.sin_port = htons(_port);
int status;
status = bind(file_descriptor, (struct sockaddr*) &_address, sizeof(_address));
if (status < 0) return false;
return true;
}
void Server::_doConsumeSocket() {
int socket_in;
int address_size = sizeof(_address);
while (_listen) {
socket_in = accept(_socket, (struct sockaddr*) &_address, (socklen_t*) &address_size);
if (socket_in < 0) continue;
{
std::lock_guard<std::mutex> lock(_mutex);
_queue.push_back(socket_in);
_signal = true;
}
_condition.notify_one();
}
}
void Server::_doProcessSocket(int id) {
while (_listen) {
int queue_size = 0;
{
std::unique_lock<std::mutex> lock(_mutex);
_condition.wait(lock,
[this] {
if (this->_signal) return true;
if (!this->_listen && !this->_queue.size()) return true;
return false;
}
);
queue_size = _queue.size();
}
if (!queue_size) {
{
std::lock_guard<std::mutex> lock(_mutex);
_signal = false;
}
_condition.notify_one();
continue;
}
int socket_in = 0;
{
std::lock_guard<std::mutex> lock(_mutex);
socket_in = _queue[0];
_queue.erase(_queue.begin());
}
Request* request = new Request(socket_in);
Response* response = new Response(socket_in);
int status = _doProcessRequest(request, response);
delete request;
delete response;
close(socket_in);
}
}
bool Server::_doProcessRequest(Request* request, Response* response) {
if (!request->isValid()) {
response->doSendError(HttpStatus::Code::BadRequest, "Invalid request.");
return false;
}
std::string path = request->getPath();
Struct::Methods method = request->getMethod();
Routes::Route route;
if (!(_routes->getRoute(path, method, route) && route.isValid())) {
response->doSendError(HttpStatus::Code::Forbidden, "Path invalid/not found.");
return false;
}
if (route.method != method) {
response->doSendError(HttpStatus::Code::MethodNotAllowed, "Method invalid/not found.");
return false;
}
void (*callback)(Request*, Response*) = route.callback;
callback(request, response);
if (!response->isSent()) {
response->doSendError(HttpStatus::Code::ServiceUnavailable, "Resource was not found or can't respond now.");
}
return true;
}
Request (parsing) and Response (sending)
Request
#pragma once
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <regex>
#include <json.hpp>
#include "Server/Struct.h"
using json = nlohmann::json;
class Request {
public:
Request(int socket, unsigned int buffer_size = 1024);
~Request();
bool isValid();
std::string getPath() {return _attributes.path;}
Struct::Methods getMethod() {return _attributes.method;}
std::unordered_map<std::string, std::string> getHeaders() {return _attributes.headers;}
std::string getHeader(std::string header) {return _attributes.headers[header];}
json getBody() {return _attributes.body;}
private:
int _socket;
unsigned int _buffer_size;
std::string _data;
Struct::Attributes _attributes;
bool _status;
std::string _doReceiveData(int sock_in);
bool _doParseData(std::string data, Struct::Attributes& attributes);
std::vector<std::string> _doSplitText(std::string text, std::string delimiter);
std::vector<std::string> _doSplitText(std::string text, std::string delimiter, int lock);
};
Request::Request(int socket, unsigned int buffer_size) {
_socket = socket;
_buffer_size = buffer_size;
_status = false;
_data = _doReceiveData(_socket);
if (!_data.length()) return;
bool result;
result = _doParseData(_data, _attributes);
if (!result) return;
if (!_attributes.isValidRequest()) return;
_status = true;
}
Request::~Request() {
}
bool Request::isValid() {
return _status;
}
std::string Request::_doReceiveData(int sock_in) {
char* buffer = new char[_buffer_size];
memset(buffer, '\0', _buffer_size);
read(sock_in, buffer, _buffer_size);
std::string data;
data.assign(buffer);
delete[] buffer;
return data;
}
bool Request::_doParseData(std::string data, Struct::Attributes& attributes) {
std::string delimiter = "\r\n";
std::vector<std::string> rows = _doSplitText(data, delimiter);
if (!rows.size()) return false;
std::string header = rows[0];
rows.erase(rows.begin());
if (!header.length()) return false;
std::vector<std::string> parsed_header = _doSplitText(header, std::string(" "));
if (parsed_header.size() < 2) return false;
Struct::Methods method = Struct::doParseHttpMethod(parsed_header[0]);
if (method == Struct::Methods::NONE) return false;
std::string path = parsed_header[1];
std::unordered_map<std::string, std::string> headers;
for (size_t i = 0; i < rows.size(); i++) {
std::string row = rows[i];
delimiter = ":";
std::vector<std::string> splited = _doSplitText(row, delimiter, true);
if (splited.size() != 2) continue;
headers[splited[0]] = splited[1];
}
_attributes.method = method;
_attributes.path = path;
_attributes.headers = headers;
std::string content_length = headers["Content-Length"];
int content_size = 0;
if (content_size = atoi(content_length.c_str())) {
std::string body = data.substr(data.length() - content_size, data.length());
json parsed_body = json::parse(body, nullptr, false);
if (parsed_body != NULL && !parsed_body.is_discarded()) _attributes.body = parsed_body;
}
return true;
}
std::vector<std::string> Request::_doSplitText(std::string text, std::string delimiter) {
std::vector<std::string> result;
int delimiter_length = delimiter.length();
std::string block;
std::string region;
int index = 0;
for (size_t i = 0; i < text.length(); i++) {
block = text.substr(i, delimiter_length);
if (block.length() != delimiter_length) continue;
if (block == delimiter) {
region = text.substr(index, i - index);
result.push_back(region);
index = i + delimiter_length;
}
}
return result;
}
std::vector<std::string> Request::_doSplitText(std::string text, std::string delimiter, int lock) {
...
}
Response
#pragma once
#include <unistd.h>
#include <iostream>
#include <string>
#include <unordered_map>
#include "Server/Struct.h"
class Response {
public:
Response(int socket_in);
~Response();
bool isSent() {return _sent;}
void setCode(HttpStatus::Code code) {_attributes.code = code;}
void setHeader(std::string key, std::string value) {_attributes.headers[key] = value;}
void setBody(json body) {_attributes.body = body;}
void doClearHeaders() {_attributes.headers.clear();}
void doClearBody() {_attributes.body = json::value_t::object;}
bool doSendSuccess();
bool doSendError(HttpStatus::Code code, const std::string& message);
private:
int _socket;
bool _sent;
Struct::Attributes _attributes;
bool _doSendPayload();
bool _doCreatePayload(std::string& payload);
};
#include "Response.h"
Response::Response(int socket_in) {
_socket = socket_in;
_sent = false;
}
Response::~Response() {
}
bool Response::doSendSuccess() {
setCode(HttpStatus::Code::OK);
setHeader("Connection", "Closed");
return _doSendPayload();
}
bool Response::doSendError(HttpStatus::Code code, const std::string& message) {
setCode(code);
doClearHeaders();
doClearBody();
setHeader("Connection", "Closed");
json body;
body["error"] = {};
body["error"]["code"] = code;
body["error"]["message"] = message;
setBody(body);
return _doSendPayload();
}
bool Response::_doSendPayload() {
if (_sent) return false;
int status;
setHeader("Server", "Dark");
setHeader("Content-Type", "application/json");
std::string payload;
status = _doCreatePayload(payload);
if (!status) return false;
status = write(_socket, payload.c_str(), payload.size());
if (status < 1) return false;
_sent = true;
return true;
}
bool Response::_doCreatePayload(std::string& payload) {
std::string current_payload;
std::string data = _attributes.body.dump(4);
int data_length = data.size();
if (data_length) {
_attributes.headers["Content-Length"] = std::to_string(data_length);
}
current_payload += _attributes.version + " " + std::to_string((int) _attributes.code) + " " + HttpStatus::getReasonPhrase(_attributes.code) + "\r\n";
std::unordered_map<std::string, std::string>::iterator iterator;
for (iterator = _attributes.headers.begin(); iterator != _attributes.headers.end(); iterator++){
std::string key = iterator->first;
std::string value = iterator->second;
current_payload += key + ": " + value + "\r\n";
}
if (data_length) current_payload += "\r\n" + data + "\r\n\r\n";
payload = current_payload;
return true;
}
Routes
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "Server/Request.h"
#include "Server/Response.h"
class Routes {
public:
Routes();
~Routes();
struct Route {
std::string path;
Struct::Methods method;
void (*callback)(Request*, Response*);
bool isValid() {
if (!path.length()) return false;
if (method < Struct::Methods::FIRST || method > Struct::Methods::LAST) return false;
if (callback == nullptr) return false;
return true;
}
};
bool setRoute(std::string path, Struct::Methods method, void (*callback)(Request*, Response*));
bool getRoute(std::string path, Struct::Methods method, Route& route);
private:
std::vector<Route> _routes;
int _getRouteIndex(std::string path, Struct::Methods method);
};
#include "Routes.h"
Routes::Routes() {
}
Routes::~Routes() {
}
bool Routes::setRoute(std::string path, Struct::Methods method, void (*callback)(Request*, Response*)) {
if (path.length() < 1) return false;
if (_getRouteIndex(path, method) >= 0) return false;
if (method < Struct::Methods::FIRST || method > Struct::Methods::LAST) return false;
if (callback == nullptr) return false;
Routes::Route route = {path, method, callback};
_routes.push_back(route);
return true;
}
bool Routes::getRoute(std::string path, Struct::Methods method, Routes::Route& route) {
int index = _getRouteIndex(path, method);
if (index < 0) return false;
route = _routes[index];
return true;
}
int Routes::_getRouteIndex(std::string path, Struct::Methods method) {
for (size_t i = 0; i < _routes.size(); i++) {
Route* route = &_routes[i];
if (route->path == path && route->method == method) {
return i;
}
}
return -1;
}
Struct and StatusCode
StatusCode
Struct
#pragma once
#include <json.hpp>
#include "Server/StatusCode.h"
using json = nlohmann::json;
class Struct {
public:
enum class Methods {
NONE = 0, GET = 1, POST = 2, FIRST = GET, LAST = POST
};
struct Attributes {
const std::string version = "HTTP/1.1";
std::string path;
Methods method;
HttpStatus::Code code;
std::unordered_map<std::string, std::string> headers;
json body;
Attributes() {
code = HttpStatus::Code::InternalServerError;
body = json::value_t::object;
}
bool isValidRequest() {
if (!path.length()) return false;
if (method < Methods::FIRST || method > Methods::LAST) return false;
if (!headers.size()) return false;
return true;
}
bool isValidResponse() {
if (!headers.size()) return false;
return true;
}
};
static Methods doParseHttpMethod(std::string value) {
Methods target = Methods::NONE;
if (value == "GET") target = Methods::GET;
if (value == "POST") target = Methods::POST;
return target;
}
private:
};
Main (usage):
#include "Server/Server.h"
void exec(Request* request, Response* response) {
json body;
body["foo"] = 123;
body["bar"] = true;
response->setBody(body);
response->doSendSuccess();
}
int main(int argc, char* argv[]) {
Server* server = new Server(5000);
server->setRoute("/getStatus", Struct::Methods::GET, exec);
server->setRoute("/postStatus", Struct::Methods::POST, exec);
server->doListen();
// let threads live for some time before they eternaly be gone
/*
actually I'm stuck with this sleep, I don't know how to hold
this until caller call doStop() without using while and
consuming process power
*/
sleep(30);
delete server;
return 1;
}
Compile
#!/bin/bash
sudo g++ \
main.cpp \
-I lib \
./lib/json.hpp \
-I src \
./src/Server/Server.h ./src/Server/Server.cpp \
./src/Server/Request.h ./src/Server/Request.cpp \
./src/Server/Response.h ./src/Server/Response.cpp \
./src/Server/Routes.h ./src/Server/Routes.cpp \
./src/Server/Struct.h \
./src/Server/StatusCode.h \
./src/Tools/Logger.h \
-pthread \
-o main.exe
sudo chmod +x main.exe
sudo ./main.exe
Basically I'm mirroring NodeJS express API Usage: server.setRoute(path, route, callback);
So, what can be done to improve my code in terms of optimization and efficiency?
sudo g++
instead of a simpleg++
? Whychmod
at all? And where is the obligatory-Werror -Wall -Wextra -O2
? \$\endgroup\$