Created
August 31, 2025 08:48
-
-
Save iKunalChhabra/97952e9cd35ac3e83a86b323d74fd37e to your computer and use it in GitHub Desktop.
C++ server from scratch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <iostream> | |
| #include <cstdlib> | |
| #include <string> | |
| #include <cstring> | |
| #include <unistd.h> | |
| #include <sys/types.h> | |
| #include <sys/socket.h> | |
| #include <arpa/inet.h> | |
| #include <netdb.h> | |
| #include <stdexcept> | |
| #include <unordered_map> | |
| #include <algorithm> | |
| #include <filesystem> | |
| #include <thread> | |
| #include <fstream> | |
| #include <ranges> | |
| #include <unordered_set> | |
| #include <zlib.h> | |
| void lower(std::string& v) | |
| { | |
| std::ranges::transform(v, v.begin(), ::tolower); | |
| } | |
| void removeWhitespace(std::string& v) | |
| { | |
| std::erase_if(v, ::isspace); | |
| } | |
| struct RequestParts | |
| { | |
| std::string method; | |
| std::string path; | |
| std::string version; | |
| std::unordered_map<std::string, std::string> headers; | |
| std::string body; | |
| }; | |
| class FileHandler | |
| { | |
| public: | |
| static bool exists(const std::string& path) | |
| { | |
| const std::filesystem::path p(path); | |
| return std::filesystem::exists(p); | |
| } | |
| static std::string readFile(const std::string& path) | |
| { | |
| std::ifstream file(path); | |
| std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); | |
| file.close(); | |
| return content; | |
| } | |
| static void writeFile(const std::string& path, const std::string& content) | |
| { | |
| std::ofstream file(path); | |
| file << content; | |
| file.close(); | |
| } | |
| }; | |
| class Response | |
| { | |
| static std::string compressString(const std::string& str) { | |
| const int level = Z_BEST_COMPRESSION; | |
| z_stream zs{}; | |
| zs.zalloc = Z_NULL; | |
| zs.zfree = Z_NULL; | |
| zs.opaque = Z_NULL; | |
| // Use deflateInit2 with 15+16 to produce a gzip-compatible stream | |
| if (deflateInit2(&zs, level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) | |
| throw std::runtime_error("deflateInit2 failed"); | |
| zs.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(str.data())); | |
| zs.avail_in = static_cast<uInt>(str.size()); | |
| std::string outString; | |
| char outBuffer[32768]; | |
| int ret; | |
| // Compress until the end of stream | |
| do { | |
| zs.next_out = reinterpret_cast<Bytef*>(outBuffer); | |
| zs.avail_out = sizeof(outBuffer); | |
| ret = deflate(&zs, zs.avail_in ? Z_NO_FLUSH : Z_FINISH); | |
| if (ret == Z_STREAM_ERROR) | |
| { | |
| deflateEnd(&zs); | |
| throw std::runtime_error("deflate stream error"); | |
| } | |
| // Append the bytes that were just produced | |
| size_t have = sizeof(outBuffer) - zs.avail_out; | |
| if (have) | |
| outString.append(outBuffer, have); | |
| } while (ret != Z_STREAM_END); | |
| if (deflateEnd(&zs) != Z_OK) | |
| throw std::runtime_error("deflateEnd failed"); | |
| return outString; | |
| } | |
| public: | |
| std::string HTTP_VERSION = "HTTP/1.1"; | |
| std::string HTTP_SUCCESS = "200 OK"; | |
| std::string HTTP_CREATED = "201 Created"; | |
| std::string HTTP_NOT_FOUND = "404 Not Found"; | |
| std::unordered_map<std::string, std::string> headers; | |
| std::unordered_set<std::string> encodingSupported = {"gzip"}; | |
| std::string http_code; | |
| std::string content; | |
| void selectEncoding(const std::string& acceptEncoding) | |
| { | |
| // parse requested encodings | |
| std::unordered_set<std::string> requestedEncodings; | |
| for (constexpr char delimiter = ','; auto&& part : acceptEncoding | std::views::split(delimiter)) { | |
| if (std::string token(part.begin(), part.end()); !token.empty()) { | |
| requestedEncodings.insert(token); | |
| } | |
| } | |
| // select encoding | |
| for (auto& encoding : requestedEncodings) | |
| { | |
| if (encodingSupported.contains(encoding)) | |
| { | |
| headers["Content-Encoding"] = encoding; | |
| } | |
| } | |
| } | |
| void handleConnectionClose(const RequestParts& request) | |
| { | |
| if (request.headers.contains("connection") && request.headers.at("connection") == "close") | |
| { | |
| headers["Connection"] = "close"; | |
| } | |
| else | |
| { | |
| headers["Connection"] = "keep-alive"; | |
| } | |
| } | |
| explicit Response(const RequestParts& request) | |
| { | |
| if (request.headers.contains("accept-encoding")) | |
| { | |
| selectEncoding(request.headers.at("accept-encoding")); | |
| } | |
| handleConnectionClose(request); | |
| } | |
| Response& addContent(const std::string& v) | |
| { | |
| if (headers.contains("Content-Encoding") && headers.at("Content-Encoding") == "gzip") | |
| { | |
| this->content = compressString(v); | |
| return *this; | |
| } | |
| this->content = v; | |
| return *this; | |
| } | |
| Response& setStatusCode(const std::string& v) | |
| { | |
| this->http_code = v; | |
| return *this; | |
| } | |
| Response& addHeader(const std::string& k, const std::string& v) | |
| { | |
| this->headers[k] = v; | |
| return *this; | |
| } | |
| std::string buildResponse() | |
| { | |
| std::string response = HTTP_VERSION + " " + http_code + "\r\n"; | |
| for (auto& [k, v] : headers) | |
| { | |
| response += (k + ": " + v + "\r\n"); | |
| } | |
| response += ("Content-Length: " + std::to_string(content.length()) + "\r\n"); | |
| response += "\r\n"; | |
| response += content; | |
| return response; | |
| } | |
| }; | |
| class Server | |
| { | |
| int server_fd = -1; | |
| int port = 4221; | |
| sockaddr_in server_addr{}; | |
| sockaddr_in client_addr{}; | |
| std::string directory; | |
| public: | |
| void createServerSocket() | |
| { | |
| server_fd = socket(AF_INET, SOCK_STREAM, 0); | |
| if (server_fd < 0) | |
| { | |
| throw std::runtime_error("Failed to create server socket\n"); | |
| } | |
| } | |
| void setSocketOptions() const | |
| { | |
| constexpr int reuse = 1; | |
| if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) | |
| { | |
| throw std::runtime_error("setsockopt failed\n"); | |
| } | |
| } | |
| void BindServerSocket() | |
| { | |
| server_addr.sin_family = AF_INET; // IPV4 | |
| server_addr.sin_addr.s_addr = INADDR_ANY; // Any Address | |
| server_addr.sin_port = htons(port); // PORT | |
| if (bind(server_fd, reinterpret_cast<sockaddr*>(&server_addr), sizeof(server_addr)) < 0) | |
| { | |
| throw std::runtime_error("Failed to bind to port 4221\n"); | |
| } | |
| } | |
| void listenOnSocket() const | |
| { | |
| if (constexpr int connection_backlog = 5; listen(server_fd, connection_backlog) != 0) | |
| { | |
| throw std::runtime_error("listen failed\n"); | |
| } | |
| } | |
| int connectToClient() | |
| { | |
| socklen_t client_addr_len = sizeof(client_addr); | |
| const int client_fd = accept(server_fd, reinterpret_cast<struct sockaddr*>(&client_addr), &client_addr_len); | |
| if (client_fd < 0) | |
| { | |
| throw std::runtime_error("Failed to accept connection\n"); | |
| } | |
| std::cout << "Client connected\n"; | |
| return client_fd; | |
| } | |
| static std::string receiveFromClient(const int client_fd) | |
| { | |
| char requestBuffer[4096]{}; | |
| const ssize_t sz = recv(client_fd, requestBuffer, sizeof(requestBuffer) - 1, 0); | |
| if (sz < 0) | |
| { | |
| throw std::runtime_error("Failed to read from connection\n"); | |
| } | |
| if (sz == 0) | |
| { | |
| // Client closed connection gracefully | |
| throw std::runtime_error("Client disconnected\n"); | |
| } | |
| std::cout << "Request received from client\n"; | |
| requestBuffer[sz] = '\0'; // make it a C-string | |
| std::string request(requestBuffer); | |
| return request; | |
| } | |
| static RequestParts parseRequest(const std::string& request) | |
| { | |
| std::cout << "Raw Request***\n" << request << "\n***\n"; | |
| RequestParts parts{}; | |
| // process first line | |
| const size_t lineEnd = request.find("\r\n"); | |
| std::string firstLine = request.substr(0, lineEnd); | |
| const size_t space1 = firstLine.find(' '); | |
| const size_t space2 = firstLine.find(' ', space1 + 1); | |
| parts.method = firstLine.substr(0, space1); | |
| parts.path = firstLine.substr(space1 + 1, space2 - (space1 + 1)); | |
| parts.version = firstLine.substr(space2 + 1); | |
| // process each header | |
| const size_t headerEnd = request.find("\r\n\r\n"); | |
| std::string headers = request.substr(lineEnd + 2, headerEnd - (lineEnd + 2)); | |
| headers += "\r\n"; | |
| std::cout << "Headers***\n" << headers << "\n***\n"; | |
| size_t headerEnd2 = headers.find("\r\n"); | |
| while (headerEnd2 != std::string::npos) | |
| { | |
| const size_t colon = headers.find(':'); | |
| std::string headerName = headers.substr(0, colon); | |
| std::string headerValue = headers.substr(colon + 1, headerEnd2 - (colon + 1)); | |
| std::cout << "Parsed Header: " << headerName << " " << headerValue << "\n"; | |
| lower(headerName); | |
| removeWhitespace(headerName); | |
| removeWhitespace(headerValue); | |
| parts.headers[headerName] = headerValue; | |
| std::cout << "Saved Header: " << headerName << " " << headerValue << "\n"; | |
| headers = headers.substr(headerEnd2 + 2); | |
| headerEnd2 = headers.find("\r\n"); | |
| } | |
| parts.body = request.substr(headerEnd + 4); | |
| return parts; | |
| } | |
| void sendResponse(const int client_fd, const RequestParts& requestParts) const | |
| { | |
| std::string response; | |
| std::cout << "path is " << requestParts.path << "\n"; | |
| if (requestParts.path.length() == 1) | |
| { | |
| Response r(requestParts); | |
| response = r.setStatusCode(r.HTTP_SUCCESS) | |
| .buildResponse(); | |
| } | |
| else if (requestParts.path.starts_with("/echo/")) | |
| { | |
| const std::string echo = requestParts.path.substr(6); | |
| Response r(requestParts); | |
| response = r.setStatusCode(r.HTTP_SUCCESS) | |
| .addContent(echo) | |
| .addHeader("Content-Type", "text/plain") | |
| .buildResponse(); | |
| } | |
| else if (requestParts.method=="GET" && requestParts.path.starts_with("/files/")) | |
| { | |
| const std::string filename = requestParts.path.substr(6); | |
| std::string filePath = directory + "/" + filename; | |
| std::cout << "filePath is " << filePath << "\n"; | |
| if (!FileHandler::exists(filePath)) | |
| { | |
| Response r(requestParts); | |
| response = r.setStatusCode(r.HTTP_NOT_FOUND) | |
| .buildResponse(); | |
| } | |
| else | |
| { | |
| Response r(requestParts); | |
| response = r.setStatusCode(r.HTTP_SUCCESS) | |
| .addContent(FileHandler::readFile(filePath)) | |
| .addHeader("Content-Type", "application/octet-stream") | |
| .buildResponse(); | |
| } | |
| } | |
| else if (requestParts.method=="POST" && requestParts.path.starts_with("/files/")) | |
| { | |
| const std::string filename = requestParts.path.substr(6); | |
| std::string filePath = directory + "/" + filename; | |
| std::cout << "filePath is " << filePath << "\n"; | |
| FileHandler::writeFile(filePath, requestParts.body); | |
| Response r(requestParts); | |
| response = r.setStatusCode(r.HTTP_CREATED) | |
| .buildResponse(); | |
| } | |
| else if (requestParts.path.starts_with("/user-agent")) | |
| { | |
| std::string userAgent; | |
| // std::println("Available headers: {}", requestParts.headers); | |
| if (requestParts.headers.contains("user-agent")) | |
| { | |
| userAgent = requestParts.headers.at("user-agent"); | |
| } | |
| else | |
| { | |
| userAgent = ""; | |
| } | |
| Response r(requestParts); | |
| response = r.setStatusCode(r.HTTP_SUCCESS) | |
| .addContent(userAgent) | |
| .addHeader("Content-Type", "text/plain") | |
| .buildResponse(); | |
| } | |
| else | |
| { | |
| Response r(requestParts); | |
| response = r.setStatusCode(r.HTTP_NOT_FOUND) | |
| .buildResponse(); | |
| } | |
| std::cout << "Sending response to client\n"; | |
| send(client_fd, response.c_str(), response.size(), 0); | |
| } | |
| void handleClient(const int client_fd) const | |
| { | |
| std::cout << "Handling client with client fd = " << client_fd << "\n"; | |
| while (true) | |
| { | |
| try { | |
| const std::string request = receiveFromClient(client_fd); | |
| const auto requestParts = parseRequest(request); | |
| sendResponse(client_fd, requestParts); | |
| if (requestParts.headers.contains("connection") && requestParts.headers.at("connection") == "close" ) | |
| { | |
| close(client_fd); | |
| break; | |
| } | |
| } catch (const std::runtime_error& e) { | |
| std::cout << "Client handling error: " << e.what() << "\n"; | |
| // close(client_fd); | |
| } | |
| } | |
| } | |
| [[noreturn]] void startServer() | |
| { | |
| std::cout << "Waiting for a client to connect...\n"; | |
| while (true) | |
| { | |
| const int client_fd = connectToClient(); | |
| std::thread([this, client_fd] | |
| { | |
| handleClient(client_fd); | |
| }).detach(); | |
| } | |
| } | |
| explicit Server(const std::string& d) | |
| { | |
| directory = d; | |
| createServerSocket(); | |
| setSocketOptions(); | |
| BindServerSocket(); | |
| listenOnSocket(); | |
| } | |
| ~Server() | |
| { | |
| close(server_fd); | |
| std::cout << "Client Disconnected\n"; | |
| } | |
| }; | |
| // ./your_program.sh --directory /tmp/ | |
| int main(const int argc, char** argv) | |
| { | |
| // std::cout << std::unitbuf; | |
| // std::cerr << std::unitbuf; | |
| std::string directory = "."; | |
| if (argc==3 && std::string(argv[1]) == "--directory") | |
| { | |
| directory = argv[2]; | |
| if (directory.back() == '/') | |
| { | |
| directory.pop_back(); | |
| } | |
| } | |
| std::cout << "Directory is " << directory << "\n"; | |
| Server server(directory); | |
| server.startServer(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment