How2Lab Logo
tech guide & how tos..


Building Your Own Web Server: A Practical Application of Socket Programming in C


Congratulations on making it to the final project of our socket programming series! You have come a long way, from understanding basic TCP echoes and UDP datagrams to mastering concurrent server handling. Now, It is time to bring all these concepts together to build a recognizable, real-world application: a simple HTTP web server.

Building a web server is perhaps the most iconic "hello world" for network programming. It demonstrates the fundamental request-response cycle of the web and allows you to interact with your code using a standard web browser.


1. Project Goal: A Basic HTTP/1.0 Web Server

Our objective is to create a console-based HTTP/1.0 web server that can:

  • Listen for incoming HTTP requests on a specified port (e.g., 8080).

  • Handle multiple concurrent requests using the `select()` model from Project 3.

  • Parse basic HTTP `GET` requests.

  • Serve static files (like `.html`, `.css`, `.js`, `.jpg`, `.png`) from a designated "web root" directory.

  • Automatically serve `index.html` when the root path (`/`) is requested.

  • Return appropriate HTTP status codes (200 OK, 404 Not Found, 400 Bad Request, 500 Internal Server Error).

  • Close the connection after each response (HTTP/1.0 behavior).


2. HTTP Protocol Overview (Simplified but Practical)

HTTP (Hypertext Transfer Protocol) is the foundation of data communication for the World Wide Web. It operates on a request-response model: a client (e.g., your web browser) sends a request to a server, and the server sends back a response.

Common HTTP Methods (Focus on GET)

  • GET: Requests data from a specified resource. This is what your browser typically uses to load web pages, images, etc. Our server will primarily handle `GET` requests.

  • `POST`: Submits data to be processed to a specified resource (e.g., form submissions).

  • `HEAD`, `PUT`, `DELETE`, etc.: Other methods we won't implement for this basic server.

HTTP Headers

Headers provide additional information about the request or response. Key headers for our server:

  • `Host`: (Request) Specifies the domain name of the server.

  • `Content-Type`: (Response) Indicates the media type of the resource (e.g., `text/html`, `image/jpeg`, `application/json`). This is crucial for the browser to correctly render the content.

  • `Content-Length`: (Response) The size of the message body in bytes. Essential for the client to know when the response data ends.

Status Codes

These 3-digit codes indicate the outcome of the request.

  • `200 OK`: The request has succeeded.

  • `404 Not Found`: The requested resource could not be found on the server.

  • `400 Bad Request`: The server cannot understand the request due to malformed syntax.

  • `500 Internal Server Error`: A generic error message, given when an unexpected condition was encountered on the server.

Structure of a Simple HTTP/1.0 Request and Response

HTTP messages are plain text, making them relatively easy to parse with C strings.

Example HTTP/1.0 Request (from client to server):


GET /index.html HTTP/1.0
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36

Note there will be a blank line (`\r\n\r\n`) after the headers, signaling the end of the request header.

Example HTTP/1.0 Response (from server to client, for `index.html`):


HTTP/1.0 200 OK
Content‐Type: text/html
Content-Length: 1234
Connection: close

<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body><h1>Hello World!</h1></body>
</html>

After sending this entire response (headers + blank line + body), our HTTP/1.0 server will close the client connection.


3. Server Architecture

Our web server will utilize the `select()`-based I/O multiplexing model introduced in Project 3. This is crucial because a web server must be able to handle multiple browser requests concurrently. When your browser requests an HTML page, it often makes separate requests for images, CSS files, and JavaScript files on that page. A single-threaded blocking server would serve one file, then wait for the connection to close before serving the next, leading to a very slow and unresponsive user experience.

The `select()` loop will continuously monitor the listening socket for new connections and existing client sockets for incoming HTTP requests. When a request comes in, the server will parse it, find the requested file, construct an HTTP response, and send the file content back to the client.


4. Implementation Steps

Socket Setup

This will be identical to the `multi_chat_server.c` from Project 3:

  • Create a master TCP socket (`socket(AF_INET, SOCK_STREAM, 0)`).

  • Use `setsockopt()` with `SO_REUSEADDR` for quick restarts.

  • `bind()` the socket to a port (e.g., 8080).

  • `listen()` for incoming connections.

  • Enter the `select()` loop to manage concurrent client connections.

Request Parsing

When `select()` indicates a client socket is ready for reading:

  • Use `recv()` to read the incoming HTTP request into a buffer. HTTP requests are typically small enough to fit into a single `recv` call for `GET` requests.

  • Parse the request line (the first line): `GET /requested_path HTTP/1.0`

    • Use `sscanf()` or string manipulation functions (`strstr`, `strtok`) to extract the method (verify It is `GET`), the requested path (`/index.html` or `/images/logo.png`), and the HTTP version.

  • Extract the requested file path: This is crucial. If the path is just `/`, you should map it to `/index.html`.

File Handling

This is where your server actually finds and sends the content.

  • Security Considerations (CRUCIAL!): This is the most important part for a real server. Never trust user input directly.

    • Define a `WEB_ROOT` constant (e.g., `"./web_root"`). All served files *must* reside within this directory.

    • Prevent directory traversal attacks: Reject any requested path containing `../` or attempting to access absolute paths (`/etc/passwd`). Sanitize the path to ensure it always refers to a file inside `WEB_ROOT`.

  • Construct the full file path: `sprintf(full_path, "%s%s", WEB_ROOT, requested_path);`

  • Attempt to open the file using `fopen()` in binary read mode (`"rb"`).

  • If `fopen()` returns `NULL`: The file was not found or access denied. Send a `404 Not Found` HTTP response.

  • If the file opens:

    • Get file size: `fseek(file, 0, SEEK_END); long file_size = ftell(file); fseek(file, 0, SEEK_SET);`

    • Read the file in chunks using `fread()` into a buffer.

    • Send these chunks using `send()` to the client socket.

Response Construction

Before sending the file content, you must send the HTTP response header.

  • Status Line: `HTTP/1.0 200 OK` or `HTTP/1.0 404 Not Found`, etc.

  • `Content‐Type` Header: This tells the browser how to interpret the file. You will need a simple mapping function (see Key Considerations below).

  • `Content-Length` Header: The size of the file in bytes.

  • `Connection: close` Header: For HTTP/1.0, this tells the client the server will close the connection after this response.

  • Send the entire header, followed by a blank line (`\r\n\r\n`), then the file content.

Error Handling

  • All `socket`, `bind`, `listen`, `accept`, `recv`, `send` calls must be checked for errors using `perror()` / `WSAGetLastError()`.

  • File operations (`fopen`, `fread`, `fseek`) should also be checked.

  • Return appropriate HTTP status codes to the client for different error scenarios (e.g., 404 for file not found, 400 for bad request, 500 for server-side file access issues).


5. Key Considerations

Basic MIME Type Mapping

Browsers use the `Content‐Type` header to know how to display the received data. You will need a simple function that takes a file extension and returns the corresponding MIME type string.


const char* get_mime_type(const char* file_path) {
    const char* ext = strrchr(file_path, '.'); // Find last '.'
    if (!ext) return "application/octet-stream"; // Default for unknown
    ext++; // Move past the dot

    if (strcmp(ext, "html") == 0 || strcmp(ext, "htm") == 0) return "text/html";
    if (strcmp(ext, "css") == 0) return "text/css";
    if (strcmp(ext, "js") == 0) return "application/javascript";
    if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) return "image/jpeg";
    if (strcmp(ext, "png") == 0) return "image/png";
    if (strcmp(ext, "gif") == 0) return "image/gif";
    if (strcmp(ext, "txt") == 0) return "text/plain";
    // Add more as needed
	
    return "application/octet-stream";
}

Serving `index.html` by Default

If the requested path is just `/` (the root of the web server), you should implicitly serve `index.html`.


if (strcmp(requested_path, "/") == 0) {
    strcpy(requested_path, "/index.html");
}

6. Full, Commented Code for the HTTP Server

This server code is a culmination of everything You have learned. Pay close attention to the comments explaining each part.

http_server.c


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h> // For isalnum

// --- Platform-specific includes and definitions ---
#ifdef _WIN32
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #pragma comment(lib, "ws2_32.lib") // Link with ws2_32.lib
    #define CLOSE_SOCKET closesocket
    #define GET_LAST_ERROR WSAGetLastError
    #define SNPRINTF _snprintf_s
    #ifndef _SSIZE_T_DEFINED
    typedef SSIZE_T ssize_t;
    #define _SSIZE_T_DEFINED
    #endif
#else
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h> // For close
    #include <errno.h>  // For errno
    #define CLOSE_SOCKET close
    #define GET_LAST_ERROR() errno
    #define SNPRINTF snprintf
#endif
// ----------------------------------------------------

#define PORT 8080 // Standard HTTP port (or common alternative for testing)
#define MAX_CLIENTS 5 // Max concurrent clients
#define BUFFER_SIZE 4096 // Increased buffer size for HTTP requests/responses
#define WEB_ROOT "./web_root" // Directory to serve files from

// Function to get MIME type based on file extension
const char* get_mime_type(const char* file_path) {
    const char* ext = strrchr(file_path, '.'); // Find last '.'
    if (!ext) return "application/octet-stream"; // Default for unknown
    ext++; // Move past the dot

    if (strcmp(ext, "html") == 0 || strcmp(ext, "htm") == 0) return "text/html";
    if (strcmp(ext, "css") == 0) return "text/css";
    if (strcmp(ext, "js") == 0) return "application/javascript";
    if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) return "image/jpeg";
    if (strcmp(ext, "png") == 0) return "image/png";
    if (strcmp(ext, "gif") == 0) return "image/gif";
    if (strcmp(ext, "txt") == 0) return "text/plain";
    if (strcmp(ext, "ico") == 0) return "image/x-icon"; // For favicons
    // Add more as needed
    return "application/octet-stream";
}

// Function to send HTTP error response
void send_error_response(int client_socket, int status_code, const char* status_message) {
    char response_header[BUFFER_SIZE];
    char html_body[BUFFER_SIZE];
    SNPRINTF(html_body, sizeof(html_body),
             "<!DOCTYPE html><html><head><title>%d %s</title></head><body><h1>%d %s</h1><p>The server encountered an error.</p></body></html>",
             status_code, status_message, status_code, status_message);

    SNPRINTF(response_header, sizeof(response_header),
             "HTTP/1.0 %d %s\r\n"
             "Content‐Type: text/html\r\n"
             "Content-Length: %zu\r\n"
             "Connection: close\r\n"
             "\r\n",
             status_code, status_message, strlen(html_body));

    send(client_socket, response_header, strlen(response_header), 0);
    send(client_socket, html_body, strlen(html_body), 0);
}

// Function to sanitize path to prevent directory traversal
// Returns 1 if path is safe, 0 if unsafe
int sanitize_path(char* path) {
    if (!path || path[0] == '\0') return 0; // Empty or NULL path
    
    // Check for absolute path (e.g., /etc/passwd)
    // On Windows, also check for drive letters (e.g., C:\)
    #ifdef _WIN32
    if (path[0] == '/' || path[0] == '\\' || (isalpha(path[0]) && path[1] == ':')) {
        return 0; // Absolute path attempt
    }
    #else
    if (path[0] == '/') {
        return 0; // Absolute path attempt on Unix-like
    }
    #endif

    // Check for directory traversal (../)
    char* token;
    char temp_path[BUFFER_SIZE];
    strncpy(temp_path, path, sizeof(temp_path) - 1);
    temp_path[sizeof(temp_path) - 1] = '\0'; // Ensure null termination

    token = strtok(temp_path, "/\\"); // Use both slashes for cross-platform
    while (token != NULL) {
        if (strcmp(token, ".".) == 0) {
            return 0; // Contains '..' indicating traversal attempt
        }
        token = strtok(NULL, "/\\");
    }
    return 1; // Path is considered safe
}


int main() {
    // --- Windows-specific Winsock initialization ---
    #ifdef _WIN32
        WSADATA wsaData;
        if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
            fprintf(stderr, "WSAStartup failed with error: %d\n", GET_LAST_ERROR());
            return 1;
        }
    #endif
    // ----------------------------------------------------

    int master_socket, new_socket, activity, i, valread;
    int client_sockets[MAX_CLIENTS]; // Array to hold client socket descriptors
    int max_sd; // Maximum file descriptor number

    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char request_buffer[BUFFER_SIZE];
    
    // Set of socket descriptors for select()
    fd_set readfds;

    // Initialize all client_sockets to 0 (empty)
    for (i = 0; i < MAX_CLIENTS; i++) {
        client_sockets[i] = 0;
    }

    // 1. Create master socket (listening socket)
    if ((master_socket = socket(AF_INET, SOCK_STREAM, 0)) ==
        #ifdef _WIN32
        INVALID_SOCKET
        #else
        0
        #endif
        ) {
        fprintf(stderr, "master socket failed with error: %d\n", GET_LAST_ERROR());
        #ifdef _WIN32
        WSACleanup();
        #endif
        exit(EXIT_FAILURE);
    }

    // Optional: Set master socket to allow multiple connections (SO_REUSEADDR)
    int opt = 1;
    if (setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)) < 0) {
        perror("setsockopt failed");
        CLOSE_SOCKET(master_socket);
        #ifdef _WIN32
        WSACleanup();
        #endif
        exit(EXIT_FAILURE);
    }

    // Prepare the sockaddr_in structure for binding
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // Listen on all available interfaces
    address.sin_port = htons(PORT);       // Convert port to network byte order

    // 2. Bind the master socket to the specified IP and port
    if (bind(master_socket, (struct sockaddr *)&address, addrlen) ==
        #ifdef _WIN32
        SOCKET_ERROR
        #else
        -1
        #endif
        ) {
        fprintf(stderr, "bind failed with error: %d\n", GET_LAST_ERROR());
        CLOSE_SOCKET(master_socket);
        #ifdef _WIN32
        WSACleanup();
        #endif
        exit(EXIT_FAILURE);
    }

    // 3. Listen for incoming connections
    if (listen(master_socket, 5) < 0) { // Backlog of 5 connections
        fprintf(stderr, "listen failed with error: %d\n", GET_LAST_ERROR());
        CLOSE_SOCKET(master_socket);
        #ifdef _WIN32
        WSACleanup();
        #endif
        exit(EXIT_FAILURE);
    }

    printf("HTTP Server listening on port %d, serving from '%s'...\n", PORT, WEB_ROOT);
    printf("Waiting for connections...\n");

    // Main server loop using select() for concurrency
    while (1) {
        // Clear the socket set
        FD_ZERO(&readfds);

        // Add master socket to set
        FD_SET(master_socket, &readfds);
        max_sd = master_socket;

        // Add child sockets to set
        for (i = 0; i < MAX_CLIENTS; i++) {
            int sd = client_sockets[i];

            if (sd > 0) { // If valid socket descriptor exists, add to read list
                FD_SET(sd, &readfds);
            }
            if (sd > max_sd) { // Find the highest file descriptor number
                max_sd = sd;
            }
        }

        // 4. Wait for an activity on one of the sockets (indefinitely)
        activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);

        if (activity ==
            #ifdef _WIN32
            SOCKET_ERROR
            #else
            -1
            #endif
            ) {
            fprintf(stderr, "select error: %d\n", GET_LAST_ERROR());
            break;
        }

        // If something happened on the master socket, then It is an incoming connection
        if (FD_ISSET(master_socket, &readfds)) {
            if ((new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen)) ==
                #ifdef _WIN32
                INVALID_SOCKET
                #else
                -1
                #endif
                ) {
                fprintf(stderr, "accept failed with error: %d\n", GET_LAST_ERROR());
                // Continue to next iteration, don't break main loop for accept error
                continue;
            }

            char client_ip[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &address.sin_addr, client_ip, INET_ADDRSTRLEN);
            printf("New connection, socket fd: %d, IP: %s, Port: %d\n",
                   new_socket, client_ip, ntohs(address.sin_port));

            // Add new socket to array of client sockets
            for (i = 0; i < MAX_CLIENTS; i++) {
                if (client_sockets[i] == 0) { // Find an empty slot
                    client_sockets[i] = new_socket;
                    printf("  -> Assigned to client slot %d\n", i);
                    break;
                }
            }
            if (i == MAX_CLIENTS) { // No space for new client
                printf("Max clients reached. Connection refused for %s:%d\n", client_ip, ntohs(address.sin_port));
                send_error_response(new_socket, 503, "Service Unavailable (Server Full)");
                CLOSE_SOCKET(new_socket);
            }
        }

        // Else, It is some I/O operation on an existing client socket
        for (i = 0; i < MAX_CLIENTS; i++) {
            int sd = client_sockets[i]; // Current client socket descriptor

            if (sd == 0) continue; // Skip if socket slot is empty

            if (FD_ISSET(sd, &readfds)) { // If this client socket has activity
                memset(request_buffer, 0, BUFFER_SIZE); // Clear buffer for incoming request

                valread = recv(sd, request_buffer, BUFFER_SIZE - 1, 0);

                if (valread == 0) { // Client disconnected
                    char client_ip[INET_ADDRSTRLEN];
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    inet_ntop(AF_INET, &address.sin_addr, client_ip, INET_ADDRSTRLEN);
                    printf("Client disconnected, IP %s, Port %d (socket %d)\n", client_ip, ntohs(address.sin_port), sd);

                    CLOSE_SOCKET(sd);
                    client_sockets[i] = 0; // Mark slot as free
                } else if (valread ==
                           #ifdef _WIN32
                           SOCKET_ERROR
                           #else
                           -1
                           #endif
                           ) {
                    fprintf(stderr, "recv failed on socket %d with error: %d\n", sd, GET_LAST_ERROR());
                    CLOSE_SOCKET(sd);
                    client_sockets[i] = 0; // Mark slot as free
                } else { // Request received
                    request_buffer[valread] = '\0'; // Null-terminate
                    printf("Request from socket %d:\n%s\n", sd, request_buffer);

                    char method[16];
                    char path[256];
                    char http_version[16];

                    // Parse the request line: GET /path HTTP/1.0
                    // sscanf is generally safe here if we control buffer sizes
                    if (sscanf(request_buffer, "%15s %255s %15s", method, path, http_version) != 3) {
                        fprintf(stderr, "Bad Request: Could not parse request line from socket %d\n", sd);
                        send_error_response(sd, 400, "Bad Request");
                        CLOSE_SOCKET(sd); client_sockets[i] = 0; continue;
                    }

                    // Only handle GET requests for simplicity
                    if (strcmp(method, "GET") != 0) {
                        fprintf(stderr, "Method Not Implemented: %s from socket %d\n", method, sd);
                        send_error_response(sd, 501, "Not Implemented");
                        CLOSE_SOCKET(sd); client_sockets[i] = 0; continue;
                    }
                    
                    // Default to index.html for root path
                    if (strcmp(path, "/") == 0) {
                        strcpy(path, "/index.html");
                    }

                    // --- Security: Sanitize the requested path ---
                    char clean_path[BUFFER_SIZE];
                    strncpy(clean_path, path + 1, sizeof(clean_path) - 1); // Remove leading '/'
                    clean_path[sizeof(clean_path) - 1] = '\0'; // Ensure null termination

                    if (!sanitize_path(clean_path)) {
                        fprintf(stderr, "Security Alert: Malicious path attempt: %s (socket %d)\n", path, sd);
                        send_error_response(sd, 403, "Forbidden"); // Forbidden or Bad Request
                        CLOSE_SOCKET(sd); client_sockets[i] = 0; continue;
                    }

                    char full_file_path[BUFFER_SIZE];
                    SNPRINTF(full_file_path, sizeof(full_file_path), "%s/%s", WEB_ROOT, clean_path);

                    FILE *file = fopen(full_file_path, "rb"); // Open file in binary read mode
                    if (file == NULL) {
                        fprintf(stderr, "File Not Found: %s (socket %d, error %d)\n", full_file_path, sd, GET_LAST_ERROR());
                        send_error_response(sd, 404, "Not Found");
                    } else {
                        // Get file size
                        fseek(file, 0, SEEK_END);
                        long file_size = ftell(file);
                        fseek(file, 0, SEEK_SET);

                        char response_header[BUFFER_SIZE];
                        const char* mime_type = get_mime_type(full_file_path);

                        // Build HTTP response header
                        SNPRINTF(response_header, sizeof(response_header),
                                 "HTTP/1.0 200 OK\r\n"
                                 "Content‐Type: %s\r\n"
                                 "Content-Length: %ld\r\n"
                                 "Connection: close\r\n" // Important for HTTP/1.0
                                 "\r\n", // End of headers
                                 mime_type, file_size);

                        // Send header
                        if (send(sd, response_header, strlen(response_header), 0) < 0) {
                            fprintf(stderr, "Failed to send header to socket %d: %d\n", sd, GET_LAST_ERROR());
                            fclose(file); CLOSE_SOCKET(sd); client_sockets[i] = 0; continue;
                        }

                        // Send file content in chunks
                        size_t bytes_read;
                        char file_buffer[BUFFER_SIZE];
                        while ((bytes_read = fread(file_buffer, 1, sizeof(file_buffer), file)) > 0) {
                            if (send(sd, file_buffer, bytes_read, 0) < 0) {
                                fprintf(stderr, "Failed to send file data to socket %d: %d\n", sd, GET_LAST_ERROR());
                                break; // Break from sending loop on error
                            }
                        }
                        printf("Served %s (%ld bytes) to socket %d\n", full_file_path, file_size, sd);
                        fclose(file);
                    }
                    
                    // Important for HTTP/1.0: Close connection after response
                    CLOSE_SOCKET(sd);
                    client_sockets[i] = 0; // Mark slot as free
                }
            }
        }
    }

    // Cleanup (in case of server loop break)
    for (i = 0; i < MAX_CLIENTS; i++) {
        if (client_sockets[i] != 0) {
            CLOSE_SOCKET(client_sockets[i]);
        }
    }
    CLOSE_SOCKET(master_socket);
    printf("Server shut down.\n");

    // --- Windows-specific Winsock cleanup ---
    #ifdef _WIN32
        WSACleanup();
    #endif
    // -----------------------------------------

    return 0;
}

7. Testing and Demonstration

To test your web server, You will need to create a `web_root` directory and place some HTML files inside it.

Setup `web_root` Directory:

  1. Create a folder named `web_root` in the same directory as your `http_server.c` executable.

  2. Inside `web_root`, create a file named `index.html` with some simple content:

    
    <!DOCTYPE html>
    <html>
    <head>
        <title>My C Web Server</title>
        <style>
            body { font-family: sans-serif; background-color: #f0f0f0; text-align: center; padding-top: 50px; }
            h1 { color: #333; }
            p { color: #666; }
        </style>
    </head>
    <body>
        <h1>Hello from Your C Web Server!</h1>
        <p>This page was served by your very own HTTP server built with C sockets.</p>
        <p>Try requesting <a href="/another.html">another page</a> or an <a href="/nonexistent.html">unknown page</a>.</p>
    </body>
    </html>
    		
  3. (Optional) Create another file, e.g., `another.html` in `web_root`:

    
    <!DOCTYPE html>
    <html>
    <head><title>Another Page</title></head>
    <body>
        <h1>This is another page!</h1>
        <p><a href="/index.html">Back to home</a></p>
    </body>
    </html>
    		

Compile and Run:

  • Linux/WSL:

    
    gcc http_server.c -o http_server
    ./http_server
    		
  • Windows (MinGW-w64):

    
    gcc http_server.c -o http_server.exe -lws2_32
    http_server.exe
    		

You should see: `HTTP Server listening on port 8080, serving from './web_root/'... Waiting for connections...`

Using a Standard Web Browser:

Open your favorite web browser (Chrome, Firefox, Edge, etc.) and type the following into the address bar:

  • To load `index.html`: `http://localhost:8080/` or `http://localhost:8080/index.html`

  • To load `another.html`: `http://localhost:8080/another.html`

  • To test a 404 error: `http://localhost:8080/nonexistent.html` (or any path that doesn't exist)

Expected Browser Output:

For `index.html`, you should see the content you put in your `index.html` file rendered as a web page.

For `nonexistent.html`, you should see a page with "404 Not Found" as the title and heading, rendered by your server's error handling.

Server Terminal Output:

You will see messages for new connections, incoming HTTP requests, and whether files were served or errors occurred. For instance:


HTTP Server listening on port 8080, serving from './web_root/'...
Waiting for connections...
New connection, socket fd: 4, IP: 127.0.0.1, Port: 58xxx
  -> Assigned to client slot 0
Request from socket 4:
GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 ...

Served ./web_root/index.html (298 bytes) to socket 4
Client disconnected, IP 127.0.0.1, Port 58xxx (socket 4)
New connection, socket fd: 5, IP: 127.0.0.1, Port: 58yyy
  -> Assigned to client slot 0
Request from socket 5:
GET /nonexistent.html HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 ...

File Not Found: ./web_root/nonexistent.html (socket 5, error 2)
Client disconnected, IP 127.0.0.1, Port 58yyy (socket 5)

Note on `HTTP/1.1` vs `HTTP/1.0`: Modern browsers will send `HTTP/1.1` requests by default and expect persistent connections. Our server explicitly sends `Connection: close` and closes the socket after each response, mimicking `HTTP/1.0` behavior. The browser will handle this gracefully by opening a new connection for subsequent requests (e.g., for CSS or images on the same page).

For a truly `HTTP/1.1` compliant server, you would need to implement persistent connections (Keep-Alive), which is a more advanced topic.


General Tips & Best Practices for the Entire Series

Throughout this series, we have emphasized certain practices that are crucial for robust network programming:

  • Consistency: Maintain a consistent code style, naming conventions, and error handling approach across your projects.

  • Cross-Platform Notes: Always provide explicit notes and conditional code blocks (`#ifdef _WIN32`) for Windows (Winsock) differences. This ensures your code is usable on various operating systems.

  • Code Quality: Write well-commented, readable code with proper indentation. This makes your code easier to understand, debug, and maintain.

  • Error Handling: Always check the return values for every socket API call, file operation, and library function. Use `perror()` on Linux/Unix and `WSAGetLastError()` on Windows to get descriptive error messages. Understanding common `errno` values (e.g., `EADDRINUSE`, `ECONNRESET`) is vital.

  • Compile & Run Instructions: Provide clear, step-by-step instructions for both Linux/WSL (using `gcc`) and Windows (using MinGW-w64 `gcc`).

  • Troubleshooting: Dedicate time to understand common issues (e.g., "Address already in use," firewall problems) and debugging tips. Tools like `netstat` (Linux/Windows) or `lsof` (Linux) can help diagnose port issues.

  • Engagement: Experiment! Modify the code, add new features, or intentionally break it to understand its limits and how errors manifest. The best way to learn is by doing.

You have built fundamental networking tools, from simple echoes to a fully functional HTTP server. You now possess a solid foundation in low-level network communication.

Next Steps and Further Learning:

The world of network programming is vast! Here are some areas to explore next:

  • Deeper Concurrency: Explore `poll()` and `epoll()` (Linux) / `kqueue()` (BSD/macOS) for higher-performance I/O multiplexing.

  • Multithreading: Learn how to use threads (e.g., POSIX threads `pthread` or Windows threads) to handle client connections, which can be more efficient than `fork()` and conceptually simpler for some tasks than `select()`.

  • Secure Sockets (SSL/TLS): Dive into using libraries like OpenSSL to add encryption and authentication to your socket applications.

  • IPv6 Socket Programming: Adapt your applications to use IPv6 addresses.

  • Non-blocking I/O: Understand how to set sockets to non-blocking mode and handle I/O without pausing your program.

  • Advanced Protocols: Implement clients or servers for other protocols like FTP, SMTP, or even build your own custom application-layer protocols.

  • Asynchronous I/O: Explore advanced event-driven architectures.


Keep coding, keep experimenting, and keep building! The network awaits your innovations.


Concluding Article: Advanced Socket Options & Error Handling in C



Share:
Buy Domain & Hosting from a trusted company
Web Services Worldwide
About the Author
Rajeev Kumar
CEO, Computer Solutions
Jamshedpur, India

Rajeev Kumar is the primary author of How2Lab. He is a B.Tech. from IIT Kanpur with several years of experience in IT education and Software development. He has taught a wide spectrum of people including fresh young talents, students of premier engineering colleges & management institutes, and IT professionals.

Rajeev has founded Computer Solutions & Web Services Worldwide. He has hands-on experience of building variety of websites and business applications, that include - SaaS based erp & e-commerce systems, and cloud deployed operations management software for health-care, manufacturing and other industries.


Refer a friendSitemapDisclaimerPrivacy
Copyright © How2Lab.com. All rights reserved.