Skip to Content
Course content

149: Socket Programming in C++

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been meaning to show you how sockets actually work. Most people treat them like magic black boxes provided by a library, but if you've ever wondered why a connection "hangs" or why data sometimes arrives in fragments, you have to go down to the Berkeley sockets API. It's a bit clunky—it's essentially a C API—but that's where the real understanding happens.

I want to build a tiny "System Health Daemon." The idea is simple: a server that sits on my machine, and when a client sends the word STATUS, the server replies with the current system uptime. Let's try to get a basic listener running.

Getting a handle on the network

First, I need a socket. In C++, especially on Unix-like systems, a socket is essentially just a file descriptor. I'll start by calling socket(). I'm going for TCP, so I'll use SOCK_STREAM.

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
    perror("Socket failed");
    return -1;
}

Okay, that worked. But a socket is just a generic endpoint. It doesn't know where it lives on the network. I need to bind it to an IP address and a port. I'll use INADDR_ANY because I don't care which network interface the request comes through—WiFi, Ethernet, whatever.

The port permission trap

Here is where I usually trip up when I'm rushing. I tried to bind this to port 80 because, well, it's the standard HTTP port. I ran the code and... bind() returned -1. I checked errno and it was EACCES (Permission Denied).

Right. I forgot that on Linux/macOS, you can't bind to any port below 1024 unless you're running as root. I'm not running this as sudo, so I'll bump it up to 8080. Let's see if that clears it.

struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080); // htons is critical; it handles Big Endian vs Little Endian

if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
    perror("Bind failed");
    // Now it actually works!
}

Note that htons() call. If you forget "host-to-network-short," your port number will be byte-swapped, and you'll spend an hour wondering why your client can't connect to a port that looks correct in your code but is actually something completely different in the eyes of the kernel.

Waiting for someone to talk back

Now the socket is bound, but it's not actually "listening." It's just a bound file descriptor. I need to put it in passive mode using listen(), and then call accept().

I ran my code, and then it just... stopped. The terminal froze. I thought I'd crashed the program, but then I realized: accept() is a blocking call. It's literally putting my thread to sleep until a client attempts to connect. This is a crucial realization. If you want a server that does other things while waiting, you'll eventually need threads or select()/poll()/epoll(), but for our health daemon, we can just wait.

listen(server_fd, 3); // Allow a backlog of 3 connections
int addrlen = sizeof(address);
int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);

// The moment a client connects, we land here.

The stream vs. packet realization

Now for the actual logic. I'll read from new_socket. I'm expecting the string "STATUS". I'll use read() into a buffer. Here's the thing that bites everyone: TCP is a stream protocol, not a packet protocol. If the client sends "STATUS", there is no guarantee that read() will return all 6 bytes in one go. It might return "STAT" and then "US" in the next call.

For this tiny example, I'll keep it simple and assume it arrives together, but in a production system, you'd need a loop and a delimiter (like a newline \n) to know when a message is actually complete. Let's try to send the response back.

char buffer[1024] = {0};
read(new_socket, buffer, 1024);

if (std::string(buffer) == "STATUS") {
    const char* response = "System OK - Uptime: 14 hours\n";
    send(new_socket, response, strlen(response), 0);
}
close(new_socket);
close(server_fd);

I tested this with telnet localhost 8080 (or nc localhost 8080 if you're on Linux), typed "STATUS", and there it was. It's not fancy, and it's certainly not thread-safe, but we've just bypassed the high-level abstractions and talked directly to the OS network stack.




📋 Practical Task

Build a Remote Command-Response Daemon

Your task is to expand on the System Health Daemon. Instead of just responding to "STATUS", create a server that supports three specific commands:

  • TIME: The server should respond with the current system time (use std::time and std::ctime).
  • PING: The server should respond with "PONG".
  • QUIT: The server should send a "Goodbye" message and then shut down the server entirely.

Requirements:

  • The server must listen on port 9000.
  • You must handle the read() buffer carefully to ensure you don't have trailing null characters or garbage interfering with your string comparisons.
  • The server should be able to handle multiple clients sequentially (meaning it stays in a loop, accepting one client, processing their command, closing that connection, and then waiting for the next client) until the QUIT command is received.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.