✨ This is where dreams go to die ✨
- Project
- Allowed Functions
- Understanding poll() and I/O multiplexing
- Select() and Macros
- Handle HTTP POST Request
Lightweight HTTP server implementation written in C++98, capable of serving static content, handling file uploads, processing CGI scripts, and more. This project follows the HTTP/1.1 standard and implements a non-blocking I/O architecture using poll() (or equivalent) for handling multiple client connections simultaneously.
- HTTP/1.1 compliant
- Configurable via NGINX-like configuration file
- Multiple virtual servers with customizable routes
- Static file serving with directory listing
- Support for GET, POST, and DELETE methods
- CGI execution (PHP, Python)
- File uploads
- Custom error pages
- HTTP redirects
- Non-blocking architecture
- C++98 compatible compiler (g++, clang++)
- POSIX-compliant operating system (Linux, macOS)
make./webserv [config_file]If no configuration file is specified, conf/default.conf will be used.
webserv/
├── Makefile
├── conf/
| └── default.conf
│
└── srcs/
├── config/
├── http/
├── tcp/
└── main.cpp
Responsibility: Implement the core server functionality and low-level networking
- Main server class, poll() implementation, connection lifecycle
- Client connection management
- Socket creation, binding, and listening
- Non-blocking I/O operations
- Main server loop and event handling
- Stress testing and performance tuning
Responsibility: Implement the HTTP protocol handling
- HTTP request parsing and validation
- HTTP response generation
- HTTP methods (GET, POST, DELETE) implementation
- Headers and status codes handling
- File upload mechanism
- HTTP protocol compliance testing
Responsibility: Implement configuration parsing and file system operations
- Configuration file parsing
- CGI script execution
- Path manipulation, directory operations
- Directory listing implementation
- File type detection and MIME types
- Configuration file format development
| Function | Signature | Description |
|---|---|---|
socket |
int socket(int domain, int type, int protocol); |
Creates an endpoint for communication and returns a file descriptor |
bind |
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
Assigns a local address to a socket |
listen |
int listen(int sockfd, int backlog); |
Marks the socket as a passive socket, used to accept incoming connection requests |
accept |
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
Accepts a connection on a socket |
connect |
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
Initiates a connection on a socket |
send |
ssize_t send(int sockfd, const void *buf, size_t len, int flags); |
Sends a message on a socket |
recv |
ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
Receives a message from a socket |
socketpair |
int socketpair(int domain, int type, int protocol, int sv[2]); |
Creates a pair of connected sockets |
setsockopt |
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); |
Sets options on sockets |
getsockname |
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
Retrieves the current address to which the socket is bound |
getprotobyname |
struct protoent *getprotobyname(const char *name); |
Returns protocol entry for the given protocol name |
| Function | Signature | Description |
|---|---|---|
htons |
uint16_t htons(uint16_t hostshort); |
Converts a 16-bit number from host byte order to network byte order |
htonl |
uint32_t htonl(uint32_t hostlong); |
Converts a 32-bit number from host byte order to network byte order |
ntohs |
uint16_t ntohs(uint16_t netshort); |
Converts a 16-bit number from network byte order to host byte order |
ntohl |
uint32_t ntohl(uint32_t netlong); |
Converts a 32-bit number from network byte order to host byte order |
| Function | Signature | Description |
|---|---|---|
getaddrinfo |
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); |
Resolves hostnames and service names into a list of addrinfo structures |
freeaddrinfo |
void freeaddrinfo(struct addrinfo *res); |
Frees memory allocated by getaddrinfo |
gai_strerror |
const char *gai_strerror(int errcode); |
Returns a human-readable string describing an error from getaddrinfo |
| Function | Signature | Description |
|---|---|---|
select |
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
Waits for file descriptors to be ready for I/O operations |
poll |
int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
Monitors multiple file descriptors for I/O readiness without blocking |
| Function | Signature | Description |
|---|---|---|
epoll_create |
int epoll_create(int size); |
Creates an epoll instance |
epoll_ctl |
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
Controls the epoll instance—adding, modifying, or deleting monitored fds |
epoll_wait |
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |
Waits for events on the epoll instance |
| Function | Signature | Description |
|---|---|---|
kqueue |
int kqueue(void); |
Creates a new kernel event queue |
kevent |
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout); |
Registers events with the queue or waits for triggered events |
| Function | Signature | Description |
|---|---|---|
opendir |
DIR *opendir(const char *name); |
Opens a directory stream corresponding to the directory name |
readdir |
struct dirent *readdir(DIR *dirp); |
Reads the next directory entry in the opened directory stream |
closedir |
int closedir(DIR *dirp); |
Closes the opened directory stream |
| Function | Signature | Description |
|---|---|---|
access |
int access(const char *pathname, int mode); |
Checks file accessibility (existence, read/write permissions) |
stat |
int stat(const char *pathname, struct stat *statbuf); |
Retrieves information about the file (size, type, permissions) |
open |
int open(const char *pathname, int flags, mode_t mode); |
Opens or creates a file with the specified flags and mode |
chdir |
int chdir(const char *path); |
Changes the current working directory |
| Function | Signature | Description |
|---|---|---|
errno |
int errno; (global variable) |
Stores the last error code set by a system call |
strerror |
char *strerror(int errnum); |
Returns a human-readable string describing the error code |
I/O multiplexing is a technique that lets a program monitor multiple file descriptors (like sockets, pipes, or files) to see which ones are ready for I/O (input or output), without blocking on just one.
Instead of calling read() on each socket and waiting (which would block execution), we can use I/O multiplexing to wait for any of them to be ready, and act only when one is.
There are three common system calls for this:
- select()
- poll()
- epoll() (Linux-specific, more scalable)
struct pollfd {
int fd; // The file descriptor to monitor
short events; // Events we are interested in (input)
short revents; // Events that actually occurred (output)
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//parameter:
// - nfds_t nfds Number of elements in fds
// - struct pollfd *fds Array of struct pollfd (each representing one fd you want to monitor)
// - int timeout Time to wait (in milliseconds)
// - > 0 → wait that many milliseconds
// - 0 → return immediately (non-blocking)
// - < 0 → wait foreverpoll() is a system call used for I/O multiplexing. It waits for one or more file descriptors to become ready to perform I/O (e.g., reading or writing).
It improves on select() by avoiding hard limits on the number of file descriptors and having a cleaner interface.
| Event | Meaning |
|---|---|
| POLLIN | Ready to read |
| POLLOUT | Ready to write |
| POLLERR | Error occurred |
| POLLHUP | Disconnected (hang up) |
| POLLNVAL | Invalid file descriptor |
#include <stdio.h>
#include <poll.h>
#include <unistd.h>
int main() {
struct pollfd pfd;
pfd.fd = 0; // stdin (fd 0)
pfd.events = POLLIN; // wait for data to read
printf("Waiting for input on stdin...\n");
int ret = poll(&pfd, 1, 5000); // wait up to 5 seconds
if (ret == -1) {
perror("poll");
} else if (ret == 0) {
printf("Timeout! No input.\n");
} else {
if (pfd.revents & POLLIN) {
printf("Data is ready to read on stdin!\n");
}
}
return 0;
}select() is a system call used to monitor multiple file descriptors (like sockets or files) at the same time, waiting until one or more of them become ready for some class of I/O (like read, write, or exception).
watch this: How one thread listens to many sockets with select in C
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);It blocks (or not, depending on timeout) until one or more of the file descriptors are ready.
fd_set is a data structure used by select() to keep track of which file descriptors you want to monitor. But we can't manipulate fd_set directly — that's where these macros come in:
Use this before anything else. It clears the set — makes sure it's empty.
FD_ZERO(&readfds);Adds a file descriptor to the set. You tell select(): "Hey, I want to monitor this fd!"
FD_SET(sockfd, &readfds); // I want to know when sockfd is ready to readAfter select() returns, you check if a file descriptor is ready using this.
if (FD_ISSET(sockfd, &readfds)) {
// sockfd is ready to read
}Removes a file descriptor from the set (you rarely use this with select itself, but can use it when managing sets manually).
FD_CLR(sockfd, &readfds); // Remove fd from the setA POST request is typically used when the client needs to send data to the server.
Common scenarios include:
- Submitting a login form (username and password).
- Uploading a file (e.g., an image, PDF, or document).
- Sending form data that may include multiple fields at once.
- Creating or updating resources in APIs.
Unlike GET, which appends data to the URL, POST includes the data in the body of the request.
A POST request consists of:
- Request line: e.g.,
POST /upload HTTP/1.0 - Headers: contain metadata such as
Content-TypeandContent-Length. - Body: contains the actual data being sent.
Example (with form data and file upload)
POST /upload HTTP/1.0
Host: localhost
Content-Type: multipart/form-data; boundary=----MyBoundary
Content-Length: 1234
------MyBoundary
Content-Disposition: form-data; name="username"
Luca
------MyBoundary
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
This is the content of the file.
------MyBoundary--About the Boundary
- When using
multipart/form-data, a boundary string is defined in the header. - This boundary separates each field in the body. In the example
boundary=----MyBoundary - Each part contains:
- A disposition header (e.g., field name, filename).
- Optionally a Content-Type.
- A blank line.
- The field content or file content.
application/x-www-form-urlencoded
- Default for forms without files.
- Data is encoded as key-value pairs separated by &.
- Example:
POST /login HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 27 username=Luca&password=1234
multipart/form-data
- Required for forms that include file uploads.
- Each field or file is separated by the boundary string.
- Example:
POST /upload HTTP/1.0 Content-Type: multipart/form-data; boundary=----MyBoundary Content-Length: 1234 ------MyBoundary Content-Disposition: form-data; name="username" Luca ------MyBoundary Content-Disposition: form-data; name="file"; filename="test.txt" Content-Type: text/plain File content goes here... ------MyBoundary--
application/json(modern APIs)
- Common in APIs but not originally typical for HTTP/1.0.
- Data is sent in JSON format.
- Example:
POST /api/data HTTP/1.0 Content-Type: application/json Content-Length: 35 {"username":"Luca","password":"1234"}
text/plain
- Rare but possible.
- Body contains plain text without formatting.
- Example:
POST /echo HTTP/1.0 Content-Type: text/plain Content-Length: 11 Hello World
When a client sends an HTTP POST request, the message body can be delivered in two ways:
-
Using Content-Length The client specifies the total size of the body in advance:
POST /upload HTTP/1.1 Content-Length: 20 name=Luca&age=30
The server reads exactly 20 bytes after the headers — and that’s it.
-
Using Transfer-Encoding: chunked In this case, the total length of the body is not specified. The client sends the body in pieces (chunks), and each chunk is preceded by a hexadecimal number indicating its size.
Example:
POST /upload HTTP/1.1 Transfer-Encoding: chunked 4\r\n Wiki\r\n 5\r\n pedia\r\n E\r\n in\r\nchunks.\r\n 0\r\n \r\n
Let’s break it down:
| Part | Meaning |
|---|---|
4\r\nWiki\r\n |
Chunk of 4 bytes → "Wiki" |
5\r\npedia\r\n |
Chunk of 5 bytes → "pedia" |
E\r\n in\r\nchunks.\r\n |
Chunk of 14 bytes → " in\r\nchunks." |
0\r\n\r\n |
Final chunk of 0 bytes, indicates the end |
When the server decodes this message, it must:
- Read each hexadecimal number (the chunk length).
- Extract the corresponding chunk.
- Stop when it encounters 0\r\n\r\n.
- Concatenate all chunks into a single body:
Wikipedia in
chunks.
This process is known as “unchunking the body.”
- Beej's Guide to Sockets
- Understanding HTTP Server Basics
- C++ Server Example by Ncona
- Web Programming in C++ (TutorialsPoint)