The Socket API socket(), bind(), listen(), connect(), accept()

 

Chapter 58 โ€“ The Socket API
Part 7 of 8 ย |ย  socket(), bind(), listen(), connect(), accept()
๐Ÿ”Œ socket()
๐Ÿ“Œ bind()
๐Ÿ‘‚ listen()
๐Ÿค accept()

The Socket โ€” Your Handle to the Network

A socket is a file descriptor that represents a communication endpoint. Just like open() returns a file descriptor for a file, socket() returns a file descriptor for a network connection. You can use read()/write() or the socket-specific send()/recv() on it.

The socket API is designed around a clear separation between server (waits for connections) and client (initiates connections). Each role calls a different sequence of functions.

Key Terms:

socket() bind() listen() accept() connect() send() / recv() close() backlog SO_REUSEADDR AF_INET SOCK_STREAM

๐Ÿ“Š Server vs Client โ€” The API Call Sequence

SERVER (TCP)
1๏ธโƒฃ socket()
Create socket fd
2๏ธโƒฃ setsockopt(SO_REUSEADDR)
Avoid TIME_WAIT bind error
3๏ธโƒฃ bind()
Assign IP + port
4๏ธโƒฃ listen()
Mark as passive (accepting)
5๏ธโƒฃ accept() โ† blocks
Wait for client, get new fd
6๏ธโƒฃ send() / recv()
Communicate on new fd
7๏ธโƒฃ close()
Close connection fd

โ† connect โ†’
โ† data โ†’
โ† FIN/ACK โ†’

CLIENT (TCP)
1๏ธโƒฃ socket()
Create socket fd
(no bind/listen needed for client)
2๏ธโƒฃ connect()
3-way handshake happens here
3๏ธโƒฃ send() / recv()
Communicate
4๏ธโƒฃ close()
Send FIN

๐Ÿ”Œ socket() โ€” Creating a Socket
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
/* Returns: socket fd on success, -1 on error */
Parameter Common Values Meaning
domain AF_INET, AF_INET6, AF_UNIX Address family (IPv4, IPv6, local)
type SOCK_STREAM, SOCK_DGRAM, SOCK_RAW TCP, UDP, Raw IP
protocol 0 (auto) Usually 0 โ€” kernel picks based on domain+type
/* Examples: */
int tcp4_fd = socket(AF_INET,  SOCK_STREAM, 0);  /* TCP/IPv4 */
int udp4_fd = socket(AF_INET,  SOCK_DGRAM,  0);  /* UDP/IPv4 */
int tcp6_fd = socket(AF_INET6, SOCK_STREAM, 0);  /* TCP/IPv6 */
int udp6_fd = socket(AF_INET6, SOCK_DGRAM,  0);  /* UDP/IPv6 */
int raw_fd  = socket(AF_INET,  SOCK_RAW, IPPROTO_ICMP); /* Raw ICMP */

if (tcp4_fd < 0) { perror("socket"); exit(1); }

๐Ÿ“Œ bind() โ€” Assigning an Address to a Socket

bind() attaches a local address (IP + port) to the socket. Servers call bind() so clients know which port to connect to. Clients usually skip bind() โ€” the OS automatically assigns an ephemeral port.

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* Returns: 0 on success, -1 on error */
/* Server: bind to port 8080 on all interfaces */
struct sockaddr_in srv_addr;
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family      = AF_INET;
srv_addr.sin_port        = htons(8080);      /* MUST use htons! */
srv_addr.sin_addr.s_addr = INADDR_ANY;       /* 0.0.0.0 = all interfaces */

if (bind(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) {
    perror("bind");
    exit(1);
}
/* Common errors:
   EADDRINUSE  - port already in use (maybe TIME_WAIT)
   EACCES      - port < 1024 and not root */

โš™๏ธ SO_REUSEADDR โ€” Essential Socket Option

When a server closes, its port may stay in TIME_WAIT for ~60 seconds. If you restart the server immediately, bind() fails with EADDRINUSE. Fix: set SO_REUSEADDR before bind. Every TCP server should do this.

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

/* Set SO_REUSEADDR โ€” ALWAYS do this for TCP servers! */
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
    perror("setsockopt SO_REUSEADDR");
    exit(1);
}

/* Now bind will succeed even if the port is in TIME_WAIT */
bind(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));

๐Ÿ‘‚ listen() โ€” Marking Socket as Passive

listen() tells the kernel: “this socket will accept incoming connections.” It creates a queue for incoming connection requests and is only for TCP servers.

#include <sys/socket.h>

int listen(int sockfd, int backlog);
/* Returns: 0 on success, -1 on error
   backlog = max length of pending connection queue */
The backlog queue โ€” two queues in Linux:

1. SYN queue (incomplete): Holds connections that have received SYN but not yet completed the 3-way handshake. Limited by /proc/sys/net/ipv4/tcp_max_syn_backlog.

2. Accept queue (complete): Holds fully established connections waiting for your accept() call. Limited by the backlog argument to listen().

If the accept queue is full, new connections may be silently dropped or RST’d. Under high load, increase backlog and call accept() faster.

/* Common backlog values: */
listen(sockfd, 5);       /* Small server โ€” 5 pending connections */
listen(sockfd, 128);     /* Typical server */
listen(sockfd, SOMAXCONN); /* Let OS pick maximum (usually 128 or 4096) */

๐Ÿค accept() โ€” Accepting an Incoming Connection

accept() blocks until a client connects. It returns a new socket fd for that specific connection. The original (listening) socket is still open and ready to accept more.

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/* Returns: new connected socket fd on success, -1 on error
   addr     = filled with client's address (IP + port) โ€” can be NULL
   addrlen  = on input: size of addr buffer; on output: actual size used */
/* Accept loop โ€” handles clients one after another (sequential): */
for (;;) {
    struct sockaddr_in cli_addr;
    socklen_t cli_len = sizeof(cli_addr);

    /* Blocks here until client connects: */
    int connfd = accept(sockfd, (struct sockaddr *)&cli_addr, &cli_len);
    if (connfd < 0) { perror("accept"); continue; }

    /* Get client info: */
    char cli_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli_addr.sin_addr, cli_ip, sizeof(cli_ip));
    printf("Client connected from %s:%u\n", cli_ip, ntohs(cli_addr.sin_port));

    /* Communicate on connfd โ€” NOT on sockfd! */
    char buf[1024];
    ssize_t n = recv(connfd, buf, sizeof(buf) - 1, 0);
    if (n > 0) {
        buf[n] = '\0';
        printf("Received: %s\n", buf);
        send(connfd, "OK\n", 3, 0);
    }

    close(connfd);  /* Close this client's connection */
    /* sockfd (listening socket) is still open for next client */
}
๐Ÿ”‘ Key insight: Two sockets are in play:
โ€ข Listening socket (sockfd) โ€” stays open, accepts new connections
โ€ข Connected socket (connfd) โ€” one per client, used to communicate
Real servers use fork(), threads, or I/O multiplexing (select/poll/epoll) to handle multiple clients simultaneously.

๐Ÿ“ž connect() โ€” Client Initiates Connection

The client calls connect() to initiate the TCP 3-way handshake with the server. It 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 */
/* Complete TCP client example: */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(void) {
    /* 1. Create socket */
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) { perror("socket"); return 1; }

    /* 2. Set server address */
    struct sockaddr_in srv_addr;
    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port   = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &srv_addr.sin_addr);

    /* 3. Connect โ€” 3-way handshake happens here */
    if (connect(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) {
        perror("connect");
        /* Common errors:
           ECONNREFUSED - no server listening on that port
           ETIMEDOUT    - server not reachable / firewall
           ENETUNREACH  - no route to host */
        return 1;
    }
    printf("Connected to server!\n");

    /* 4. Send data */
    const char *msg = "Hello, Server!";
    send(sockfd, msg, strlen(msg), 0);

    /* 5. Receive response */
    char buf[1024];
    ssize_t n = recv(sockfd, buf, sizeof(buf) - 1, 0);
    if (n > 0) {
        buf[n] = '\0';
        printf("Server replied: %s\n", buf);
    }

    /* 6. Close */
    close(sockfd);
    return 0;
}

๐Ÿ’ป Complete TCP Echo Server (All Pieces Together)
/* tcp_echo_server.c โ€” Complete production-quality template */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT    8080
#define BACKLOG 128
#define BUFSIZE 4096

int main(void) {
    int listenfd, connfd;
    struct sockaddr_in srv_addr, cli_addr;
    socklen_t cli_len;
    char buf[BUFSIZE];
    ssize_t n;

    /* Step 1: Create socket */
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) { perror("socket"); exit(1); }

    /* Step 2: SO_REUSEADDR โ€” avoid "Address already in use" on restart */
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    /* Step 3: Bind */
    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family      = AF_INET;
    srv_addr.sin_port        = htons(PORT);
    srv_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(listenfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) {
        perror("bind"); exit(1);
    }

    /* Step 4: Listen */
    if (listen(listenfd, BACKLOG) < 0) { perror("listen"); exit(1); }
    printf("Echo server listening on port %d...\n", PORT);

    /* Step 5: Accept loop */
    for (;;) {
        cli_len = sizeof(cli_addr);
        connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &cli_len);
        if (connfd < 0) { perror("accept"); continue; }

        /* Print client info */
        char cli_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cli_addr.sin_addr, cli_ip, sizeof(cli_ip));
        printf("[+] Client: %s:%u\n", cli_ip, ntohs(cli_addr.sin_port));

        /* Step 6: Echo loop (handle partial recv) */
        while ((n = recv(connfd, buf, sizeof(buf), 0)) > 0) {
            ssize_t sent = 0;
            while (sent < n) {  /* loop because send may not send all */
                ssize_t s = send(connfd, buf + sent, n - sent, 0);
                if (s <= 0) break;
                sent += s;
            }
        }

        printf("[-] Client disconnected: %s\n", cli_ip);
        close(connfd);  /* close connected socket, NOT listenfd */
    }

    close(listenfd);
    return 0;
}
/* Build and test:
   gcc -o echo_server tcp_echo_server.c && ./echo_server &
   echo "Hello" | nc 127.0.0.1 8080
   # Output: Hello
*/

๐ŸŽฏ Interview Questions โ€” Socket API
Q1: What is the difference between the listening socket and the connected socket returned by accept()?

A: The listening socket (created by socket() + bind() + listen()) is passive โ€” it just waits for connections. When accept() is called, it creates a new socket fd specifically for that client’s connection. The listening socket stays open to accept more clients. The connected socket is what you use to communicate with that specific client. Closing the connected socket doesn’t affect the listening socket.

Q2: Why does connect() sometimes return ECONNREFUSED?

A: ECONNREFUSED means the target machine is reachable but no process is listening on that port. The server’s kernel sends back a TCP RST (reset) packet in response to the SYN. Common causes: server not started, server crashed, listening on a different port, or firewall that sends RST. Note: if firewall silently drops packets (no RST), you get ETIMEDOUT instead.

Q3: What is the backlog parameter in listen() and what happens when it’s full?

A: Backlog limits the number of fully established connections that can be queued waiting for accept(). If the queue fills up (server not calling accept() fast enough), new incoming connections may be silently dropped or RST’d depending on the kernel setting. The client sees the connection as timing out or being refused. Solution: larger backlog (use SOMAXCONN), call accept() faster, or use multiple threads/processes.

Q4: Can recv() return fewer bytes than you asked for? How do you handle it?

A: Yes, definitely. TCP is a byte stream โ€” recv() may return anywhere from 1 byte to the amount requested, depending on what’s in the kernel’s receive buffer. This is normal. Always loop on recv() until you’ve received all bytes expected. Use a length-prefix protocol so you know how many bytes to expect. Check for: recv() returns 0 = connection closed by peer. recv() returns -1 = error.

Q5: Does a client need to call bind() before connect()?

A: No. If a client doesn’t call bind(), the OS automatically assigns an ephemeral (temporary) port from the dynamic port range (typically 32768โ€“60999 on Linux) and uses the machine’s outgoing IP. You only call bind() on the client if you specifically need the connection to come from a particular local port or IP address (rare โ€” used in some NAT traversal scenarios).

Q6: What is SO_REUSEADDR and when do you need it?

A: SO_REUSEADDR allows a socket to bind to a local address even if a socket from a previous connection is still in TIME_WAIT on that port. Without it, restarting a TCP server quickly after shutdown fails with EADDRINUSE because the port is still in TIME_WAIT. Set it with setsockopt() before bind(). Every TCP server should use this option.

Leave a Reply

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