Linux IPC
56 – TLPI
Beginner–Intermediate
What You Will Learn
After picking a domain, the second major choice when calling socket() is the socket type. The two most common types are SOCK_STREAM (reliable, connection-based) and SOCK_DGRAM (message-based, no connection). Understanding the guarantees and trade-offs of each is essential before writing any network code.
Every socket implementation provides at least two types. They are supported in both the Unix domain and the Internet domain.
| Property | SOCK_STREAM | SOCK_DGRAM |
|---|---|---|
| Reliable delivery? | Yes ✔ | No ✘ |
| Message boundaries preserved? | No ✘ (byte stream) | Yes ✔ |
| Connection required? | Yes (connection-oriented) | No (connectionless) |
| Internet protocol | TCP | UDP |
| Typical use case | HTTP, SSH, FTP, databases | DNS, video streaming, games |
A stream socket delivers a reliable, bidirectional, byte-stream channel. Breaking this down:
Reliable — Data you send is either received intact and in order by the other end, or you receive an error notification. Nothing silently disappears. In the Internet domain this is provided by TCP.
Bidirectional — Both sides can send and receive on the same connection simultaneously.
Byte-stream — There are no message boundaries. If you call write(fd, buf, 100) twice, the receiver may get all 200 bytes in one read(), or split across several calls. This is identical to a pipe’s behaviour. You are responsible for adding your own framing if you need to distinguish individual messages.
Stream sockets are connection-oriented: before any data can flow, a connection must be established between two sockets. A stream socket can be connected to exactly one peer at a time.
| Sender writes | Receiver may read |
| write(fd, “Hello”, 5) write(fd, “World”, 5) |
read() → “HelloWorld” (10 bytes) OR two separate reads of 5 each |
Creating a TCP (stream) socket:
#include <sys/socket.h>
/* TCP socket — AF_INET domain, stream type */
int tcpfd = socket(AF_INET, SOCK_STREAM, 0);
/* Unix domain stream socket */
int unixfd = socket(AF_UNIX, SOCK_STREAM, 0);
Terminology around stream sockets:
- Peer socket — the socket at the other end of the connection.
- Peer address — the address of that peer socket.
- Peer application — the application using the peer socket.
- Remote / foreign — synonyms for peer (from the sender’s perspective).
- Local — refers to this end of the connection.
A datagram socket sends and receives data in discrete chunks called datagrams. Each datagram is a self-contained message. Unlike stream sockets:
- Message boundaries are preserved — each
sendto()corresponds to exactly onerecvfrom(). You never get half a message. - Delivery is not guaranteed — datagrams can be lost, reordered, or duplicated. No error notification is given when a message is dropped.
- No connection required — you can send to any address at any time. This is called connectionless.
In the Internet domain, datagram sockets use UDP (User Datagram Protocol). This is why we often just say “UDP socket” for an Internet domain datagram socket.
| Sender | Network | Receiver |
| sendto() “Msg1” | ✔ delivered | recvfrom() → “Msg1” |
| sendto() “Msg2” | ✘ dropped | never received |
| sendto() “Msg3” | ⟳ reordered | recvfrom() → “Msg3” (arrived before Msg2) |
Creating a UDP (datagram) socket:
#include <sys/socket.h>
/* UDP socket — AF_INET domain, datagram type */
int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
/* Unix domain datagram socket */
int unixdg = socket(AF_UNIX, SOCK_DGRAM, 0);
Basic UDP send/receive (no connect needed):
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
/* ---- SENDER side ---- */
void udp_sender(void)
{
int sockfd;
struct sockaddr_in dest;
const char *msg = "Hello UDP";
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(9000);
inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr);
/* sendto: specify destination each time — no prior connect needed */
sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr *)&dest, sizeof(dest));
close(sockfd);
}
/* ---- RECEIVER side ---- */
void udp_receiver(void)
{
int sockfd;
struct sockaddr_in me, sender;
socklen_t slen = sizeof(sender);
char buf[256];
ssize_t n;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
memset(&me, 0, sizeof(me));
me.sin_family = AF_INET;
me.sin_port = htons(9000);
me.sin_addr.s_addr = INADDR_ANY; /* listen on all interfaces */
bind(sockfd, (struct sockaddr *)&me, sizeof(me));
/* recvfrom: also tells you who sent it */
n = recvfrom(sockfd, buf, sizeof(buf)-1, 0,
(struct sockaddr *)&sender, &slen);
buf[n] = '\0';
printf("Got: '%s'\n", buf);
close(sockfd);
}
It is possible to call connect() on a datagram socket. This does not establish a real connection the way TCP does. Instead it simply records a default destination address in the kernel. After that you can use send()/write() instead of sendto(), and the kernel fills in the destination automatically.
The socket remains connectionless underneath — no handshake happens. You can call connect() again to change the destination, or pass a null address to unset it.
/* Connect a UDP socket to a default destination */
connect(udpfd, (struct sockaddr *)&dest, sizeof(dest));
/* Now write() instead of sendto() — dest auto-filled */
write(udpfd, "data", 4);
/* Can still override with sendto() if needed */
| Use TCP (SOCK_STREAM) when… | Use UDP (SOCK_DGRAM) when… |
|---|---|
| Data must arrive and be in order (e.g. file transfer, HTTP) | Speed matters more than reliability (e.g. live video, VoIP) |
| You cannot afford lost data | You want low latency and can handle occasional loss in app logic |
| Message size is variable or large | Short, self-contained queries like DNS lookups |
| You need flow control and congestion control | You want to broadcast/multicast to many receivers |
Q1. What does “reliable delivery” mean for SOCK_STREAM?
It means either the data arrives intact and in the exact order it was sent, or the application receives an error. Nothing is silently dropped. TCP achieves this through sequence numbers, acknowledgements, and retransmission.
Q2. What is a “byte stream” and why does it matter to the programmer?
A byte stream has no inherent message boundaries. A single write() call by the sender may be split across multiple read() calls at the receiver, or several writes may arrive in a single read. Programmers must add application-level framing (e.g. a length header before each message) to know where one message ends and the next begins.
Q3. What happens if a UDP datagram is lost?
Nothing automatic. UDP provides no acknowledgement or retransmission. The sender gets no error. If the application requires reliability over UDP it must implement its own ACK/retry logic (as QUIC and many game protocols do).
Q4. What is a “connectionless” socket?
A socket that does not require a prior handshake before sending data. SOCK_DGRAM sockets are connectionless — you can call sendto() to any destination address at any time without first establishing a session.
Q5. What does calling connect() on a UDP socket actually do?
It records a default peer address in the kernel for that socket. No handshake occurs. After this you can use send()/write() without specifying a destination each time. The socket remains connectionless — you can call connect() again to change the default destination.
Q6. Name the socket type and protocol used by a web browser connecting to an HTTP server.
SOCK_STREAM type in the AF_INET (or AF_INET6) domain, which maps to TCP at the transport layer.
Q7. What does “message boundaries are preserved” mean for SOCK_DGRAM?
Each sendto() call produces exactly one datagram, and each recvfrom() call receives exactly that one datagram — it will never be split or merged with another. This is the opposite of the byte-stream behaviour of SOCK_STREAM.
