A Simple Echo Server
Serve many clients at once.
A Simple Echo Server is a free C Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What We Are Building
An echo server accepts TCP connections and sends back whatever bytes a client sends. It is the "hello world" of network programming.
We will build it on epoll so a single thread serves many clients at once. The pieces are: a listening socket, the epoll loop, and per-client read-then-write handling.
Creating the Listening Socket
Start with socket() for an IPv4 TCP endpoint. The triple is AF_INET, SOCK_STREAM, and protocol 0.
This returns a file descriptor that we will bind, mark non-blocking, and eventually register with epoll. Check for -1 on every system call.
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) { perror("socket"); exit(1); }Reusing the Address
Before binding, set SO_REUSEADDR with setsockopt(). This lets the server restart and rebind to the same port without waiting out the TIME_WAIT state.
Without it, a quick restart often fails with EADDRINUSE, which is a frustrating surprise during development.
int yes = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);Binding to a Port
Fill a struct sockaddr_in with the family, port, and address. Use htons() for the port and INADDR_ANY to listen on all interfaces.
Then call bind() to attach the socket to that address. htons converts host byte order to network byte order, which is required.
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
bind(listen_fd, (struct sockaddr*)&addr, sizeof addr);Listening and Going Non-Blocking
Call listen() to mark the socket passive and set a backlog for pending connections. SOMAXCONN requests the system maximum.
Then switch the socket to non-blocking so accept() never stalls the event loop. Now it is ready to register with epoll.
listen(listen_fd, SOMAXCONN);
set_nonblocking(listen_fd);Registering the Listener
Create the epoll instance and add the listening socket with EPOLLIN. A readable event here means a new client is waiting to be accepted.
We store the fd in data.fd so we can recognize the listener inside the loop.
int epfd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = listen_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);Accepting a Client
When the listener is readable, loop accept() until EAGAIN. Each new client socket is set non-blocking and registered for EPOLLIN.
From here on, epoll will tell us whenever that client sends data we can echo back.
int c = accept(listen_fd, NULL, NULL);
if (c != -1) {
set_nonblocking(c);
struct epoll_event cev = { .events = EPOLLIN, .data.fd = c };
epoll_ctl(epfd, EPOLL_CTL_ADD, c, &cev);
}The Echo Logic
For a ready client, recv() into a buffer. If you get bytes, send the same bytes straight back with send().
This simple read-then-write is the echo. In production you would buffer unsent bytes, but for a demo we assume the small reply fits the send buffer.
char buf[4096];
ssize_t r = recv(fd, buf, sizeof buf, 0);
if (r > 0) {
send(fd, buf, r, 0);
}Detecting Disconnect
A recv() return of 0 means the client performed an orderly shutdown. A negative return with anything other than EAGAIN is a real error.
In both cases, remove the descriptor from epoll and close() it to free resources and stop further events.
if (r == 0 || (r < 0 && errno != EAGAIN)) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}The Full Loop
Combine the parts: wait for events, branch on whether the descriptor is the listener or a client, and act accordingly.
This compact loop is a fully functional concurrent echo server. One thread, many clients, no per-connection threads.
for (;;) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == listen_fd) accept_clients();
else echo_or_close(events[i].data.fd);
}
}Testing and Next Steps
Compile on Linux and connect with nc localhost 8080; whatever you type comes back. Open several terminals to prove concurrency.
To harden it: buffer partial sends, handle EPOLLOUT, switch to EPOLLET with full draining, and handle EPOLLRDHUP for half-closes.
Quick Check
Test your understanding of the echo server flow.
Recap
You built an epoll echo server: create, set SO_REUSEADDR, bind, listen, and go non-blocking. Register the listener, accept clients in a loop, and echo with recv()/send().
Handle a 0 return as disconnect and close cleanly. From this skeleton you can grow a high-performance, single-threaded network server.
Frequently asked questions
Is the “A Simple Echo Server” lesson free?
Yes — the full text of “A Simple Echo Server” is free to read here on the web, and the C Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C Academy course, upgrade to CoddyKit PRO.
What will I learn in “A Simple Echo Server”?
Serve many clients at once. You practise C Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C Academy?
No prior experience is required. C Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “A Simple Echo Server” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C Academy lesson?
Yes. Every C Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Blocking vs Non-Blocking I/O
- Setting Up epoll
- The Event Loop
- A Simple Echo Server