C Sockets and Network Programming
Socket programming allows C programs to communicate across networks. POSIX systems provide socket APIs that can be used to build clients, servers, network utilities, and distributed applications.
This topic focuses on the commonly used POSIX socket interface, including TCP and UDP communication.
What Is a Socket?
A socket is an operating-system communication endpoint. Once created, a socket is represented by a file descriptor that can be configured and used with networking APIs.
A socket can communicate with another endpoint on the same machine or across a network.
TCP vs UDP
| Feature | TCP | UDP |
|---|---|---|
| Connection | Connection-oriented | Connectionless |
| Reliability | Reliable ordered byte stream | No delivery or ordering guarantee |
| Data model | Byte stream | Datagrams |
| Typical use | Web services, file transfer, APIs | Streaming, discovery, real-time applications |
| Setup | listen/accept or connect | sendto/recvfrom |
Creating a Socket
The socket() function creates a socket and returns a file descriptor.
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
return EXIT_FAILURE;
}
printf("Socket created: %d\n", fd);
close(fd);
return EXIT_SUCCESS;
}
AF_INET specifies IPv4 addressing, while SOCK_STREAM requests a stream socket, normally used with TCP.
Common Socket Parameters
| Parameter | Meaning |
|---|---|
| AF_INET | IPv4 |
| AF_INET6 | IPv6 |
| SOCK_STREAM | Stream socket, commonly TCP |
| SOCK_DGRAM | Datagram socket, commonly UDP |
IPv4 Socket Addresses
IPv4 socket addresses are commonly represented with struct sockaddr_in.
#include <netinet/in.h>
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_port = htons(8080);
address.sin_addr.s_addr = htonl(INADDR_ANY);
Network protocols use network byte order. Functions such as htons() and htonl() convert host-order integers into network byte order.
A TCP Server
A basic TCP server normally follows this sequence: create a socket, bind it to a local address and port, listen for connections, accept a client, exchange data, and close the connection.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void)
{
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1)
{
perror("socket");
return EXIT_FAILURE;
}
int reuse = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof reuse);
struct sockaddr_in address = {0};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_port = htons(8080);
if (bind(server_fd,
(struct sockaddr *)&address,
sizeof address) == -1)
{
perror("bind");
close(server_fd);
return EXIT_FAILURE;
}
if (listen(server_fd, 10) == -1)
{
perror("listen");
close(server_fd);
return EXIT_FAILURE;
}
printf("Server listening on port 8080...\n");
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd == -1)
{
perror("accept");
close(server_fd);
return EXIT_FAILURE;
}
const char response[] = "Hello from server\n";
if (send(client_fd, response, sizeof response - 1, 0) == -1)
{
perror("send");
}
close(client_fd);
close(server_fd);
return EXIT_SUCCESS;
}
Understanding bind()
bind() associates a socket with a local address. A TCP server generally binds to the local IP address and port on which it expects incoming connections.
if (bind(server_fd,
(struct sockaddr *)&address,
sizeof address) == -1)
{
perror("bind");
}
Binding to INADDR_ANY allows an IPv4 server to listen on the host's available IPv4 interfaces.
Understanding listen()
listen() changes a TCP socket into a listening socket. The backlog parameter provides a limit related to pending connection requests.
if (listen(server_fd, 10) == -1)
{
perror("listen");
}
Understanding accept()
accept() accepts an incoming TCP connection and returns a new socket descriptor representing the connection to that client. The original listening socket remains available for accepting additional connections.
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd == -1)
{
perror("accept");
}
A TCP Client
A TCP client typically creates a socket, constructs the server address, and calls connect().
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
return EXIT_FAILURE;
}
struct sockaddr_in server = {0};
server.sin_family = AF_INET;
server.sin_port = htons(8080);
if (inet_pton(AF_INET, "127.0.0.1", &server.sin_addr) != 1)
{
fprintf(stderr, "Invalid server address.\n");
close(fd);
return EXIT_FAILURE;
}
if (connect(fd,
(struct sockaddr *)&server,
sizeof server) == -1)
{
perror("connect");
close(fd);
return EXIT_FAILURE;
}
char buffer[256];
ssize_t bytes = recv(fd, buffer, sizeof buffer - 1, 0);
if (bytes == -1)
{
perror("recv");
close(fd);
return EXIT_FAILURE;
}
buffer[bytes] = '\0';
printf("Server: %s", buffer);
close(fd);
return EXIT_SUCCESS;
}
Understanding connect()
connect() establishes a connection to a remote socket for a connection-oriented protocol such as TCP.
if (connect(fd,
(struct sockaddr *)&server,
sizeof server) == -1)
{
perror("connect");
}
send() and recv()
TCP sockets commonly use send() and recv() to exchange data.
const char message[] = "Hello\n";
send(fd, message, sizeof message - 1, 0);
char buffer[1024];
ssize_t bytes = recv(fd, buffer, sizeof buffer, 0);
TCP provides a byte stream, not a message protocol. One send() call does not necessarily correspond to one recv() call. Applications that exchange structured messages must define their own framing protocol.
Handling Partial Sends
A send() operation can write fewer bytes than requested. When an application must send an entire buffer, it should loop until all bytes have been transferred or an error occurs.
#include <sys/socket.h>
#include <stddef.h>
int sendAll(int fd, const void *data, size_t size)
{
const char *bytes = data;
size_t sent = 0;
while (sent < size)
{
ssize_t result = send(fd, bytes + sent,
size - sent, 0);
if (result <= 0)
{
return -1;
}
sent += (size_t)result;
}
return 0;
}
Detecting Connection Closure
For a TCP stream, recv() returning zero indicates that the peer has performed an orderly shutdown of its sending side and no more data is available.
ssize_t bytes = recv(fd, buffer, sizeof buffer, 0);
if (bytes == 0)
{
printf("Peer closed the connection.\n");
}
else if (bytes < 0)
{
perror("recv");
}
A Simple TCP Echo Server
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void)
{
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1)
{
perror("socket");
return EXIT_FAILURE;
}
struct sockaddr_in address = {0};
address.sin_family = AF_INET;
address.sin_port = htons(8080);
address.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server_fd, (struct sockaddr *)&address,
sizeof address) == -1)
{
perror("bind");
close(server_fd);
return EXIT_FAILURE;
}
if (listen(server_fd, 10) == -1)
{
perror("listen");
close(server_fd);
return EXIT_FAILURE;
}
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd == -1)
{
perror("accept");
close(server_fd);
return EXIT_FAILURE;
}
char buffer[1024];
ssize_t bytes;
while ((bytes = recv(client_fd, buffer,
sizeof buffer, 0)) > 0)
{
ssize_t sent = 0;
while (sent < bytes)
{
ssize_t result = send(client_fd,
buffer + sent,
(size_t)(bytes - sent),
0);
if (result <= 0)
{
perror("send");
close(client_fd);
close(server_fd);
return EXIT_FAILURE;
}
sent += result;
}
}
if (bytes < 0)
{
perror("recv");
}
close(client_fd);
close(server_fd);
return EXIT_SUCCESS;
}
UDP Sockets
UDP uses datagrams rather than a persistent byte-stream connection. A UDP socket can send data to a destination using sendto() and receive datagrams with recvfrom().
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1)
{
perror("socket");
return EXIT_FAILURE;
}
struct sockaddr_in destination = {0};
destination.sin_family = AF_INET;
destination.sin_port = htons(9000);
if (inet_pton(AF_INET, "127.0.0.1",
&destination.sin_addr) != 1)
{
fprintf(stderr, "Invalid address.\n");
close(fd);
return EXIT_FAILURE;
}
const char message[] = "Hello UDP";
if (sendto(fd, message, sizeof message - 1, 0,
(struct sockaddr *)&destination,
sizeof destination) == -1)
{
perror("sendto");
close(fd);
return EXIT_FAILURE;
}
close(fd);
return EXIT_SUCCESS;
}
A UDP Server
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1)
{
perror("socket");
return EXIT_FAILURE;
}
struct sockaddr_in address = {0};
address.sin_family = AF_INET;
address.sin_port = htons(9000);
address.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(fd, (struct sockaddr *)&address,
sizeof address) == -1)
{
perror("bind");
close(fd);
return EXIT_FAILURE;
}
char buffer[1024];
struct sockaddr_in sender;
socklen_t senderLength = sizeof sender;
ssize_t bytes = recvfrom(fd, buffer, sizeof buffer - 1, 0,
(struct sockaddr *)&sender,
&senderLength);
if (bytes == -1)
{
perror("recvfrom");
close(fd);
return EXIT_FAILURE;
}
buffer[bytes] = '\0';
printf("Received: %s\n", buffer);
close(fd);
return EXIT_SUCCESS;
}
TCP Message Framing
Because TCP is a byte stream, an application needs a protocol for identifying where one logical message ends and another begins.
- Fixed-size messages
- Delimiter-based messages
- Length-prefixed messages
- Self-describing serialization formats
For example, a length-prefixed protocol might send a fixed-size length field followed by exactly that many payload bytes.
IPv4 Address Conversion
inet_pton() converts a textual IP address into binary network-address form.
#include <arpa/inet.h>
#include <stdio.h>
struct in_addr address;
if (inet_pton(AF_INET, "192.168.1.10", &address) != 1)
{
fprintf(stderr, "Invalid IPv4 address.\n");
}
inet_ntop() performs the reverse conversion from binary address form to readable text.
#include <arpa/inet.h>
#include <stdio.h>
char text[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &address,
text, sizeof text) != NULL)
{
printf("Address: %s\n", text);
}
Hostname and Service Resolution
Modern POSIX network programs commonly use getaddrinfo() to resolve hostnames and service names and obtain address structures suitable for socket operations.
#include <netdb.h>
#include <stdio.h>
#include <string.h>
struct addrinfo hints = {0};
struct addrinfo *result;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
int status = getaddrinfo("example.com", "80",
&hints, &result);
if (status != 0)
{
fprintf(stderr, "getaddrinfo: %s\n",
gai_strerror(status));
}
else
{
freeaddrinfo(result);
}
AF_UNSPEC allows the resolver to return IPv4 or IPv6 addresses. This approach is generally more flexible than manually constructing an IPv4-only address.
A More Portable TCP Client Pattern
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void)
{
struct addrinfo hints = {0};
struct addrinfo *result;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
int status = getaddrinfo("localhost", "8080",
&hints, &result);
if (status != 0)
{
fprintf(stderr, "getaddrinfo: %s\n",
gai_strerror(status));
return EXIT_FAILURE;
}
int fd = -1;
for (struct addrinfo *rp = result; rp != NULL; rp = rp->ai_next)
{
fd = socket(rp->ai_family,
rp->ai_socktype,
rp->ai_protocol);
if (fd == -1)
{
continue;
}
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0)
{
break;
}
close(fd);
fd = -1;
}
freeaddrinfo(result);
if (fd == -1)
{
fprintf(stderr, "Could not connect.\n");
return EXIT_FAILURE;
}
close(fd);
return EXIT_SUCCESS;
}
Socket Options
setsockopt() allows programs to configure socket behavior. A common server option is SO_REUSEADDR, which can make restarting a server easier when recently used addresses are still subject to TCP state rules.
int enabled = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
&enabled, sizeof enabled) == -1)
{
perror("setsockopt");
}
Closing a Socket
A socket is a file descriptor, so close() is normally used to release it.
if (close(fd) == -1)
{
perror("close");
}
shutdown()
shutdown() can disable communication in one or both directions without immediately closing the socket descriptor.
if (shutdown(fd, SHUT_WR) == -1)
{
perror("shutdown");
}
SHUT_WR indicates that the application will no longer send data. This can be useful when one side has finished transmitting but still wants to receive data.
Blocking Sockets
By default, many socket operations are blocking. For example, accept() can wait until a connection arrives and recv() can wait until data becomes available.
Blocking behavior simplifies small programs but can become problematic in servers that need to handle many clients simultaneously.
Nonblocking Sockets
A socket can be configured for nonblocking operation. Nonblocking I/O allows a program to avoid waiting indefinitely for an operation to complete.
Event mechanisms such as select(), poll(), epoll(), or kqueue() can then be used depending on the target platform and architecture.
Handling Multiple TCP Clients
A simple server can handle clients sequentially, but real servers often use processes, threads, or an event-driven architecture.
for (;;)
{
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd == -1)
{
perror("accept");
continue;
}
/* Handle client. */
close(client_fd);
}
For concurrent servers, a common POSIX design is to accept a client and then hand it to a worker process or thread. Event-driven servers instead monitor many descriptors through an I/O multiplexing mechanism.
Sockets and fork()
Because sockets are file descriptors, they can be inherited across fork(). A classic concurrent server pattern is to accept a connection, fork a child to handle it, and have the parent continue accepting clients.
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd == -1)
{
perror("accept");
}
else
{
pid_t pid = fork();
if (pid == 0)
{
close(server_fd);
/* Handle client_fd. */
close(client_fd);
_exit(0);
}
close(client_fd);
}
A production implementation must also handle fork() failures and collect child processes to avoid zombies.
Network Byte Order
Network protocols define byte ordering for multi-byte numeric fields. The conversion functions htons(), htonl(), ntohs(), and ntohl() are commonly used with socket programming.
| Function | Conversion |
|---|---|
| htons() | Host short to network short |
| htonl() | Host long to network long |
| ntohs() | Network short to host short |
| ntohl() | Network long to host long |
Do Not Send Raw C Structs Blindly
Sending a C struct directly over a network can cause portability problems because of padding, alignment, integer representation, endianness, and differences between platforms.
Define an explicit wire format and serialize each field in a documented representation.
Network Error Handling
Many socket functions report errors through their return values and errno. getaddrinfo() is an important exception: its errors should be interpreted using getaddrinfo's return value and gai_strerror(), not simply by assuming errno contains the relevant resolver error.
int status = getaddrinfo(host, service,
&hints, &result);
if (status != 0)
{
fprintf(stderr, "getaddrinfo: %s\n",
gai_strerror(status));
}
Security Considerations
Network programs receive data from potentially untrusted sources. Never assume that received bytes form a valid string, integer, structure, or request.
- Validate message lengths before parsing
- Avoid buffer overflows
- Define maximum request sizes
- Validate numeric ranges
- Handle malformed messages
- Avoid trusting client-provided metadata
- Use encrypted protocols such as TLS when confidentiality or authentication is required
Avoiding Buffer Overflows
char buffer[256];
ssize_t bytes = recv(fd, buffer,
sizeof buffer - 1, 0);
if (bytes > 0)
{
buffer[bytes] = '\0';
}
The size of the destination buffer must always be considered when receiving network data. Network input should never be treated as automatically trustworthy.
TCP Client-Server Sequence
| Server | Client |
|---|---|
| socket() | socket() |
| bind() | connect() |
| listen() | send()/recv() |
| accept() | close() |
| send()/recv() | |
| close() |
UDP Communication Sequence
A typical UDP server creates a datagram socket, binds it to a local address, and receives datagrams with recvfrom(). A client can use sendto() to transmit a datagram to the server.
Common Mistakes
- Forgetting to check socket API return values
- Assuming one TCP send() equals one TCP recv()
- Ignoring partial sends
- Treating TCP as a message-oriented protocol
- Failing to handle recv() returning zero
- Forgetting network byte-order conversions
- Sending raw C structs as a portable network format
- Trusting received data without validation
- Forgetting to close sockets
- Using blocking sockets without considering server concurrency
Best Practices
- Use getaddrinfo() for flexible address resolution
- Check every important socket operation for failure
- Design an explicit application-level protocol
- Handle partial TCP sends and receives correctly
- Validate all network input
- Use network byte order for protocol fields
- Avoid transmitting raw implementation-dependent C structures
- Close sockets and clean up resources reliably
- Use concurrency or I/O multiplexing for multi-client servers
- Use TLS or another secure transport when sensitive data crosses an untrusted network
Quick Reference
| API | Purpose |
|---|---|
| socket() | Create a socket |
| bind() | Associate a socket with a local address |
| listen() | Prepare a TCP socket to accept connections |
| accept() | Accept a TCP connection |
| connect() | Connect a socket to a remote endpoint |
| send()/recv() | Exchange data over connected sockets |
| sendto()/recvfrom() | Exchange UDP datagrams |
| getaddrinfo() | Resolve host and service information |
| inet_pton()/inet_ntop() | Convert IP addresses between text and binary forms |
| shutdown() | Disable one or both directions of communication |
| close() | Release a socket descriptor |
Practice Exercises
- Build a TCP server that sends a greeting to one client
- Build a TCP client that connects to the server and prints the response
- Create a TCP echo server that handles multiple messages
- Implement a sendAll() helper for reliable buffer transmission
- Build a UDP sender and receiver using sendto() and recvfrom()
- Use getaddrinfo() to create an IPv4/IPv6-capable client
- Add a simple length-prefixed message protocol to a TCP application
- Create a concurrent server using fork() and wait-related process management
- Add input validation and maximum message-size limits to a network service
Conclusion
C socket programming provides the foundation for network clients and servers on POSIX systems. TCP offers a reliable ordered byte stream, while UDP provides lightweight datagram communication. Understanding socket creation, addressing, connection management, data transfer, message framing, error handling, and resource cleanup is essential for building reliable network applications.