Linux - socket(), bind(), listen(), accept(), connect()
In Linux and POSIX systems, network and inter-process stream communication is built upon the Berkeley Sockets (BSD) API. Every connection lifecycle—from local Unix domain pipes to high-throughput internet servers—is driven by a coordinated state machine of foundational system calls:
TCP Socket Connection Lifecycle State Machine:
Passive Server (Listener) Active Client
───────────────────────── ─────────────
socket() socket()
│ │
▼ │
bind() │
│ │
▼ │
listen() │
│ │
▼ ▼
accept() / accept4() ◄──[ SYN, SYN-ACK, ACK ]─── connect()
│ │
▼ ▼
[ Connected socket fd ] [ Connected socket fd ]
│ │
▼ ▼
read() / recv() ◄─── Data Stream ───► write() / send()
│ │
▼ ▼
close() ───[ FIN / ACK ]───► close()
1. socket() and socketpair(): Creating Endpoints
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
int socketpair(int domain, int type, int protocol, int sv[2]);
- Return value: An integer file descriptor representing the socket endpoint, or
-1on error witherrnoset.
[!NOTE] What makes a file descriptor a socket: You cannot get a socket file descriptor by calling
open(2)(in fact, callingopen()on a Unix domain socket file on disk fails withENXIO). A socket FD is only created by socket-producing syscalls (socket(),socketpair(),accept4(), or received viaSCM_RIGHTS). Read more in Socket vs. General File Descriptor I/O.
Domains (Address Families)
The domain parameter tells the kernel which communication protocol layer to instantiate:
| Domain | Protocol Family | Addressing Scheme | Typical Use Case |
|---|---|---|---|
AF_INET |
IPv4 Internet protocols | 32-bit IP address + 16-bit port (struct sockaddr_in) |
Standard internet traffic. |
AF_INET6 |
IPv6 Internet protocols | 128-bit IPv6 address + 16-bit port (struct sockaddr_in6) |
Modern dual-stack services. |
AF_UNIX / AF_LOCAL |
Local IPC | Filesystem path or abstract name (struct sockaddr_un) |
High-speed on-machine IPC (Docker, systemd). |
AF_NETLINK |
Kernel-to-user routing | Kernel subsystem ID (struct sockaddr_nl) |
Network routing, firewall rules (iptables/nftables). |
AF_PACKET |
Raw Ethernet frames | Network device interface index | Packet sniffers (tcpdump, Wireshark). |
Socket Types & Modern Linux Flags
The type defines the delivery semantics. Crucially, modern Linux allows bitwise OR-ing creation flags directly into type:
SOCK_STREAM: Sequenced, reliable, two-way connection-based byte streams (TCP forAF_INET, stream socket forAF_UNIX).SOCK_DGRAM: Connectionless, unreliable datagrams of fixed maximum length (UDP).SOCK_SEQPACKET: Sequenced, reliable, two-way connection-based datagrams that preserve record message boundaries (SCTP or Unix seqpacket).SOCK_RAW: Direct access to network layer protocols (bypassing transport layer).
Atomic Creation Flags (SOCK_NONBLOCK & SOCK_CLOEXEC)
Historically, configuring a non-blocking socket required two separate system calls:
// Legacy approach: 2 system calls + race condition vulnerability
int fd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
fcntl(fd, F_SETFD, FD_CLOEXEC);
In multi-threaded programs, if another thread called fork() + execve() between socket() and fcntl(FD_CLOEXEC), the file descriptor leaked into the child process.
Modern Linux solves this by allowing atomic flag specification inside socket():
// Modern approach: 1 atomic system call, zero leaks
int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
socketpair(): Instant Unnamed Bidirectional IPC
socketpair() creates two connected, anonymous sockets in an array sv[2]. It is restricted to the AF_UNIX domain:
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv) == -1) {
perror("socketpair");
}
pid_t pid = fork();
if (pid == 0) {
// Child process: closes sv[0], talks via sv[1]
close(sv[0]);
write(sv[1], "ping", 4);
close(sv[1]);
_exit(0);
} else {
// Parent process: closes sv[1], reads from sv[0]
close(sv[1]);
char buf[16];
read(sv[0], buf, sizeof(buf));
close(sv[0]);
}
Unlike unidirectional pipe(), socketpair() provides a full-duplex communication channel and supports passing file descriptors via sendmsg().
2. bind(): Assigning an Identity
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
When a socket is created, it has a domain and type but no address. bind() assigns a local identity to the socket:
- For
AF_INET: binds to a local IP address and port (e.g.,0.0.0.0:8080). - For
AF_UNIX: binds to a filesystem path (e.g.,/run/my-app.sock) or an abstract socket name.
Ephemeral Ports & Special Addresses
INADDR_ANY(0.0.0.0): Listens on all available local network interfaces.- Port
0: Tells the kernel to allocate an available dynamic ephemeral port (from/proc/sys/net/ipv4/ip_local_port_range, typically32768–60999). After binding, callgetsockname()to discover the port allocated by the OS.
Common Errors
EADDRINUSE: Another process is already listening on this port, or a previous instance of your server died and the connection is stuck in the 60-secondTIME_WAITstate. (Solution: enableSO_REUSEADDRbefore binding via setsockopt).EACCES: Attempted to bind to a privileged port (< 1024) withoutCAP_NET_BIND_SERVICEor root privileges.
3. listen(): The Dual-Queue Engine
int listen(int sockfd, int backlog);
listen() transforms an active socket into a passive listening socket ready to accept incoming client connections.
How the Linux Kernel Handles Incoming TCP Connections:
[ Client sends SYN ]
│
▼
┌────────────────────────────────────────────────────────┐
│ 1. SYN Queue (Incomplete Connection Queue) │
│ Stores half-open handshakes (SYN_RECV state) │
│ Cap: /proc/sys/net/ipv4/tcp_max_syn_backlog │
└────────────────────────────┬───────────────────────────┘
│ [ Client sends ACK ]
▼
┌────────────────────────────────────────────────────────┐
│ 2. Accept Queue (Completed Connection Queue) │
│ Stores 3-way handshake established sockets │
│ Cap: min(backlog, /proc/sys/net/core/somaxconn) │
└────────────────────────────┬───────────────────────────┘
│ [ Server calls accept() ]
▼
[ Application receives new fd ]
The backlog Parameter and Queue Limits
The backlog parameter specifies the maximum length of the Accept Queue (connections that have completed the 3-way handshake and are waiting for user space to call accept()):
- The actual queue capacity is capped at
min(backlog, /proc/sys/net/core/somaxconn). - If
somaxconnis4096and you passlisten(fd, 128), the queue size is128. - If the Accept Queue fills up and a new connection arrives, the kernel typically drops the client's ACK silently (allowing the client to retransmit) unless
/proc/sys/net/ipv4/tcp_abort_on_overflowis set to1(which sends an RST).
4. accept() and accept4(): Reaping Established Connections
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
sockfd: The listening (passive) socket.addr: Output pointer filled with the remote client's IP and port.- Return value: A brand new connected file descriptor dedicated exclusively to communicating with this specific client. The original
sockfdremains open and passive, ready to accept further incoming connections.
accept4(): Eliminating Race Conditions
Like socket(), Linux provides accept4() to apply SOCK_NONBLOCK and SOCK_CLOEXEC to the new socket descriptor atomically:
struct sockaddr_storage client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept4(listen_fd,
(struct sockaddr *)&client_addr,
&addr_len,
SOCK_NONBLOCK | SOCK_CLOEXEC);
Without accept4(), in high-concurrency event loops (like Nginx, Envoy, or libuv), every accepted connection requires two additional fcntl() syscalls, doubling context-switch overhead.
5. connect(): Active Handshake Initiation
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect() initiates a connection from an active socket to the remote listening peer specified by addr:
- If the socket has not yet been bound via
bind(), the kernel automatically binds it to an available local IP address and ephemeral port. - For TCP sockets,
connect()starts the TCP three-way handshake (sending SYN).
Non-Blocking connect() Pattern
When using non-blocking I/O (SOCK_NONBLOCK), connect() cannot block waiting for the multi-millisecond network round-trip. Instead:
connect()returns-1immediately witherrno == EINPROGRESS.- The application adds the socket descriptor to an epoll instance, monitoring for write events (
EPOLLOUT). - When
epoll_wait()notifies that the descriptor is writable, check whether the connection succeeded or failed:
int result;
socklen_t result_len = sizeof(result);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &result, &result_len) < 0 || result != 0) {
// Connection failed (e.g. ECONNREFUSED, ETIMEDOUT)
fprintf(stderr, "Connection error: %s\n", strerror(result));
} else {
// Connection established successfully!
}
Calling connect() on UDP Sockets
While UDP is inherently connectionless, calling connect() on a SOCK_DGRAM socket is a standard performance optimization:
- It records the remote peer address inside the kernel socket data structure.
- You can now use standard
write()/read()andsend()/recv()instead ofsendto()/recvfrom(). - The kernel filters incoming packets, delivering only datagrams from that specific peer.
- The socket receives asynchronous ICMP "Port Unreachable" errors on subsequent reads/writes (standard UDP sockets discard ICMP errors silently).
6. shutdown() vs close(): Graceful Teardown
int shutdown(int sockfd, int how);
Closing a network connection involves two distinct concepts: closing the OS file descriptor handle vs. terminating the underlying TCP network session.
| System Call | Effect on File Descriptor | Effect on Network Protocol (TCP) |
|---|---|---|
close(fd) |
Decrements the process descriptor reference count. Once refcount reaches 0, frees the fd and sends a FIN packet. |
If child processes inherited the fd (e.g. via fork()), TCP remains open until all processes close it! |
shutdown(fd, how) |
The file descriptor remains valid and open in the process table. | Immediately terminates the TCP connection phase, regardless of how many other processes hold copies of the descriptor! |
The how Flags in shutdown()
SHUT_RD: Disables further receive operations. Any incoming data buffered or arriving later is acknowledged and discarded.SHUT_WR(TCP Half-Close): Transmits a TCP FIN packet to the peer, signaling: "I am done sending data, but I am still listening for your response."SHUT_RDWR: Disables both transmission and reception.
Summary Table
| System Call | Primary Role | Key Options / Flags | Common Errors |
|---|---|---|---|
socket() |
Allocates endpoint | SOCK_NONBLOCK, SOCK_CLOEXEC |
EMFILE (too many open fds), EAFNOSUPPORT |
socketpair() |
Creates 2 connected local sockets | AF_UNIX, SOCK_STREAM, SOCK_DGRAM |
EMFILE, EOPNOTSUPP |
bind() |
Assigns local IP/port or path | Port 0 (ephemeral), INADDR_ANY |
EADDRINUSE, EACCES (< 1024 without root) |
listen() |
Converts socket to passive receiver | backlog (size of Accept queue) |
EADDRINUSE, EDESTADDRREQ |
accept4() |
Reaps established connection | SOCK_NONBLOCK, SOCK_CLOEXEC |
EAGAIN / EWOULDBLOCK, EMFILE |
connect() |
Initiates active handshake | EINPROGRESS (non-blocking) |
ECONNREFUSED, ETIMEDOUT, ENETUNREACH |
shutdown() |
Directional TCP half-close | SHUT_RD, SHUT_WR, SHUT_RDWR |
ENOTCONN, EBADF |
Frequently Asked Questions (FAQ)
Can you obtain a socket file descriptor by calling open()?
No. Calling open() or openat() on a Unix domain socket inode on disk always fails with ENXIO (No such device or address). A socket file descriptor is a bidirectional communication endpoint that must be initialized with internal kernel socket buffers, protocol operations (proto_ops), and state machines. You can only create or acquire a socket file descriptor through dedicated socket-producing system calls such as socket(), socketpair(), accept(), accept4(), or by receiving an existing one over a Unix domain socket via recvmsg() with SCM_RIGHTS.
What is the difference between shutdown() and close() on a socket?
close() only decrements the process descriptor reference count, whereas shutdown() immediately terminates protocol communication on the connection across all processes. If a socket descriptor is shared with child processes across fork() or duplicated via dup(), calling close() on one descriptor does not transmit a TCP FIN packet or close the TCP session until every reference across all processes reaches zero. In contrast, calling shutdown(fd, SHUT_RDWR) transmits a TCP FIN packet immediately, regardless of how many processes or descriptors reference the underlying connection.
What happens when the listen() backlog queue overflows in Linux?
By default, Linux silently drops incoming client handshake ACKs without sending an error, forcing the client to retransmit. When the Accept Queue reaches its limit (min(backlog, /proc/sys/net/core/somaxconn)), the Linux kernel ignores the final ACK of the three-way handshake so the client will back off and retransmit, giving the server application time to catch up and call accept(). However, if /proc/sys/net/ipv4/tcp_abort_on_overflow is set to 1, the kernel immediately responds with a TCP RST to reset and abort the connection.
Why should you prefer accept4() over accept() in production servers?
accept4() atomically sets SOCK_NONBLOCK and SOCK_CLOEXEC on the accepted socket descriptor at creation time, eliminating race conditions and avoiding extra syscalls. In multi-threaded programs, if another thread calls fork() and execve() between a standard accept() call and a subsequent fcntl(fd, F_SETFD, FD_CLOEXEC), the file descriptor leaks into the child process. Furthermore, setting non-blocking and cloexec flags via fcntl() requires two extra syscall context switches per connection.
Related Topics
- Socket I/O Syscalls (
send,recv,sendmsg): Streaming, datagrams, and passing file descriptors over SCM. - Socket Options Syscalls (
setsockopt,getsockopt): Fine-tuningSO_REUSEPORT,SO_REUSEADDR, andTCP_NODELAY. - Linux - Sockets Overview: Architectural comparisons, Unix Domain Sockets vs TCP/IP.
- Linux - epoll: High-performance I/O event notification for scalable servers.
