Skip to Content
Course content

151: Building a Simple TCP Echo Server

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

Alright, let's get our hands dirty with some networking. We're building a TCP Echo Server. If you've never done this, the concept is dead simple: the server sits there, waits for a client to say something, and then immediately shouts that exact same thing back at them. It's the "rubber duck" of networking.

Trying to open the front door

I'll start by just trying to get a socket open. I'm using the POSIX socket API here because it's the foundation for almost everything in Unix-like environments. First, I need a socket, a way to tell the OS which port I want, and a way to actually start listening.

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    bind(server_fd, (struct sockaddr*)&address, sizeof(address));
    listen(server_fd, 3);
    
    std::cout <"Listening on port 8080..."> << std::endl;
    return 0;
}

I ran this, and it seemed to work, but the program immediately exited. That's my first "aha" moment. A server isn't a script; it's a daemon. It needs to stay alive. I need a loop that waits for a connection. But if I just put a while(true) loop around the listen() call, I'm just telling the OS to start listening over and over again, which is redundant and wrong.

The "Hang" and the Accept

Now I need to actually accept a connection. I'll add accept() inside the loop. Here is where things usually get confusing for people. listen() puts the server in a state where it can accept connections, but accept() is the actual handshake that creates a new, specific socket for that one client.

while (true) {
    int addrlen = sizeof(address);
    int new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
    
    if (new_socket < 0) {
        std::cerr <"Accept failed"> << std::endl;
        continue;
    }
    std::cout <"Connection established!"> << std::endl;
    close(new_socket);
}

I tried this, connected via telnet localhost 8080, and it worked—sort of. It printed "Connection established!" and then immediately kicked me out. Why? Because I'm closing the socket immediately. I'm opening the door, saying "Hi!", and then slamming it in the client's face. To make this an echo server, I need to actually read the data sent by the client before I close the connection.

Reading and Echoing

Here is the tricky part. I can't just read once; the client might send a long message in chunks. I need a nested loop: one for the connections, and one for the data flowing through that connection. I'll use a buffer to hold the incoming bytes.

char buffer[1024] = {0};
while (true) {
    int bytes_read = recv(new_socket, buffer, 1024, 0);
    if (bytes_read <= 0) {
        // Connection closed or error
        break;
    }
    
    std::cout <"Received: "> << std::string(buffer, bytes_read) << std::endl;
    
    // Now, the "Echo" part
    send(new_socket, buffer, bytes_read, 0);
}
close(new_socket);

I tested this with a few messages, and it worked! But then I noticed something annoying. If I sent "Hello", the server echoed "Hello", but the next time I sent a message, the buffer still had bits of the old message in it if the new one was shorter. I forgot to clear the buffer or, more accurately, I was relying on the bytes_read count. While send() uses the count correctly, my std::cout was acting weird because I wasn't null-terminating the string.

Cleaning up the Mess

To wrap this up, I realized that if I kill the server and try to restart it immediately, I get a "Address already in use" error. This is a classic socket headache. The OS keeps the port in a TIME_WAIT state. To fix this, I have to set the SO_REUSEADDR socket option before calling bind().

int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

With that added, the server is robust enough for a basic experiment. It handles the connection, loops through the data, sends it back, and lets me restart the process without waiting for the OS to clear the port cache. It's not a production-grade server—it's single-threaded, so one slow client will block everyone else—but that's a problem for another lesson.




📋 Practical Task

Exercise: The Uppercase Echo Server

Your task is to modify the TCP Echo Server we built in this lesson. Instead of returning the exact text the client sent, the server should transform all lowercase letters in the received message to uppercase letters before sending them back.

  • Modify the recv loop to iterate through the buffer.
  • Use std::toupper() from the <cctype> header to convert the characters.
  • Ensure that you only modify the bytes actually received (up to bytes_read).
  • Test it using telnet or nc (netcat) to ensure that "hello world" becomes "HELLO WORLD".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.