In short
A socket is the programming interface (API) through which an application hands data to the transport layer and receives it back — the end point identified by the pair (IP address, port number), while a connection is identified by the five-tuple (protocol, local IP, local port, remote IP, remote port). The Berkeley/POSIX socket API (socket, bind, listen, accept, connect, send, recv, close) is the de-facto standard, wrapped by Python's socket, Java's Socket/ServerSocket and Windows Winsock. It matters because every network application a student will ever write or debug — web server, chat app, microservice, game server, IoT device — sits on it. Examined in computer-networks labs (every AICTE/CBCS CSE curriculum has a socket-programming lab), in GATE CS as concept questions on the call sequence, concurrency and the five-tuple, and asked constantly in placement interviews at TCS, Infosys, Wipro, Cognizant and in systems rounds at Amazon, Flipkart, Zoho, Juspay, PhonePe and Zscaler.
Explained
What a socket is, and the types
- Socket — an abstraction (a file descriptor on Unix) representing one end of a communication channel; created by
socket(domain, type, protocol). - Domain / address family:
AF_INET(IPv4),AF_INET6(IPv6),AF_UNIX/AF_LOCAL(same-machine IPC via a filesystem path),AF_PACKET(link-layer frames, Linux). - Type:
SOCK_STREAM— TCP: connection-oriented, reliable, ordered, byte stream, no message boundaries.SOCK_DGRAM— UDP: connectionless, unreliable, unordered, preserves message boundaries (onesendto= one datagram).SOCK_RAW— direct access to IP (or ICMP); used byping,traceroute, packet sniffers; needs root/Administrator privilege.SOCK_SEQPACKET— reliable, connection-oriented and message-preserving (SCTP, AF_UNIX).
- Protocol — usually 0, meaning the default for that family+type (IPPROTO_TCP for stream, IPPROTO_UDP for datagram).
- On Unix a socket descriptor can be used with
read/write,select,fcntl,dup, and is inherited acrossfork— "everything is a file". - Descriptor vs port: the descriptor is a process-local integer; the port is the 16-bit network-visible number. They are unrelated numbers.
Addresses, ports and byte order
- Address structure:
struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; }—sockaddr_in6for IPv6, genericsockaddrused in call prototypes (hence the casts in C). - Port ranges (IANA, followed by Linux/Windows): 0–1023 well-known (needs privilege on Unix: 80 HTTP, 443 HTTPS, 22 SSH, 53 DNS, 25 SMTP), 1024–49151 registered (3306 MySQL, 5432 PostgreSQL, 6379 Redis, 8080 Tomcat), 49152–65535 dynamic/ephemeral. Linux's actual ephemeral range is in
/proc/sys/net/ipv4/ip_local_port_range(commonly 32768–60999). - Byte order: network byte order is big-endian; x86/ARM are little-endian, so convert:
htons/htonl(host→network, short/long) andntohs/ntohlon the way back. Ports and raw IPv4 addresses must be converted; the payload is not converted automatically — you must define your own wire format. - Address conversion:
inet_pton/inet_ntop(IPv4 and IPv6, preferred) replace the olderinet_addr/inet_ntoa. - Name resolution:
getaddrinfo()is the modern, protocol-independent call (returns a linked list of candidate addresses, works for IPv4 and IPv6, replacesgethostbyname);getnameinfo()does the reverse. Python:socket.getaddrinfo,gethostbyname. - Wildcard address
INADDR_ANY(0.0.0.0, written as""in Python) binds every local interface;127.0.0.1binds loopback only, so the server is unreachable from other machines.
The TCP call sequence, step by step
Server:
socket()→ descriptor in CLOSED state.setsockopt(SO_REUSEADDR)→ lets you rebind a port still in TIME_WAIT (otherwise restart fails with "Address already in use").bind(fd, addr, len)→ attaches a local IP+port. Without it the kernel picks an ephemeral port, which clients cannot guess.listen(fd, backlog)→ marks it passive, creates the queue of completed/pending connections; socket goes to LISTEN.backlogbounds the accept queue (Linux also caps it bysomaxconn).accept(fd, &peeraddr, &len)→ blocks until a connection is complete, then returns a new connected descriptor; the listening descriptor stays open for more clients.recv/send(orread/write) on the *new* descriptor, in a loop.close()the connected descriptor per client; close the listener on shutdown.
Client:
socket().- (optional
bind) — normally skipped; the kernel assigns source IP and an ephemeral port. connect(fd, servaddr, len)→ triggers the TCP three-way handshake; returns when it completes (or fails withECONNREFUSEDif nothing listens,ETIMEDOUT/EHOSTUNREACHif unreachable).send/recvloop, thenclose().
- Where the handshake really happens: the kernel completes SYN → SYN-ACK → ACK;
accept()only *hands over* an already-established connection. So a connection can exist before the server ever callsaccept. - **
close()vsshutdown():**closereleases the descriptor;shutdown(fd, SHUT_WR)sends FIN but keeps reading — the correct way to signal "no more data from me" (half-close) while still receiving the reply. - TIME_WAIT (2×MSL, typically 60 s on Linux) is held by the side that closes first, so that stray duplicates die out; this is why a just-killed server may refuse to rebind without
SO_REUSEADDR.
The UDP sequence, and connected UDP
- Server:
socket(AF_INET, SOCK_DGRAM)→bind()→recvfrom()/sendto()loop. **Nolisten, noaccept** — there is no connection to accept. - Client:
socket()→sendto(data, servaddr)→recvfrom(). Noconnectneeded. connect()on a UDP socket does not send any packet; it merely records the peer so you can usesend/recv, and lets the kernel report ICMP port-unreachable errors back to you (otherwise they are silently dropped).- A UDP
recvfromreturns exactly one datagram; if your buffer is smaller, the rest is discarded. TCPrecvreturns *whatever bytes are available* — possibly fewer than asked. - UDP datagram payload limit: 65,535 − 8 (UDP header) − 20 (IPv4 header) = 65,507 bytes; in practice keep under the path MTU (1,472 bytes payload on 1,500-byte Ethernet) to avoid IP fragmentation.
Concurrency models for servers
- Iterative server: one
accept→ serve fully → next client. Simple, fine for short request/response (classic daytime/echo), but one slow client blocks everyone. Typical for UDP servers. - **Concurrent, process-per-client (
fork):** parentaccepts and forks; child closes the listener and serves; parent closes the connected fd and reaps children withwaitpid/SIGCHLD to avoid zombies. Robust (crash isolation) but heavy. - Thread-per-client: cheaper than fork, shared memory needs locking; a thread pool bounds resource use. Java's
ExecutorService+ServerSocket, Pythonthreadingorsocketserver.ThreadingTCPServer. - I/O multiplexing (single thread, many sockets):
select()— portable but O(n) scan and limited byFD_SETSIZE(1024);poll()— no fixed limit;epoll()(Linux),kqueue(BSD/macOS), IOCP (Windows) — O(1) event notification, the basis of nginx, Redis, Node.js and Pythonasyncio/selectors. - C10K problem: serving ten thousand-plus concurrent connections needs event-driven I/O, not a thread per connection — the standard interview framing of why
epollexists. - Blocking vs non-blocking:
O_NONBLOCK/fcntlorsock.setblocking(False)makes calls returnEWOULDBLOCK/EAGAINinstead of waiting;connectthen completes asynchronously (check writability withselect, thengetsockopt(SO_ERROR)). - Timeouts:
settimeout()in Python,SO_RCVTIMEO/SO_SNDTIMEO, orselectwith a timeout — never rely on a blocking call returning on its own.
Socket options, buffers and framing
setsockopt/getsockoptat levelsSOL_SOCKET,IPPROTO_TCP,IPPROTO_IP:SO_REUSEADDR— rebind in TIME_WAIT;SO_REUSEPORT(Linux) — several processes bind the same port and the kernel load-balances.SO_KEEPALIVE— probe idle connections to detect dead peers.SO_RCVBUF/SO_SNDBUF— kernel buffer sizes (affect throughput via the bandwidth-delay product).SO_LINGER— behaviour ofclosewhen data is unsent.TCP_NODELAY— disable Nagle's algorithm (which coalesces small writes); needed for latency-sensitive, small-message traffic.IP_ADD_MEMBERSHIP,IP_MULTICAST_TTL,SO_BROADCASTfor multicast/broadcast UDP.
- Message framing is the application's job on TCP, because TCP is a byte stream: use a fixed-length header carrying the length (
struct.pack('!I', n)), or a delimiter (\r\nin HTTP/SMTP), or a self-describing format. Assuming onesend= onerecvis the single most common bug. - Partial writes:
sendmay accept fewer bytes than given; loop on the return value (sendallin Python does this for you).recvreturning 0 means the peer performed an orderly close (EOF);-1witherrnomeans error. - Security: plain sockets are cleartext. Wrap with TLS (
ssl.SSLContext.wrap_socketin Python — the old module-levelssl.wrap_socketwas removed in 3.12 — JavaSSLSocket, OpenSSL in C) for confidentiality and server authentication. In India, "reasonable security practices" under Section 43A of the IT Act, 2000 and the DPDP Act, 2023 (passed 2023; rules were still being notified in phases as of this writing) make encryption of personal data in transit a compliance matter, not just good practice. CERT-In's 2022 directions also require accurate logs and time sync (NTP) for service providers.
Key points
- Socket — one end point of communication = (IP address, port); on Unix it is a file descriptor.
- Five-tuple — protocol + local IP + local port + remote IP + remote port uniquely identifies a TCP connection; two clients can share a server port because their tuples differ.
- socket → bind → listen → accept — the server sequence for TCP; the client needs only socket → connect.
- **
acceptreturns a new descriptor** — the listening socket keeps listening; serving happens on the returned one. - **No
listen/acceptin UDP** —bindthenrecvfrom/sendto;connecton UDP is local-only, sends nothing. - **
SOCK_STREAMhas no message boundaries** — the application must frame (length prefix or delimiter);SOCK_DGRAMpreserves them. - **
recvreturning 0** — orderly shutdown by the peer (EOF), not an error, and not "no data yet". - Network byte order is big-endian — convert ports and addresses with
htons/htonl,ntohs/ntohl. - **
INADDR_ANY(0.0.0.0)** — bind all interfaces; binding 127.0.0.1 makes the service loopback-only. - Ephemeral port — kernel-assigned client source port from the dynamic range (commonly 32768–60999 on Linux).
- **
backlog** inlisten— size of the queue of connections waiting to be accepted, not the number of clients a server can serve. - **
SO_REUSEADDR** — cures "Address already in use" caused by a previous socket in TIME_WAIT. - TIME_WAIT (2×MSL) — held by the active closer so delayed duplicates expire before the tuple is reused.
- **
shutdownvsclose** —shutdownsignals FIN in one direction (half-close);closefrees the descriptor. - **
select/poll/epoll** — monitor many sockets in one thread;epollscales to the C10K problem,selectis capped at FD_SETSIZE 1024. - Nagle's algorithm — coalesces small TCP writes; disable with
TCP_NODELAYfor interactive traffic. - Raw sockets — bypass TCP/UDP to craft IP/ICMP packets; require privilege (ping, traceroute, sniffers).
- Well-known ports below 1024 need root to bind on Unix-like systems; Windows imposes no such restriction.
Formulas and examples
SO_RCVBUF ≥ BDP.Example 1 — Ports used by three clients on one web server. A server listens on 203.0.113.10:443. Clients A (198.51.100.5) and B (198.51.100.6) connect once each; A opens a second connection.
- Listening socket: (TCP, 203.0.113.10, 443, *, *) — one socket, still in LISTEN.
- Connection 1: (TCP, 203.0.113.10, 443, 198.51.100.5, 51000).
- Connection 2: (TCP, 203.0.113.10, 443, 198.51.100.6, 51000) — same client port is fine; the remote IP differs.
- Connection 3: (TCP, 203.0.113.10, 443, 198.51.100.5, 51001) — same client, so the port must differ.
- Total descriptors open on the server = 1 listening + 3 connected = 4, all on port 443. Answer: a server needs only one port for all clients.
Example 2 — BDP and buffer sizing. A link from Bengaluru to Mumbai has 200 Mbps capacity and RTT 30 ms.
- BDP = 200 × 10⁶ × 0.030 = 6 × 10⁶ bits = 6,000,000/8 = 750,000 bytes ≈ 732 KiB.
- A default
SO_RCVBUFof 64 KiB (65,536 B) caps throughput at 65,536 × 8 / 0.030 = 17.47 × 10⁶ bits/s ≈ 17.5 Mbps — about 8.7% of the link. So raiseSO_RCVBUF/SO_SNDBUF(or rely on Linux auto-tuning) to use the pipe.
Example 3 — Framing bug made concrete. A client sends "HELLO" (5 B) then "WORLD" (5 B) with two send calls over TCP; the server calls recv(1024) once.
- TCP may deliver 10 bytes in one read ("HELLOWORLD"), or 5+5, or 3+7 — all are legal.
- With a 4-byte length prefix: wire bytes =
00 00 00 05 H E L L O 00 00 00 05 W O R L D= 2 × (4 + 5) = 18 bytes; overhead = 8/18 ≈ 44% for such tiny messages, which is why small records are usually batched. - Receiver algorithm: read exactly 4 bytes → n =
unpack('!I')→ read exactly n bytes → repeat.
**Example 4 — File transfer time and number of send calls.** Transfer a 10 MB file (10 × 1,048,576 = 10,485,760 B) over a 100 Mbps path, application buffer 4,096 B.
send/recviterations ≈ 10,485,760 / 4,096 = 2,560.- Pure transmission time = 10,485,760 × 8 / 100 × 10⁶ = 83,886,080/10⁸ = 0.839 s, plus the handshake (~1 RTT) and TCP slow-start, so measured time is higher.
- Segments at MSS 1,460 B ≈ 10,485,760/1,460 ≈ 7,182 segments.
**Example 5 — select versus thread-per-client at 5,000 connections.** Each thread has a 1 MB stack (Linux default 8 MB virtual, 1 MB commonly configured).
- Thread model: 5,000 × 1 MB = 5 GB of stack address space plus context-switch cost.
selectcannot be used at all here: FD_SETSIZE is 1024 < 5,000.pollworks but scans 5,000 entries per call;epollreturns only the ready ones — hence **epollis the correct choice**.
# TCP echo server (iterative) and client - Python 3
import socket, struct
def server(host="", port=9000): # "" = INADDR_ANY
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port)); s.listen(5)
while True:
conn, addr = s.accept() # new descriptor per client
with conn:
while True:
data = conn.recv(1024)
if not data: # 0 bytes = peer closed (EOF)
break
conn.sendall(data) # sendall loops over partial writes
def client(host="127.0.0.1", port=9000, msg=b"HELLO"):
with socket.create_connection((host, port), timeout=5) as c:
c.sendall(struct.pack("!I", len(msg)) + msg) # 4-byte length prefix
n = struct.unpack("!I", recv_exact(c, 4))[0]
return recv_exact(c, n)
def recv_exact(sock, n): # the correct way to read n bytes
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("peer closed early")
buf += chunk
return buf
# UDP server and client - no listen(), no accept()
srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
srv.bind(("", 9001))
data, peer = srv.recvfrom(65535) # one datagram, with sender address
srv.sendto(data.upper(), peer)
| Aspect | TCP socket (SOCK_STREAM) | UDP socket (SOCK_DGRAM) |
|---|---|---|
| Calls used | socket, bind, listen, accept, connect, send, recv | socket, bind, sendto, recvfrom |
| Connection | Three-way handshake via connect/accept | None |
| Boundaries | Byte stream - app must frame | One send = one datagram |
| Reliability | Retransmission, ordering, flow and congestion control | None (app must handle loss) |
| Descriptor per client | Yes (from accept) | No - one socket serves all peers |
| Typical server model | Concurrent (fork/thread/epoll) | Iterative |
| Used by | HTTP, SSH, SMTP, FTP, database drivers | DNS, DHCP, SNMP, VoIP, games, QUIC base |
| Call | Side | Blocks? | Returns |
|---|---|---|---|
| socket | both | no | descriptor |
| bind | mainly server | no | 0 or error |
| listen | server | no | 0; socket becomes passive |
| accept | server | yes (unless non-blocking) | new connected descriptor + peer address |
| connect | client | yes for TCP (handshake); instant for UDP | 0 or error |
| recv | both | yes until data/EOF | byte count; 0 = peer closed |
| send | both | yes if send buffer full | bytes actually queued |
| close / shutdown | both | no | 0 or error |
Where it is used
- Web and app servers: nginx and Redis are epoll-based single-threaded loops; Tomcat and Spring Boot use Java NIO; every Indian product company's backend sits on these.
- Payments and trading, where latency is money: UPI switches, broker order gateways and exchange feeds (NSE/BSE market data over multicast UDP, orders over TCP with
TCP_NODELAY) at Juspay, Zerodha, Upstox, PhonePe, Razorpay. - Telecom and networking firms: Jio, Airtel, Tejas Networks, Cisco/Juniper India, HCLTech and Wipro network-software teams write raw-socket and packet-processing code for probes, DPI and protocol testing.
- Embedded and IoT: ESP32/Arduino Wi-Fi modules use a cut-down socket API (lwIP) to push telemetry over MQTT (TCP 1883/8883) or CoAP (UDP 5683) — smart meters, EV chargers, agri sensors.
- Job roles: backend/systems engineer, SRE and network engineer (debugging with
netstat -an,ss -tulpen,lsof -i,tcpdump, Wireshark), security analyst writing scanners and honeypots, and QA writing load generators. - Academic labs and exams: the computer-networks lab in most Indian universities marks chat, file-transfer, echo, DNS-lookup and concurrent-server programs written directly on sockets in C, Java or Python.
Interview questions
- Q: What exactly is a socket? A: An endpoint abstraction — a file descriptor bound to (IP, port) — through which an application reads and writes data via the transport layer; it is an API object, not a protocol.
- Q: Why can one server port serve thousands of clients? A: Because a connection is identified by the full five-tuple; each client's IP and ephemeral port differ, so the kernel demultiplexes to a different connected socket while the port stays 443.
- Q: What does
listen(fd, 5)mean — only five clients? A: No. It makes the socket passive and sets the accept-queue length to roughly five pending connections; the number of clients served over time is unlimited. - Q: Why does
accept()return a new descriptor? A: So the listening socket can keep accepting while the returned descriptor carries that one established connection's data. - Q: Does the three-way handshake happen inside
accept()? A: No — the kernel completes it and queues the connection;acceptjust dequeues an already-established connection, which is why a connection can exist beforeacceptis called. - Q: What does
recv()returning 0 mean? A: The peer closed its side in an orderly way (EOF). A blocked socket with no data yet simply does not return; an error returns −1 witherrno. - Q: Why do I sometimes receive two messages joined together over TCP? A: TCP is a byte stream with no message boundaries, so sends can be coalesced (Nagle) or split; the application must frame with a length prefix or delimiter.
- Q: What is the difference between
close()andshutdown()? A:closereleases the descriptor entirely;shutdown(SHUT_WR)sends FIN to say "no more data from me" while still allowing reads — a half-close. - Q: Why does restarting my server give "Address already in use", and how do you fix it? A: The old socket is in TIME_WAIT (2×MSL) holding the port; set
SO_REUSEADDRbeforebind(SO_REUSEPORTif several processes must share the port). - Q: Can you call
connect()on a UDP socket, and what does it do? A: Yes; it sends nothing on the wire — it records the default peer sosend/recvwork and lets ICMP port-unreachable errors be reported to the application. - Q:
selectvspollvsepoll? A:selectis portable but O(n) and capped at FD_SETSIZE 1024;pollremoves the cap but still scans all descriptors;epoll(Linux) is event-registered and O(1) in ready descriptors, so it scales to the C10K problem. - Q: Why do
htons/htonlexist and when can you skip them? A: Because the network uses big-endian while x86/ARM hosts are little-endian; they are needed for port numbers and binary address/length fields, and are unnecessary for single bytes or text payloads. - Q: How would you handle a slow client in a single-threaded
epollserver? A: Keep per-connection state, use non-blocking sockets, buffer unsent output and register for writability instead of blocking insend, with a timeout to drop stalled peers.
Common mistakes
- Treating one
sendas onerecv— no length prefix or delimiter, so messages merge or split under load; and ignoringsend's return value (partial writes). - Writing the server loop as
accept→ serve on the listening descriptor instead of the oneacceptreturned, or forgetting to close connected descriptors (descriptor leak, thenEMFILE). - Calling
listen/accepton a UDP socket, or expectingconnecton UDP to perform a handshake. - Omitting
htons(port)insin_port, leavingsockaddr_inun-zeroed, or passing the wrongaddrlenin C — the server then listens on a byte-swapped port nobody can reach. - Binding
127.0.0.1(or forgettingSO_REUSEADDR) and concluding the network or firewall is broken when the service is simply loopback-only or the port is in TIME_WAIT. - Treating
recvreturning 0 as an error, or looping onEAGAIN/EWOULDBLOCKfrom a non-blocking socket as if it were a failure. - In
fork-based servers, not reaping children (zombies) and not closing the unused descriptor in each process, so FINs are never seen.
Related topics
TCP, UDP, HTTP and DNS · OSI and TCP/IP models · IP addressing and subnetting · Operating systems: processes, threads and I/O multiplexing · Network security: firewalls, VPN and IDS · Distributed systems and RPC/REST APIs