Assignment Chef icon Assignment Chef
All English tutorials

Programming lesson

Building a TCP TextFilter Server with Event-Driven Architecture in C

Learn how to implement a TCP client and server with a three-way handshake and event-driven concurrency in C, using the TextFilter project as a practical example.

TCP server C tutorial event-driven server three-way handshake C TextFilter project concurrent server select() C programming socket programming C network programming assignment C TCP client server asynchronous communication C single-threaded concurrency real-time server example game server networking AI server architecture high-performance TCP C networking tutorial

Introduction to TCP and the TextFilter Project

In today's hyper-connected world, understanding how data travels across networks is crucial for any aspiring software engineer. The Transmission Control Protocol (TCP) is the backbone of most internet communication, from loading social media feeds to streaming your favorite shows. In this tutorial, we'll walk through building a TCP client and server that performs a three-way handshake—a fundamental process that establishes a reliable connection. This project, often called TextFilter, is a classic assignment in networking courses, but we'll give it a modern twist by implementing an event-driven server.

Event-driven programming is everywhere: think of how a live sports app updates scores in real-time without refreshing, or how your favorite game handles multiple players simultaneously. By the end of this tutorial, you'll have a solid grasp of synchronous and asynchronous communication, and you'll be able to build a concurrent server using a single thread—a skill that's highly relevant in high-performance applications like AI inference servers or real-time data pipelines.

Understanding the Three-Way Handshake

Before diving into code, let's recap the TCP three-way handshake. It's a three-step process to establish a connection:

  1. SYN: The client sends a SYN (synchronize) packet to the server.
  2. SYN-ACK: The server responds with SYN-ACK (synchronize-acknowledge).
  3. ACK: The client sends an ACK (acknowledge) packet.

In our TextFilter project, we simulate this handshake using custom "HELLO" messages. The client sends "HELLO X", the server replies with "HELLO Y" where Y = X+1, and finally the client sends "HELLO Z" with Z = Y+1. If the numbers don't match, we print an error. This simple protocol teaches you the basics of stateful communication.

Think of it like a three-step verification in a multiplayer game: Player 1 sends a join request (HELLO X), the server acknowledges with a session ID (HELLO X+1), and Player 1 confirms (HELLO X+2). If the IDs mismatch, the connection fails.

Part A: Implementing the Client

The client is straightforward: it creates a TCP socket, connects to the server, sends the initial HELLO message, reads the server's response, and sends the final message. Here's a skeleton:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
    connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr));

    char buffer[1024] = {0};
    int x = 1; // example X value
    snprintf(buffer, sizeof(buffer), "HELLO %d", x);
    send(sock, buffer, strlen(buffer), 0);
    printf("Sent: %s\n", buffer);

    read(sock, buffer, 1024);
    printf("Received: %s\n", buffer);

    // Parse Y and send Z = Y+1
    int y;
    sscanf(buffer, "HELLO %d", &y);
    int z = y + 1;
    snprintf(buffer, sizeof(buffer), "HELLO %d", z);
    send(sock, buffer, strlen(buffer), 0);
    printf("Sent: %s\n", buffer);

    close(sock);
    return 0;
}

This client works for both parts B and C. Notice the use of inet_pton to convert IP address and htons for port number. Always flush stdout with fflush(stdout) after printing to ensure output appears immediately.

Part B: Single-Threaded Server (Synchronous)

The server listens for incoming connections, accepts one client at a time, and handles the handshake. This is a blocking server: if one client takes time, others wait. Here's a basic implementation:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in address;
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    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);

    int addrlen = sizeof(address);
    int new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen);

    char buffer[1024] = {0};
    read(new_socket, buffer, 1024);
    printf("Received: %s\n", buffer);
    fflush(stdout);

    // Parse X and send Y = X+1
    int x;
    sscanf(buffer, "HELLO %d", &x);
    int y = x + 1;
    snprintf(buffer, sizeof(buffer), "HELLO %d", y);
    send(new_socket, buffer, strlen(buffer), 0);
    printf("Sent: %s\n", buffer);
    fflush(stdout);

    // Wait for Z
    read(new_socket, buffer, 1024);
    printf("Received: %s\n", buffer);
    fflush(stdout);

    int z;
    sscanf(buffer, "HELLO %d", &z);
    if (z != y + 1) {
        printf("ERROR\n");
        fflush(stdout);
    }

    close(new_socket);
    close(server_fd);
    return 0;
}

This works for a single client, but what if you want to handle multiple clients concurrently? That's where event-driven programming comes in.

Part C: Event-Driven Server (Asynchronous)

An event-driven server uses a single thread to manage multiple connections by monitoring file descriptors for activity. This is ideal for I/O-bound tasks like our handshake. We'll use select() or poll(). Here's a version using select():

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in address;
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    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);

    fd_set readfds;
    int max_sd;
    int client_sockets[30] = {0}; // track clients

    while (1) {
        FD_ZERO(&readfds);
        FD_SET(server_fd, &readfds);
        max_sd = server_fd;

        for (int i = 0; i < 30; i++) {
            int sd = client_sockets[i];
            if (sd > 0) FD_SET(sd, &readfds);
            if (sd > max_sd) max_sd = sd;
        }

        int activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);

        if (FD_ISSET(server_fd, &readfds)) {
            int new_socket = accept(server_fd, NULL, NULL);
            for (int i = 0; i < 30; i++) {
                if (client_sockets[i] == 0) {
                    client_sockets[i] = new_socket;
                    break;
                }
            }
        }

        for (int i = 0; i < 30; i++) {
            int sd = client_sockets[i];
            if (FD_ISSET(sd, &readfds)) {
                char buffer[1024] = {0};
                int valread = read(sd, buffer, 1024);
                if (valread == 0) {
                    close(sd);
                    client_sockets[i] = 0;
                } else {
                    printf("Received: %s\n", buffer);
                    fflush(stdout);
                    // Handle handshake state
                    // Simplified: just echo back for demo
                    send(sd, buffer, strlen(buffer), 0);
                }
            }
        }
    }
    return 0;
}

This server can handle multiple clients concurrently without threading. Each client's state (like expected next number) would be stored in a structure associated with the socket. This pattern is used in high-performance web servers like Nginx and in real-time systems like game servers.

Compilation and Testing

Compile both programs with gcc:

gcc -o tcpclient tcpclient.c
gcc -o tcpserver tcpserver.c

Run the server first, then the client. Use 127.0.0.1 for local testing. Make sure to fix all warnings—the autograder will reject code with warnings.

Common Mistakes and Tips

  • Forgetting to convert byte order: Use htons() and htonl() for port and IP.
  • Not flushing stdout: Always call fflush(stdout) after printf.
  • Blocking on read: In the event-driven server, handle partial reads and maintain state.
  • Buffer overflow: Ensure buffer size is adequate and null-terminate strings.

Real-World Applications

Event-driven TCP servers are used in many modern systems:

  • AI Chatbots: Handling multiple user sessions with a single thread.
  • Live Sports Updates: Pushing scores to thousands of clients simultaneously.
  • Online Gaming: Managing player connections and game state.
  • Financial Trading: Processing high-frequency orders with low latency.

By mastering this project, you're not just completing an assignment—you're building skills that are directly applicable to cutting-edge technologies.

Conclusion

In this tutorial, we implemented a TCP client and server that perform a three-way handshake, and we extended it to an event-driven concurrent server. This project teaches you the fundamentals of networking, synchronous vs. asynchronous communication, and the power of event-driven architectures. Whether you're building the next viral app or a high-frequency trading system, these concepts are essential. Now go ahead and apply these skills to your own projects!