Home C / C++ Socket Programming in C: TCP and UDP for Production Systems
Advanced 3 min · July 13, 2026

Socket Programming in C: TCP and UDP for Production Systems

Master TCP and UDP socket programming in C with production-ready examples, debugging guides, and real-world incident analysis.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of C programming (pointers, structs, functions).
  • Understanding of IP addresses and ports.
  • Familiarity with Linux/Unix command line and compilation with gcc.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Sockets are endpoints for communication between processes over a network.
  • TCP provides reliable, connection-oriented communication; UDP is connectionless and faster but unreliable.
  • Key functions: socket(), bind(), listen(), accept(), connect(), send(), recv().
  • Always check return values and handle errors gracefully.
  • Use non-blocking I/O or select/poll for scalable servers.
✦ Definition~90s read
What is Socket Programming in C?

Socket programming in C is the practice of using the Berkeley sockets API to create network applications that communicate over TCP or UDP protocols.

Think of sockets like phone calls vs.
Plain-English First

Think of sockets like phone calls vs. text messages. TCP is like a phone call: you dial, connect, talk, and hang up. UDP is like sending a postcard: you drop it in the mail and hope it arrives. TCP ensures everything arrives in order; UDP is faster but can lose data.

Imagine you're building a chat application, a file transfer tool, or a multiplayer game. At the heart of these systems lies socket programming—the ability to send and receive data across a network. In C, sockets are the foundation of network communication, used by everything from web servers to database clients. This tutorial will take you from zero to production-ready TCP and UDP socket code. You'll learn the essential functions, handle errors like a pro, and avoid common pitfalls that cause crashes or security holes. We'll cover both TCP (reliable, ordered) and UDP (fast, lightweight) with real-world examples. By the end, you'll be able to write a simple HTTP server or a UDP-based game server. Let's dive into the world of Berkeley sockets.

1. Understanding Sockets: The Basics

A socket is an endpoint for communication between two machines. In C, socket programming uses the Berkeley sockets API. There are two main types: SOCK_STREAM (TCP) and SOCK_DGRAM (UDP). TCP provides reliable, ordered, error-checked delivery. UDP is connectionless and faster but unreliable. The socket() function creates a socket: int sock = socket(AF_INET, SOCK_STREAM, 0); AF_INET is for IPv4. Always check the return value; on error it returns -1. The socket descriptor is a file descriptor, so you can use read()/write() or send()/recv().

create_socket.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket creation failed");
        return 1;
    }
    printf("Socket created successfully: %d\n", sock);
    close(sock);
    return 0;
}
Output
Socket created successfully: 3
💡Always check return values
📊 Production Insight
In production, set SO_REUSEADDR to avoid 'Address already in use' errors after a restart.
🎯 Key Takeaway
Socket creation is the first step; always handle errors.

2. TCP Server: Bind, Listen, Accept

A TCP server follows these steps: create a socket, bind to an address and port, listen for incoming connections, and accept them. bind() associates the socket with a local address (struct sockaddr_in). listen() marks the socket as passive, ready to accept connections. accept() blocks until a client connects, returning a new socket for communication. Here's a simple echo server that sends back whatever it receives.

tcp_server.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // Creating socket file descriptor
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Forcefully attaching socket to port 8080
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // Bind the socket to the network address and port
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // Listen for incoming connections
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    // Accept a connection
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    // Read and echo back
    int valread = read(new_socket, buffer, BUFFER_SIZE);
    printf("Received: %s\n", buffer);
    send(new_socket, buffer, strlen(buffer), 0);
    printf("Echo message sent\n");

    close(new_socket);
    close(server_fd);
    return 0;
}
Output
Server listening on port 8080
Received: Hello
Echo message sent
🔥Port numbers
📊 Production Insight
In production, use non-blocking accept() with select() or epoll to handle multiple clients concurrently.
🎯 Key Takeaway
TCP server requires socket, bind, listen, accept sequence.

3. TCP Client: Connect and Communicate

A TCP client creates a socket and connects to the server using connect(). It then sends and receives data. The client needs the server's IP address and port. Here's a client that connects to the echo server above.

tcp_client.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char *hello = "Hello from client";
    char buffer[1024] = {0};

    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation error");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // Convert IPv4 address from text to binary
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        perror("invalid address");
        return -1;
    }

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connection failed");
        return -1;
    }

    send(sock, hello, strlen(hello), 0);
    printf("Hello message sent\n");
    int valread = read(sock, buffer, 1024);
    printf("Received: %s\n", buffer);

    close(sock);
    return 0;
}
Output
Hello message sent
Received: Hello from client
⚠ inet_pton vs inet_addr
📊 Production Insight
Always handle partial sends/recvs by looping until all data is transferred.
🎯 Key Takeaway
Client uses socket, connect, send, recv.

4. UDP Server and Client: Connectionless Communication

UDP is connectionless. The server creates a socket, binds to a port, and then uses recvfrom() to receive data from any client. The client uses sendto() to send data. No connect() or accept() is needed. Here's a simple UDP echo server and client.

udp_client.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    char buffer[BUFFER_SIZE];
    char *hello = "Hello UDP";
    struct sockaddr_in servaddr;

    // Creating socket file descriptor
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    sendto(sockfd, hello, strlen(hello), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("Hello message sent\n");

    int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, NULL, NULL);
    buffer[n] = '\0';
    printf("Received: %s\n", buffer);

    close(sockfd);
    return 0;
}
Output
Hello message sent
Received: Hello UDP
🔥UDP vs TCP
📊 Production Insight
UDP servers often need to handle packet loss and reordering at the application layer.
🎯 Key Takeaway
UDP uses recvfrom/sendto without connection setup.

5. Handling Multiple Clients with select()

For a TCP server to handle multiple clients concurrently, you can use select() to monitor multiple file descriptors. select() blocks until one or more sockets are ready for I/O. This allows a single-threaded server to manage many connections. Here's a simple example that accepts new connections and echoes data from existing ones.

tcp_select_server.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
    int server_fd, new_socket, client_sockets[MAX_CLIENTS], max_sd, sd, activity, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};
    fd_set readfds;

    // Initialize all client sockets to 0
    for (int i = 0; i < MAX_CLIENTS; i++) {
        client_sockets[i] = 0;
    }

    // Create server socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

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

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

        activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
        if ((activity < 0) && (errno != EINTR)) {
            perror("select error");
        }

        // If something happened on the master socket, it's a new connection
        if (FD_ISSET(server_fd, &readfds)) {
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            printf("New connection, socket fd: %d\n", new_socket);

            // Add new socket to array of sockets
            for (int i = 0; i < MAX_CLIENTS; i++) {
                if (client_sockets[i] == 0) {
                    client_sockets[i] = new_socket;
                    printf("Adding to list of sockets as %d\n", i);
                    break;
                }
            }
        }

        // Check all client sockets for data
        for (int i = 0; i < MAX_CLIENTS; i++) {
            sd = client_sockets[i];
            if (FD_ISSET(sd, &readfds)) {
                if ((valread = read(sd, buffer, 1024)) == 0) {
                    // Client disconnected
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    printf("Host disconnected, ip %s, port %d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
                    close(sd);
                    client_sockets[i] = 0;
                } else {
                    buffer[valread] = '\0';
                    send(sd, buffer, strlen(buffer), 0);
                }
            }
        }
    }

    return 0;
}
Output
Server listening on port 8080
New connection, socket fd: 4
Adding to list of sockets as 0
Host disconnected, ip 127.0.0.1, port 54321
💡select() limitations
📊 Production Insight
In production, use epoll() for better scalability with thousands of connections.
🎯 Key Takeaway
select() enables single-threaded handling of multiple clients.

6. Error Handling and Best Practices

Network programming is error-prone. Always check return values. Common errors: EINTR (interrupted system call), EAGAIN/EWOULDBLOCK (non-blocking socket no data), ECONNRESET (peer reset connection). Use perror() or strerror(errno) for logging. Set SO_REUSEADDR to avoid 'Address already in use'. For non-blocking sockets, handle EAGAIN properly. Also, be careful with byte ordering: use htons(), htonl(), ntohs(), ntohl() for port and IP. Always close sockets when done to avoid file descriptor leaks.

error_handling.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>

void handle_error(const char *msg) {
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
}

int main() {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        handle_error("socket");
        return 1;
    }
    // Simulate an error
    if (connect(sock, NULL, 0) < 0) {
        handle_error("connect");
    }
    close(sock);
    return 0;
}
Output
connect: Bad address
⚠ Signal handling
📊 Production Insight
Use a logging library to capture errors with timestamps and context.
🎯 Key Takeaway
Robust error handling is critical for production network code.
● Production incidentPOST-MORTEMseverity: high

The Silent Connection Drop: A Production Outage

Symptom
Users reported intermittent connection failures; the server appeared to hang.
Assumption
The developer assumed a network issue or firewall problem.
Root cause
The server used a single-threaded accept loop without handling SIGPIPE or checking errno properly. When a client disconnected abruptly, a subsequent write caused SIGPIPE, killing the process.
Fix
Added signal handler for SIGPIPE (SIG_IGN) and proper error checking on send/recv. Also implemented non-blocking sockets with select() to handle multiple clients.
Key lesson
  • Always handle SIGPIPE in network servers to avoid silent crashes.
  • Check return values of send/recv for errors and closed connections.
  • Use non-blocking I/O or multiplexing for scalable servers.
  • Test with concurrent clients to uncover race conditions.
  • Log all errors with errno for debugging.
Production debug guideSymptom to Action4 entries
Symptom · 01
Connection refused
Fix
Check if server is running and listening on the correct port. Use netstat -tlnp.
Symptom · 02
Connection timed out
Fix
Check firewall rules, network connectivity, and server load. Use ping and traceroute.
Symptom · 03
Data corruption or missing bytes
Fix
Verify send/recv return values. Ensure proper buffer management and endianness.
Symptom · 04
Server crash after many connections
Fix
Look for SIGPIPE, file descriptor leaks, or memory exhaustion. Use valgrind and strace.
★ Quick Debug Cheat SheetCommon socket errors and immediate actions.
bind() fails with EADDRINUSE
Immediate action
Wait or set SO_REUSEADDR
Commands
int opt=1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
netstat -tlnp | grep <port>
Fix now
Add setsockopt before bind.
accept() returns -1+
Immediate action
Check errno; if EAGAIN/EWOULDBLOCK, retry; else log error.
Commands
perror("accept");
strace -p <pid>
Fix now
Handle EINTR and EAGAIN.
send() returns 0 or -1+
Immediate action
Check if connection closed; handle SIGPIPE.
Commands
signal(SIGPIPE, SIG_IGN);
if (n <= 0) close(sock);
Fix now
Ignore SIGPIPE and check return value.
recv() returns 0+
Immediate action
Peer closed connection; close socket.
Commands
if (n == 0) { close(sock); break; }
shutdown(sock, SHUT_RDWR);
Fix now
Properly close and clean up.
FeatureTCPUDP
ConnectionConnection-orientedConnectionless
ReliabilityReliable (acknowledgments)Unreliable (no ack)
OrderingOrdered deliveryNo ordering
SpeedSlower due to overheadFaster
Use casesWeb, email, file transferStreaming, gaming, DNS
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
create_socket.cint main() {1. Understanding Sockets
tcp_server.cint main() {2. TCP Server
tcp_client.cint main() {3. TCP Client
udp_client.cint main() {4. UDP Server and Client
tcp_select_server.cint main() {5. Handling Multiple Clients with select()
error_handling.cvoid handle_error(const char *msg) {6. Error Handling and Best Practices

Key takeaways

1
Sockets are the foundation of network communication in C.
2
TCP is reliable and connection-oriented; UDP is fast but unreliable.
3
Always check return values and handle errors gracefully.
4
Use select/poll/epoll for handling multiple clients.
5
Set SO_REUSEADDR and ignore SIGPIPE for robust servers.

Common mistakes to avoid

5 patterns
×

Forgetting to convert byte order with htons()

×

Not checking return values of send/recv

×

Using blocking sockets without handling EINTR

×

Not setting SO_REUSEADDR before bind

×

Ignoring SIGPIPE

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the steps to create a TCP server in C.
Q02SENIOR
What is the difference between select() and epoll()?
Q03SENIOR
How do you handle partial sends in TCP?
Q04JUNIOR
What is the purpose of SO_REUSEADDR?
Q05SENIOR
How would you design a UDP-based reliable protocol?
Q01 of 05JUNIOR

Explain the steps to create a TCP server in C.

ANSWER
Create socket with socket(), bind to address with bind(), listen for connections with listen(), accept a client with accept(), then send/recv data.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between TCP and UDP?
02
How do I handle multiple clients in a TCP server?
03
Why do I get 'Address already in use'?
04
What is the purpose of htons() and ntohs()?
05
How do I make a socket non-blocking?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's C Basics. Mark it forged?

3 min read · try the examples if you haven't

Previous
Memory Safety in C: Secure Coding Practices
20 / 24 · C Basics
Next
Build Tools for C: Make, CMake, and Meson