What is a Socket?
A socket is a communication endpoint. Think of it like a telephone โ you need two ends to have a conversation. Sockets allow two programs (called processes) to talk to each other, whether they are running on the same machine or on different machines connected over a network.
Sockets were first introduced in BSD Unix in the early 1980s. Today they are the standard way to do network programming on Linux and all Unix-like systems.
The key idea: a socket gives you a file descriptor, just like opening a file. Once you have that descriptor, you can read and write data through it โ the OS handles the network complexity underneath.
Every socket lives inside a communication domain. The domain decides two things:
- How far the communication can reach (same machine or across a network)
- What format is used to name (address) the socket
Address = file path
/tmp/mysocket
Address = IP + port
192.168.1.1:8080
Address = IPv6 + port
fe80::1:8080
AF_UNIX (also written as AF_LOCAL) is for processes on the same Linux machine. It uses a file path as the address. This is fast because data never leaves the machine โ the kernel copies it directly.
AF_INET is for IPv4 networking โ the classic internet protocol used since the 1980s. You identify a socket with an IP address and a port number.
AF_INET6 is for IPv6 โ the modern successor to IPv4 with larger address space. Same concept as AF_INET but with 128-bit addresses.
In embedded and BLE systems you will mostly see AF_UNIX (for IPC between processes on device) and AF_INET/AF_INET6 (for network communication).
Once you pick a domain, you also pick a socket type. The type tells the OS what kind of communication behaviour you want.
- Connection-oriented
- Reliable delivery (no data loss)
- Data arrives in order
- Bidirectional byte stream
- No message boundaries
- Like a phone call
- Connectionless
- Unreliable (may lose data)
- Order not guaranteed
- Message-oriented
- Each send = one message
- Like sending a letter
SOCK_STREAM โ You connect once, then stream data back and forth. The OS guarantees all bytes arrive and in the right order. Used for HTTP, SSH, FTP, and most reliable network applications. Under IPv4/IPv6 this maps to TCP.
SOCK_DGRAM โ You send self-contained messages (datagrams) without establishing a connection first. Each message is independent. Some may be lost or arrive out of order. Used for DNS, DHCP, streaming video. Under IPv4/IPv6 this maps to UDP.
There is also SOCK_RAW for raw packet access (used for ping, traceroute, packet sniffers) but it requires root privilege and is outside normal application use.
The first step in any socket program is calling socket() to create a socket. It returns a file descriptor โ a small integer you use for all future operations on this socket.
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/* Returns file descriptor on success, -1 on error */
Parameters:
- domain โ The communication domain: AF_UNIX, AF_INET, AF_INET6
- type โ Socket type: SOCK_STREAM or SOCK_DGRAM
- protocol โ Usually 0 (let the OS choose the right protocol for the domain+type combination)
Practical example โ creating different socket types:
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
int main(void)
{
int tcp_sock, udp_sock, unix_sock;
/* TCP socket over IPv4 */
tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_sock == -1) {
perror("socket TCP");
exit(EXIT_FAILURE);
}
printf("TCP socket fd = %d\n", tcp_sock);
/* UDP socket over IPv4 */
udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_sock == -1) {
perror("socket UDP");
exit(EXIT_FAILURE);
}
printf("UDP socket fd = %d\n", udp_sock);
/* UNIX domain stream socket (same machine IPC) */
unix_sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (unix_sock == -1) {
perror("socket UNIX");
exit(EXIT_FAILURE);
}
printf("UNIX socket fd = %d\n", unix_sock);
/* Close all sockets when done */
close(tcp_sock);
close(udp_sock);
close(unix_sock);
return 0;
}
Notice that socket() works exactly like open() for files โ it gives you a file descriptor. You close it with close() when done. The OS cleans up the socket resource on close.
Different domains use different address formats. Each domain has its own address structure. Because C does not have polymorphism, all socket system calls use a generic structure struct sockaddr as a common “base type”. You cast your domain-specific structure to this generic type when passing to system calls.
Generic โ used in system call signatures
AF_UNIX
sun_family
sun_path (file path)
AF_INET
sin_family
sin_port + sin_addr
AF_INET6
sin6_family
sin6_port + sin6_addr
/* Generic socket address - used in system call parameters */
struct sockaddr {
sa_family_t sa_family; /* Address family: AF_UNIX, AF_INET, etc */
char sa_data[14]; /* Address data (domain-specific) */
};
/* UNIX domain address */
struct sockaddr_un {
sa_family_t sun_family; /* Always AF_UNIX */
char sun_path[108]; /* Path to socket file, e.g. "/tmp/server.sock" */
};
/* IPv4 address */
struct sockaddr_in {
sa_family_t sin_family; /* Always AF_INET */
in_port_t sin_port; /* Port number (network byte order) */
struct in_addr sin_addr; /* IPv4 address */
};
When you call bind(), connect(), sendto() etc. you always cast your specific structure to (struct sockaddr *). The kernel reads the sa_family field first to know what domain it is dealing with, then interprets the rest of the structure accordingly.
Since Linux 2.6.27, you can OR additional flags into the type argument of socket() to avoid extra system calls:
- SOCK_NONBLOCK โ Set the socket to non-blocking mode. Reads and writes return immediately even if data is not ready (return EAGAIN). Useful in event-driven servers.
- SOCK_CLOEXEC โ Automatically close the socket when exec() is called. Prevents unintended inheritance by child processes after fork+exec.
/* Create a non-blocking, close-on-exec TCP socket in one call */
int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
Without these flags you would need separate calls to fcntl() to set O_NONBLOCK and FD_CLOEXEC. The combined approach also avoids race conditions in multi-threaded programs.
Q1. What is a socket and why is it represented as a file descriptor?
A socket is a communication endpoint that allows processes to exchange data. In Linux, everything is a file โ sockets are no exception. By representing a socket as a file descriptor, the kernel lets you use the same read(), write(), close() system calls on sockets as on regular files. This unification simplifies programming and allows sockets to work with standard I/O multiplexing tools like select(), poll(), and epoll().
Q2. What is the difference between AF_UNIX and AF_INET?
AF_UNIX (Unix domain sockets) are for IPC between processes on the same machine. They use a file system path as the address. AF_INET is for IPv4 networking between processes on different machines. AF_UNIX is faster because data never leaves the kernel โ no TCP/IP stack overhead. AF_INET requires IP routing and protocol processing.
Q3. What is the difference between SOCK_STREAM and SOCK_DGRAM?
SOCK_STREAM provides a reliable, ordered, connection-oriented byte stream (TCP). SOCK_DGRAM provides unreliable, connectionless, message-oriented communication (UDP). SOCK_STREAM guarantees delivery and ordering but has connection setup overhead. SOCK_DGRAM has lower overhead but the application must handle retransmission if needed.
Q4. Why does socket() take a protocol argument if we usually pass 0?
For most domain+type combinations there is only one protocol. Passing 0 lets the kernel choose automatically. The protocol argument exists for cases where multiple protocols could serve the same socket type in a domain. For example, for raw sockets (SOCK_RAW over AF_INET), you may specify IPPROTO_TCP or IPPROTO_UDP to filter specific packet types.
Q5. What happens if you call close() on a socket file descriptor?
The kernel decrements the reference count of the socket. When the count reaches zero, the socket is destroyed. For a stream socket, this triggers a connection teardown (FIN exchange). For AF_UNIX sockets, the socket file in the filesystem is NOT automatically deleted โ the server must unlink it explicitly before rebinding.
Q6. What is SOCK_NONBLOCK and when would you use it in embedded systems?
SOCK_NONBLOCK sets the socket to non-blocking mode. I/O calls return immediately with EAGAIN if they cannot proceed. In embedded event-driven systems (using epoll or a main loop), blocking is unacceptable. A non-blocking socket lets a single thread handle multiple connections or serve both socket I/O and hardware events without stalling.
Q7. Why do we cast domain-specific address structures to struct sockaddr *?
C lacks polymorphism. Socket API was designed before void* was standard. struct sockaddr acts as a generic base type. All domain-specific structures (sockaddr_un, sockaddr_in, sockaddr_in6) start with the same sa_family field, so the kernel reads that first and then interprets the rest based on the domain. The cast to struct sockaddr* is the C equivalent of passing a “base class pointer”.
Learn how to build the full socket lifecycle for both server and client.
