Sockets Introduction Socket API: bind(), listen(), accept(), connect()

 

Chapter 56: Sockets Introduction
Part 2 of 5 β€” Socket API: bind(), listen(), accept(), connect()
πŸ”— bind()
πŸ‘‚ listen()
βœ… accept()
πŸ“ž connect()

The Socket Lifecycle

After creating a socket with socket(), there is a sequence of system calls that set it up for communication. The exact sequence differs between a server and a client, and also differs between stream and datagram sockets. This part covers the core API calls in detail.

Stream Socket Lifecycle β€” Server vs Client
SERVER
1️⃣ socket() β€” create socket
2️⃣ bind() β€” assign address
3️⃣ listen() β€” mark as passive
4️⃣ accept() β€” wait for client
5️⃣ read()/write() β€” exchange data
6️⃣ close() β€” done
CLIENT
1️⃣ socket() β€” create socket
2️⃣ connect() β€” reach server
3️⃣ read()/write() β€” exchange data
4️⃣ close() β€” done

Key Terms in This Part

bind() listen() accept() connect() backlog passive socket active socket well-known address socklen_t

bind() β€” Assigning an Address to a Socket

A freshly created socket has no address. bind() gives the socket a name (address) so other processes can find it. For servers, binding to a well-known address is how clients know where to connect.

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* Returns 0 on success, -1 on error */

Parameters:

  • sockfd β€” The socket file descriptor returned by socket()
  • addr β€” Pointer to the address structure (sockaddr_in for IPv4, sockaddr_un for UNIX, etc.)
  • addrlen β€” Size of the address structure in bytes

Example β€” binding a TCP server socket to port 8080 on all interfaces:

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>

int main(void)
{
    int sockfd;
    struct sockaddr_in server_addr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    /* Clear the structure - always do this to avoid garbage */
    memset(&server_addr, 0, sizeof(server_addr));

    server_addr.sin_family      = AF_INET;
    server_addr.sin_port        = htons(8080);  /* port in network byte order */
    server_addr.sin_addr.s_addr = INADDR_ANY;   /* bind to all local interfaces */

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    printf("Socket bound to port 8080\n");
    close(sockfd);
    return 0;
}

Important notes about bind():

  • Ports below 1024 are privileged ports β€” only root (or CAP_NET_BIND_SERVICE capability) can bind to them.
  • If you try to reuse a port that was recently closed, bind() may fail with EADDRINUSE. Use the SO_REUSEADDR socket option (setsockopt) to avoid this during development.
  • For AF_UNIX, the socket path must not already exist. Call unlink() first or before binding.
  • Clients typically do not call bind(). The kernel auto-assigns an ephemeral (temporary) port when they call connect().
/* SO_REUSEADDR - allow reuse of local port even if recently used */
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/* Always call setsockopt BEFORE bind() */

listen() β€” Marking Socket as Passive (Server Only)

After binding, a stream server must call listen(). This marks the socket as a passive socket β€” one that accepts incoming connections instead of actively connecting to a peer.

#include <sys/socket.h>

int listen(int sockfd, int backlog);
/* Returns 0 on success, -1 on error */

The backlog parameter controls the size of the pending connection queue. When a client sends a TCP SYN (connection request), the kernel puts the half-open connection into this queue. The server processes them one by one with accept(). If the queue is full, new connection attempts are refused (or silently dropped depending on the OS).

listen() Backlog Queue
Client A β†’ SYN
Client B β†’ SYN
Client C β†’ SYN
β†’
Pending Queue
backlog = 5
[ C ] [ B ] [ A ]
FIFO order
β†’
Server
accept()
/* Typical listen() usage */
#define BACKLOG 10

if (listen(sockfd, BACKLOG) == -1) {
    perror("listen");
    exit(EXIT_FAILURE);
}
printf("Server listening, backlog = %d\n", BACKLOG);

In modern Linux, the actual backlog is capped by the kernel parameter /proc/sys/net/core/somaxconn (default 128 on many distros). High-traffic servers increase this. The value SOMAXCONN (defined in sys/socket.h) is also a useful symbolic constant.

Datagram sockets do not use listen() β€” they are connectionless and can receive from any sender immediately after bind().

accept() β€” Accepting a Connection (Server Only)

accept() dequeues the next completed connection from the listening socket’s queue. It blocks until a client connects. When a client connects, accept() returns a new file descriptor β€” the connected socket β€” while the original listening socket remains open for more connections.

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/* Returns new socket file descriptor on success, -1 on error */

Parameters:

  • sockfd β€” The listening (passive) socket created by socket() + bind() + listen()
  • addr β€” Buffer filled with the connecting client’s address. Pass NULL if you don’t need it.
  • addrlen β€” Value-result: set it to sizeof(addr structure) before call; contains actual size after call. Pass NULL if addr is NULL.

How accept() Works
Listening Socket
fd=3 (server socket)
port 8080
stays open for more clients
β†’ accept() β†’
New Connected Socket
fd=4 (per-client socket)
connected to client
use for read/write

The listening socket (fd=3) is kept for future clients. The new socket (fd=4) is the actual data channel to this specific client.

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

/* Accepting a client and printing its IP address */
void accept_client(int listen_fd)
{
    int client_fd;
    struct sockaddr_in client_addr;
    socklen_t addrlen = sizeof(client_addr);

    client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &addrlen);
    if (client_fd == -1) {
        perror("accept");
        return;
    }

    /* Convert binary IP to human-readable string */
    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));

    printf("Client connected from %s:%d (fd=%d)\n",
           client_ip, ntohs(client_addr.sin_port), client_fd);

    /* Use client_fd to communicate with this specific client */
    /* ... read()/write() calls ... */

    close(client_fd);  /* done with this client */
}

A key design point: for handling multiple clients, a typical approach is to fork() after accept(), giving each child process its own copy of client_fd. The parent closes client_fd and loops back to accept(). Alternatively, use threads or epoll for non-blocking I/O.

connect() β€” Connecting to a Server (Client Side)

The client calls connect() to initiate a connection to a server’s listening socket. For TCP (SOCK_STREAM), this triggers the three-way handshake. connect() blocks until the connection is established or fails.

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* Returns 0 on success, -1 on error */

Parameters:

  • sockfd β€” Client’s socket file descriptor
  • addr β€” Address of the server to connect to
  • addrlen β€” Size of the address structure
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>

int main(void)
{
    int sockfd;
    struct sockaddr_in server_addr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port   = htons(8080);

    /* Convert string IP to binary */
    if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
        perror("inet_pton");
        exit(EXIT_FAILURE);
    }

    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect");
        exit(EXIT_FAILURE);
    }

    printf("Connected to server on port 8080\n");
    /* Now use read()/write() on sockfd */

    close(sockfd);
    return 0;
}

connect() behaviour for different socket types:

  • SOCK_STREAM β€” Triggers TCP three-way handshake. Blocks until connection succeeds or fails. Error ECONNREFUSED means no server is listening at that address/port.
  • SOCK_DGRAM β€” Does not send any data. Stores the server’s address in the kernel so future write() calls on this socket automatically go to that address. Covered in Part 5.

If connect() fails, the socket is no longer usable. You must close it and create a new socket before retrying.

Network Byte Order β€” htons, ntohs, htonl, ntohl

Different CPU architectures store multi-byte integers differently. x86 is little-endian (least significant byte first). Network protocols use big-endian (most significant byte first) as the standard. You must always convert port numbers and IP addresses when setting up address structures.

Byte Order Conversion
Port 8080 (decimal)
= 0x1F90 (hex)
Little-endian (host): 90 1F
Big-endian (network): 1F 90
htons(8080) → converts host→network
#include <arpa/inet.h>

/* Short (16-bit) conversions β€” used for port numbers */
uint16_t htons(uint16_t host_port);   /* host to network short */
uint16_t ntohs(uint16_t net_port);    /* network to host short */

/* Long (32-bit) conversions β€” used for IPv4 addresses */
uint32_t htonl(uint32_t host_addr);   /* host to network long */
uint32_t ntohl(uint32_t net_addr);    /* network to host long */

/* Example */
server_addr.sin_port = htons(8080);   /* ALWAYS use htons for ports */
printf("Port: %d\n", ntohs(client_addr.sin_port)); /* ALWAYS use ntohs to read ports */

On big-endian machines (like SPARC, some ARMs), these functions are no-ops. On little-endian (x86, most ARM Cortex-M in LE mode), they actually swap bytes. Always use them regardless β€” code must be portable.

Interview Questions β€” bind, listen, accept, connect

Q1. Why does accept() return a new socket instead of reusing the listening socket?

The listening socket must remain open to accept more clients. If accept() reused it, only one client could ever be served. The new connected socket is the actual per-client data channel, while the listening socket continues to queue incoming connections. This design allows a single server to serve many clients simultaneously.

Q2. What is the backlog in listen() and what happens if it overflows?

The backlog is the maximum number of pending (not yet accepted) connections the kernel will queue for the socket. If it overflows, new connection attempts are silently dropped (or refused with RST for TCP). The actual limit is also bounded by the kernel’s somaxconn parameter. High-traffic servers increase somaxconn and use a larger backlog.

Q3. What is EADDRINUSE and how do you fix it?

EADDRINUSE means the address and port combination is already in use. This happens when a server restarts quickly after a crash and the old TCP port is still in TIME_WAIT state. Fix: call setsockopt() with SO_REUSEADDR before bind(). This tells the kernel to reuse the port even if it is in TIME_WAIT.

Q4. What does connect() do for a SOCK_DGRAM socket?

For datagram sockets, connect() does not send any packets. It records the server’s address in the kernel so subsequent write() calls automatically go to that address. It also restricts incoming datagrams to those from that address. This avoids specifying the destination address on every sendto() call.

Q5. What is inet_pton() and why is it preferred over inet_addr()?

inet_pton() (presentation to network) converts a human-readable IP address string (“192.168.1.1”) to binary network byte order. It supports both IPv4 and IPv6. The older inet_addr() only supports IPv4 and uses INADDR_NONE (-1) to signal an error, which is ambiguous since 255.255.255.255 also maps to -1. inet_pton() returns a separate error indicator and is the portable choice.

Q6. Does a client need to call bind() before connect()?

No. If a client does not call bind(), the kernel automatically assigns an ephemeral port (from the local port range, typically 32768–60999 on Linux). The kernel also selects the correct source IP based on routing. Clients only call bind() when they need a specific source port, which is uncommon.

Q7. What error do you get when connect() fails because no server is listening?

ECONNREFUSED. The remote host received the TCP SYN but no process was listening on that port, so it replied with RST. This is different from ETIMEDOUT (connection request timed out, usually because the host is unreachable) and EHOSTUNREACH (no route to the host).

Next: Part 3 β€” Stream Socket Client-Server with Full Code

See a complete working TCP echo server and client implementation.

Leave a Reply

Your email address will not be published. Required fields are marked *