Network Sockets in C
Time: 30 min | Prerequisites: Processes and IPC, Threads and Synchronization | Theory companion: Linux Fundamentals, Section 1.3
Learning Objectives
By the end of this tutorial you will be able to:
- Explain the difference between TCP and UDP sockets
- Build a TCP chat server and client using
socket(),bind(),listen(),accept(),connect() - Map socket operations to the pipe and file descriptor patterns you already know
Before You Start
All exercises run on any Linux machine — your host laptop (Ubuntu, Fedora, Arch, etc.) or the Raspberry Pi via SSH. You need gcc installed:
This tutorial uses pthreads for the chat application. You should be comfortable with pthread_create() and pthread_join() from the Threads and Synchronization tutorial.
1. TCP vs UDP
Pipes and shared memory work between processes on the same machine. To communicate between machines (your laptop and the RPi, or two laptops), you need sockets.
A socket is a file descriptor — you read() and write() just like pipes, but the data travels over the network.
| TCP | UDP | |
|---|---|---|
| Connection | Yes — connect() first |
No — just send |
| Reliable | Yes — retransmits lost packets | No — fire and forget |
| Ordered | Yes | No |
| Use case | SSH, HTTP, sensor logging | Video streaming, DNS |
For a chat application, TCP is the right choice — you don't want to lose messages.
2. TCP Chat — Server and Client
A minimal TCP chat: the server listens for a client, then both sides can type messages back and forth. Each side uses two threads — one for sending (reading from stdin) and one for receiving (reading from the socket).
chat_server.c — waits for a client, then chats
/* chat_server.c — TCP chat server
*
* Build: gcc -Wall -pthread -o chat_server chat_server.c
* Run: ./chat_server 9000
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
volatile int connected = 1;
void *receiver(void *arg)
{
int sock = *(int *)arg;
char buf[256];
int n;
while ((n = recv(sock, buf, sizeof(buf) - 1, 0)) > 0) {
buf[n] = '\0';
printf("\r [Remote] %s\n [You] ", buf);
fflush(stdout);
}
printf("\n Connection closed by remote.\n");
connected = 0;
return NULL;
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "Usage: %s <port>\n", argv[0]);
return 1;
}
int port = atoi(argv[1]);
/* Create socket */
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) { perror("socket"); return 1; }
/* Allow port reuse (avoids "Address already in use" after restart) */
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/* Bind to port */
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = INADDR_ANY,
.sin_port = htons(port),
};
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); return 1;
}
/* Listen for one client */
listen(server_fd, 1);
printf("Waiting for connection on port %d...\n", port);
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &len);
if (client_fd < 0) { perror("accept"); return 1; }
printf("Connected from %s:%d\n\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
/* Receiver thread reads from socket */
pthread_t t_recv;
pthread_create(&t_recv, NULL, receiver, &client_fd);
/* Main thread reads from stdin and sends */
char line[256];
printf(" [You] ");
fflush(stdout);
while (connected && fgets(line, sizeof(line), stdin)) {
line[strcspn(line, "\n")] = '\0';
if (strlen(line) == 0) continue;
send(client_fd, line, strlen(line), 0);
printf(" [You] ");
fflush(stdout);
}
close(client_fd);
close(server_fd);
pthread_join(t_recv, NULL);
return 0;
}
chat_client.c — connects to the server
/* chat_client.c — TCP chat client
*
* Build: gcc -Wall -pthread -o chat_client chat_client.c
* Run: ./chat_client 127.0.0.1 9000 (same machine)
* ./chat_client 192.168.1.42 9000 (across network)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
volatile int connected = 1;
void *receiver(void *arg)
{
int sock = *(int *)arg;
char buf[256];
int n;
while ((n = recv(sock, buf, sizeof(buf) - 1, 0)) > 0) {
buf[n] = '\0';
printf("\r [Remote] %s\n [You] ", buf);
fflush(stdout);
}
printf("\n Connection closed by server.\n");
connected = 0;
return NULL;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "Usage: %s <server-ip> <port>\n", argv[0]);
return 1;
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return 1; }
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(atoi(argv[2])),
};
inet_pton(AF_INET, argv[1], &addr.sin_addr);
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("connect"); return 1;
}
printf("Connected to %s:%s\n\n", argv[1], argv[2]);
pthread_t t_recv;
pthread_create(&t_recv, NULL, receiver, &sock);
char line[256];
printf(" [You] ");
fflush(stdout);
while (connected && fgets(line, sizeof(line), stdin)) {
line[strcspn(line, "\n")] = '\0';
if (strlen(line) == 0) continue;
send(sock, line, strlen(line), 0);
printf(" [You] ");
fflush(stdout);
}
close(sock);
pthread_join(t_recv, NULL);
return 0;
}
Build and Test
Test on the same machine (two terminals):
# Terminal 1: start server
gcc -Wall -pthread -o chat_server chat_server.c
./chat_server 9000
# Terminal 2: connect client
gcc -Wall -pthread -o chat_client chat_client.c
./chat_client 127.0.0.1 9000
Test across the network (laptop to RPi, or laptop to laptop):
# On RPi (server):
./chat_server 9000
# On laptop (client) — use RPi's IP address:
./chat_client 192.168.1.42 9000
Type a message in either terminal — it appears on the other side.
3. How Sockets Map to What You Already Know
Pipe: fd[0] ◄─────────── fd[1] (one direction, same machine)
Socket: sock_fd ◄─── TCP ───► sock_fd (bidirectional, across network)
| Already learned | Socket equivalent |
|---|---|
pipe() creates FDs |
socket() creates FD |
read(fd, ...) |
recv(sock, ...) |
write(fd, ...) |
send(sock, ...) |
close(fd) |
close(sock) |
| Pipe connects parent-child | Socket connects any two machines |
Note
Sockets are file descriptors. This is "everything is a file" in action. You can even use read() and write() instead of recv() and send() — they work identically on TCP sockets. The socket API just adds connect(), bind(), listen(), and accept() for setting up the connection.
4. Extensions
Tip
Extension A: UDP sensor broadcast
Rewrite the server to use SOCK_DGRAM (UDP) and sendto() — broadcast sensor readings to all listeners on the network. No connect(), no accept(). This is how many IoT sensor networks work.
Hint: sendto(sock, buf, len, 0, (struct sockaddr *)&dest, sizeof(dest))
Tip
Extension B: Multi-client server
The current server handles one client. Modify it to accept() in a loop, creating a new thread for each client. Maintain a list of connected client FDs protected by a mutex. When one client sends a message, broadcast it to all others — a real chat room.
What Just Happened?
| Concept | What You Did | Shell Equivalent | Later in Course |
|---|---|---|---|
socket() |
Created a network endpoint | nc (netcat) |
IoT protocols, remote sensor access |
bind() + listen() |
Set up a server on a port | nc -l -p 9000 |
Embedded web servers |
connect() |
Connected to a remote server | nc host port |
Remote sensor clients |
recv() / send() |
Exchanged data over TCP | Read/write in nc |
Telemetry, control protocols |
Forward references:
- Sockets + threads → The SDL2 dashboard tutorials use socket-based data feeds
- TCP server → Embedded web servers (
lighttpd, custom) use the samebind/listen/acceptpattern - UDP broadcast → Sensor networks, service discovery (mDNS, SSDP)
Deliverable
- [ ]
chat_server.ccompiles and runs — waits for a connection on the specified port - [ ]
chat_client.ccompiles and runs — connects to the server - [ ] Chat works between two terminals on the same machine (localhost)
- [ ] Chat works between two different machines (if available)
- [ ] Can explain the difference between TCP and UDP
- [ ] Can map
socket()/recv()/send()to the piperead()/write()model - [ ] (Optional) Extension A or B completed
Checkpoint
| Question | Your Answer |
|---|---|
| Can you chat between two terminals on the same machine? | |
| Can you chat between your laptop and a classmate's? | |
| What happens if you start the client before the server? | |
What does SO_REUSEADDR prevent? |