The Socket API
Networking on Linux starts with sockets. A socket is a file descriptor that represents one end of a network conversation. Every networked program you have ever used -- web browsers, SSH clients, game servers -- builds on the same handful of system calls: socket(), bind(), listen(), accept(), connect().
This chapter walks through each call, the address structures that feed them, and the DNS resolution machinery that maps hostnames to addresses.
The Two Workflows
Before any code, understand the two fundamental patterns.
CLIENT SERVER
------ ------
socket() socket()
| |
connect() -----> [network] -----> bind()
| |
write()/read() listen()
| |
close() accept() ---> new fd
|
read()/write()
|
close()
The client creates a socket and immediately connects. The server creates a socket, binds it to an address, starts listening, and accepts incoming connections.
Address Structures
Every socket call that touches an address needs a struct sockaddr. In practice you never use the generic one directly. You fill in a protocol-specific structure and cast it.
struct sockaddr (generic, 16 bytes)
+--------+---------------------------+
| family | 14 bytes of data |
+--------+---------------------------+
struct sockaddr_in (IPv4)
+--------+--------+------------------+
| AF_INET| port | 4-byte IPv4 addr| + 8 bytes padding
+--------+--------+------------------+
struct sockaddr_in6 (IPv6)
+--------+--------+------+-----------+----------+
|AF_INET6| port |flow | 16-byte IPv6 addr | + scope_id
+--------+--------+------+-----------+----------+
Creating a Socket in C
/* create_socket.c -- create a TCP socket and print its fd */
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main(void)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
printf("TCP socket fd = %d\n", fd);
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_fd < 0) {
perror("socket");
close(fd);
return 1;
}
printf("UDP socket fd = %d\n", udp_fd);
close(fd);
close(udp_fd);
return 0;
}
The three arguments: address family (AF_INET for IPv4, AF_INET6 for IPv6), socket type (SOCK_STREAM for TCP, SOCK_DGRAM for UDP), and protocol (0 lets the kernel pick the obvious one).
Caution: A socket fd is just a number. If you forget to close it, you leak a file descriptor. In a long-running server, this eventually hits the per-process fd limit and new connections silently fail.
Filling an Address: inet_pton
inet_pton converts a human-readable address string into binary form. inet_ntop goes the other direction.
/* addr_convert.c -- convert addresses between text and binary */
#include <stdio.h>
#include <arpa/inet.h>
int main(void)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); /* host-to-network byte order */
if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) != 1) {
fprintf(stderr, "bad address\n");
return 1;
}
/* Convert back to string */
char buf[INET_ADDRSTRLEN];
const char *result = inet_ntop(AF_INET, &addr.sin_addr,
buf, sizeof(buf));
if (!result) {
perror("inet_ntop");
return 1;
}
printf("Address: %s Port: %d\n", buf, ntohs(addr.sin_port));
/* IPv6 example */
struct sockaddr_in6 addr6;
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(9090);
inet_pton(AF_INET6, "::1", &addr6.sin6_addr);
char buf6[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &addr6.sin6_addr, buf6, sizeof(buf6));
printf("IPv6 Address: %s Port: %d\n", buf6, ntohs(addr6.sin6_port));
return 0;
}
htons and ntohs convert between host byte order and network byte order (big-endian). Every multi-byte field in a sockaddr must be in network byte order.
Caution: Forgetting
htons()on the port is a classic bug. Port 8080 in little-endian becomes 47137 in big-endian. Your server binds to the wrong port and you spend an hour debugging.
DNS Resolution: getaddrinfo
Hard-coding IP addresses is fragile. getaddrinfo resolves hostnames and service names, returning a linked list of address structures ready to pass to connect() or bind().
/* resolve.c -- resolve a hostname to IP addresses */
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "usage: %s hostname\n", argv[0]);
return 1;
}
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
hints.ai_socktype = SOCK_STREAM; /* TCP */
int status = getaddrinfo(argv[1], "http", &hints, &res);
if (status != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 1;
}
for (p = res; p != NULL; p = p->ai_next) {
char ipstr[INET6_ADDRSTRLEN];
void *addr;
const char *ipver;
if (p->ai_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
addr = &ipv4->sin_addr;
ipver = "IPv4";
} else {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
addr = &ipv6->sin6_addr;
ipver = "IPv6";
}
inet_ntop(p->ai_family, addr, ipstr, sizeof(ipstr));
printf(" %s: %s\n", ipver, ipstr);
}
freeaddrinfo(res);
return 0;
}
getaddrinfo is thread-safe, handles both IPv4 and IPv6, and replaces the older gethostbyname. Always use it.
Try It: Compile
resolve.cand run it withlocalhost, thengoogle.com, then a hostname that does not exist. Observe the error fromgai_strerror.
A Complete TCP Client in C
/* tcp_client.c -- connect to a server, send a message, read reply */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "usage: %s host port\n", argv[0]);
return 1;
}
struct addrinfo hints, *res;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
int status = getaddrinfo(argv[1], argv[2], &hints, &res);
if (status != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 1;
}
int sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sockfd < 0) {
perror("socket");
freeaddrinfo(res);
return 1;
}
if (connect(sockfd, res->ai_addr, res->ai_addrlen) < 0) {
perror("connect");
close(sockfd);
freeaddrinfo(res);
return 1;
}
freeaddrinfo(res);
const char *msg = "Hello, server!\n";
write(sockfd, msg, strlen(msg));
char buf[1024];
ssize_t n = read(sockfd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Server replied: %s", buf);
}
close(sockfd);
return 0;
}
The flow: resolve address, create socket, connect, write, read, close. Every real-world client follows this skeleton.
A Complete TCP Server in C
/* tcp_server.c -- accept one connection, echo, exit */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void)
{
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) { perror("socket"); return 1; }
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(7878);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(listen_fd);
return 1;
}
if (listen(listen_fd, 5) < 0) {
perror("listen");
close(listen_fd);
return 1;
}
printf("Listening on port 7878...\n");
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr,
&client_len);
if (conn_fd < 0) {
perror("accept");
close(listen_fd);
return 1;
}
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("Connection from %s:%d\n", client_ip, ntohs(client_addr.sin_port));
char buf[1024];
ssize_t n = read(conn_fd, buf, sizeof(buf));
if (n > 0) {
write(conn_fd, buf, n); /* echo back */
}
close(conn_fd);
close(listen_fd);
return 0;
}
SO_REUSEADDR lets you restart the server immediately after stopping it. Without it, the kernel holds the port in TIME_WAIT state for up to 60 seconds.
Try It: Run the server in one terminal and the client in another (
./tcp_client 127.0.0.1 7878). Then modify the server to handle multiple connections in a loop instead of exiting after the first one.
TCP vs UDP
| Feature | TCP (SOCK_STREAM) | UDP (SOCK_DGRAM) |
|---|---|---|
| Connection | Yes (connect/accept) | No (sendto/recvfrom) |
| Reliability | Guaranteed delivery | Best-effort |
| Ordering | Preserved | Not guaranteed |
| Framing | Byte stream | Message boundaries |
| Overhead | Higher (handshake, ACKs) | Lower |
| Typical use | HTTP, SSH, databases | DNS, gaming, streaming |
Rust: std::net
Rust's standard library wraps the socket API into safe, high-level types. No raw sockaddr structs, no casts, no byte-order functions to remember.
// tcp_client.rs -- connect, send, receive use std::io::{Read, Write}; use std::net::TcpStream; fn main() -> std::io::Result<()> { let mut stream = TcpStream::connect("127.0.0.1:7878")?; stream.write_all(b"Hello, server!\n")?; let mut buf = [0u8; 1024]; let n = stream.read(&mut buf)?; print!("Server replied: {}", String::from_utf8_lossy(&buf[..n])); Ok(()) }
One line to connect. One line to write. One line to read. The ? operator propagates errors without crashing.
// tcp_server.rs -- accept one connection, echo use std::io::{Read, Write}; use std::net::TcpListener; fn main() -> std::io::Result<()> { let listener = TcpListener::bind("0.0.0.0:7878")?; println!("Listening on port 7878..."); let (mut stream, addr) = listener.accept()?; println!("Connection from {}", addr); let mut buf = [0u8; 1024]; let n = stream.read(&mut buf)?; stream.write_all(&buf[..n])?; Ok(()) }
Rust Note:
TcpListener::bindhandlessocket(),bind(), andlisten()in a single call. The address is parsed from a string automatically.SO_REUSEADDRis set by default on most platforms.
Rust: UDP
// udp_example.rs -- send and receive a datagram use std::net::UdpSocket; fn main() -> std::io::Result<()> { let socket = UdpSocket::bind("0.0.0.0:0")?; // OS picks port socket.send_to(b"ping", "127.0.0.1:9000")?; let mut buf = [0u8; 1024]; let (n, src) = socket.recv_from(&mut buf)?; println!("Got {} bytes from {}: {}", n, src, String::from_utf8_lossy(&buf[..n])); Ok(()) }
Rust: The nix Crate for Low-Level Control
When you need setsockopt, raw sockaddr manipulation, or socket options that std::net does not expose, use the nix crate.
// nix_socket.rs -- create a socket with nix for low-level control // Cargo.toml: nix = { version = "0.29", features = ["net"] } use nix::sys::socket::{ socket, bind, listen, accept, AddressFamily, SockType, SockFlag, SockaddrIn, }; use std::net::Ipv4Addr; use std::io::{Read, Write}; use std::os::fd::FromRawFd; fn main() -> Result<(), Box<dyn std::error::Error>> { let fd = socket( AddressFamily::Inet, SockType::Stream, SockFlag::empty(), None, )?; let addr = SockaddrIn::new(0, 0, 0, 0, 7879); // 0.0.0.0:7879 bind(fd.as_raw_fd(), &addr)?; listen(&fd, nix::sys::socket::Backlog::new(5)?)?; println!("nix: listening on port 7879"); let conn_fd = accept(&fd)?; println!("nix: accepted connection"); // Wrap in std File for Read/Write traits let mut stream = unsafe { std::net::TcpStream::from_raw_fd(conn_fd) }; let mut buf = [0u8; 256]; let n = stream.read(&mut buf)?; stream.write_all(&buf[..n])?; Ok(()) }
Driver Prep: In kernel modules, you will encounter
struct socketandsock_create_kern(), which mirror the userspace socket API. Understanding the syscall interface here maps directly to the kernel's internal socket layer.
Data Flow Through the Stack
Application: write(fd, buf, len)
|
+---------+
| TCP/UDP | segmentation, checksums, sequence numbers
+---------+
|
+---------+
| IP | routing, fragmentation, TTL
+---------+
|
+---------+
| Driver | DMA to NIC hardware
+---------+
|
[wire]
Every write() to a socket sends data down this stack. Every read() pulls data up.
Knowledge Check
- What is the difference between
SOCK_STREAMandSOCK_DGRAM? Which transport protocol does each imply? - Why must you call
htons()on the port number before storing it insockaddr_in? - What does
getaddrinforeturn, and why is it preferred overgethostbyname?
Common Pitfalls
- Forgetting
htons/htonl-- your address or port is silently wrong. - Not checking return values --
connect()can fail for dozens of reasons. - Not calling
freeaddrinfo-- leaks the linked list returned bygetaddrinfo. - Using
INADDR_ANYwithouthtonl-- works by accident on little-endian (0 is 0 in any byte order), butINADDR_LOOPBACKwill not. - Assuming
read()returns a complete message -- TCP is a byte stream. Onewrite()can arrive as multipleread()calls. - Binding to a specific address when you want all interfaces -- use
INADDR_ANY(0.0.0.0) to accept connections on every interface.