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.
socket()Create socket fd
setsockopt(SO_REUSEADDR)Avoid TIME_WAIT bind error
bind()Assign IP + port
listen()Mark as passive (accepting)
accept() โ blocksWait for client, get new fd
send() / recv()Communicate on new fd
close()Close connection fd
socket()Create socket fd
connect()3-way handshake happens here
send() / recv()Communicate
close()Send FIN
#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() 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 */
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() 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 */
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() 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 */
}
โข 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.
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;
}
/* 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
*/
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.
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.
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.
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.
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).
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.
