Sockets Introduction Datagram Sockets: sendto() and recvfrom()

 

Chapter 56: Sockets Introduction
Part 4 of 5 โ€” Datagram Sockets: sendto() and recvfrom()
๐Ÿ“ฆ sendto()
๐Ÿ“ฌ recvfrom()
๐Ÿ”“ Connectionless

What is a Datagram Socket?

A datagram socket (SOCK_DGRAM) is connectionless. There is no connect/accept handshake. Each message (datagram) is independent โ€” it contains the destination address and is sent as a single unit. Think of it like sending a letter in the postal system: you write the address on each letter separately, and there is no guaranteed delivery.

In IPv4/IPv6, datagram sockets use UDP. In AF_UNIX, datagram sockets provide reliable message-passing between processes on the same machine (AF_UNIX datagrams are reliable unlike UDP).

Datagram sockets are useful when speed matters more than reliability (DNS, DHCP, telemetry) or when the application handles its own reliability (QUIC, game protocols). In embedded systems, AF_UNIX datagrams are a lightweight IPC mechanism.

Key Terms in This Part

SOCK_DGRAM sendto() recvfrom() datagram connectionless message boundary truncation MSG_TRUNC

sendto() and recvfrom() โ€” Datagram I/O System Calls

Because datagram sockets are connectionless, you must provide the destination address with every send. Similarly, when receiving, you often want to know who sent the datagram. This is why there are specialised calls: sendto() and recvfrom().

#include <sys/socket.h>

ssize_t sendto(int sockfd,
               const void *buffer,
               size_t length,
               int flags,
               const struct sockaddr *dest_addr,
               socklen_t addrlen);
/* Returns number of bytes sent, or -1 on error */

ssize_t recvfrom(int sockfd,
                 void *buffer,
                 size_t length,
                 int flags,
                 struct sockaddr *src_addr,
                 socklen_t *addrlen);
/* Returns number of bytes received, 0 on EOF, or -1 on error */

sendto() parameters:

  • sockfd โ€” The datagram socket file descriptor
  • buffer โ€” Pointer to data to send
  • length โ€” Number of bytes to send
  • flags โ€” Control flags. Pass 0 for normal use. Covered in detail in Chapter 61.
  • dest_addr โ€” Address of the destination socket
  • addrlen โ€” Size of dest_addr structure

recvfrom() parameters:

  • buffer โ€” Buffer to store received data
  • length โ€” Maximum bytes to receive
  • flags โ€” Control flags. Pass 0 for normal use.
  • src_addr โ€” Filled with the sender’s address after the call. Pass NULL if you don’t need it.
  • addrlen โ€” Value-result: set to sizeof(src_addr structure) before call; contains actual size after.

Datagram Socket Communication Flow
Client
socket(AF_INET, SOCK_DGRAM)
sendto(server_addr, data)
No bind/connect needed
kernel assigns port
๐Ÿ“ฆ datagram (addr + data)
โ†’
Server
socket(AF_INET, SOCK_DGRAM)
bind(well-known-port)
recvfrom(buffer, &client_addr)
No listen/accept needed
can recv from ANY client

Key difference from stream sockets: the datagram server does NOT call listen() or accept(). After bind(), it can immediately receive datagrams from any client. Multiple clients can send to the same server socket simultaneously.

Message Boundaries and Truncation

Unlike stream sockets, datagram sockets preserve message boundaries. Each sendto() call creates exactly one datagram. Each recvfrom() call receives exactly one datagram โ€” the full message as sent, or nothing.

Stream vs Datagram: Message Boundaries
SOCK_STREAM (no boundaries)
Sender: write(“HELLO”) + write(“WORLD”)
โ†“ kernel may combine
Receiver: read() โ†’ “HELLOWORLD” (one read)
No message boundary preserved!
SOCK_DGRAM (boundaries kept)
Sender: sendto(“HELLO”) + sendto(“WORLD”)
โ†“ two separate datagrams
Receiver: recvfrom() โ†’ “HELLO”
Receiver: recvfrom() โ†’ “WORLD”
Each message is preserved!

Truncation: If you call recvfrom() with a buffer smaller than the incoming datagram, the excess bytes are silently discarded. You cannot retrieve them with another read โ€” the datagram is gone. Always use a buffer large enough for the largest expected datagram.

To detect truncation, use recvmsg() instead of recvfrom(). It returns the MSG_TRUNC flag in the msg_flags field of the msghdr structure if the datagram was truncated.

/* Detecting truncation with recvmsg() */
#include <sys/socket.h>

struct msghdr msg;
struct iovec iov;
char buf[256];  /* intentionally small to demonstrate truncation */

iov.iov_base = buf;
iov.iov_len  = sizeof(buf);

memset(&msg, 0, sizeof(msg));
msg.msg_iov    = &iov;
msg.msg_iovlen = 1;

ssize_t nr = recvmsg(sockfd, &msg, 0);
if (nr == -1) {
    perror("recvmsg");
} else {
    if (msg.msg_flags & MSG_TRUNC) {
        printf("WARNING: datagram was truncated! Only got %zd bytes\n", nr);
    } else {
        printf("Received %zd bytes (complete)\n", nr);
    }
}

On Linux, it is also possible to send a datagram of length 0 using sendto(). The receiver gets a recvfrom() returning 0 bytes โ€” not EOF. However, not all Unix implementations allow zero-length datagrams.

Complete UDP Datagram Server

The server below receives datagrams on port 9000 and sends a response back to the sender using the address captured by recvfrom().

/* udp_server.c โ€” Simple UDP datagram server */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT    9000
#define BUFSIZE 1024

int main(void)
{
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addrlen;
    char buf[BUFSIZE];
    ssize_t nr;
    char client_ip[INET_ADDRSTRLEN];

    /* Step 1: Create UDP socket */
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    /* Step 2: Bind to well-known port */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family      = AF_INET;
    server_addr.sin_port        = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    printf("UDP server listening on port %d\n", PORT);

    /* Step 3: Receive loop โ€” no listen/accept needed */
    for (;;) {
        client_addrlen = sizeof(client_addr);

        nr = recvfrom(sockfd, buf, sizeof(buf) - 1, 0,
                      (struct sockaddr *)&client_addr, &client_addrlen);
        if (nr == -1) {
            perror("recvfrom");
            continue;
        }

        buf[nr] = '\0';

        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
        printf("Received %zd bytes from %s:%d: %s\n",
               nr, client_ip, ntohs(client_addr.sin_port), buf);

        /* Send a response back to the same client */
        const char *response = "ACK";
        if (sendto(sockfd, response, strlen(response), 0,
                   (struct sockaddr *)&client_addr, client_addrlen) == -1) {
            perror("sendto");
        }
    }

    close(sockfd);
    return 0;
}

Complete UDP Datagram Client
/* udp_client.c โ€” Simple UDP datagram client */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define PORT      9000
#define BUFSIZE   1024

int main(void)
{
    int sockfd;
    struct sockaddr_in server_addr;
    char send_buf[BUFSIZE];
    char recv_buf[BUFSIZE];
    ssize_t nr;
    socklen_t server_addrlen;

    /* Step 1: Create UDP socket */
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    /* Step 2: Set up server address (no connect/bind needed) */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port   = htons(PORT);

    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("inet_pton");
        exit(EXIT_FAILURE);
    }

    printf("UDP client ready. Type messages (Ctrl+D to exit):\n");

    while (fgets(send_buf, sizeof(send_buf), stdin) != NULL) {
        size_t len = strlen(send_buf);

        /* Remove trailing newline */
        if (len > 0 && send_buf[len-1] == '\n') {
            send_buf[--len] = '\0';
        }

        /* Step 3: Send datagram to server โ€” must specify address every time */
        if (sendto(sockfd, send_buf, len, 0,
                   (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
            perror("sendto");
            continue;
        }

        /* Step 4: Receive server response */
        server_addrlen = sizeof(server_addr);
        nr = recvfrom(sockfd, recv_buf, sizeof(recv_buf) - 1, 0,
                      (struct sockaddr *)&server_addr, &server_addrlen);
        if (nr == -1) {
            perror("recvfrom");
            continue;
        }

        recv_buf[nr] = '\0';
        printf("Server response: %s\n", recv_buf);
    }

    close(sockfd);
    return 0;
}
/* Build and run */
gcc -Wall -o udp_server udp_server.c
gcc -Wall -o udp_client udp_client.c

/* Terminal 1 */
./udp_server

/* Terminal 2 */
./udp_client
/* Type: hello */
/* See: Server response: ACK */

Notice the client does not call bind() or connect(). The kernel auto-assigns an ephemeral source port. The server captures the client address from recvfrom() and uses it in sendto() to reply.

Using recv() and read() Instead of recvfrom()

If you don’t need the sender’s address, you can use simpler alternatives:

  • Pass NULL for both src_addr and addrlen to recvfrom() โ€” it behaves like recv()
  • Call recv() directly with a flags argument of 0
  • Call read() โ€” equivalent to recv() with flags=0
/* These three are equivalent when you don't need sender's address */

/* Option 1: recvfrom with NULL address */
nr = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);

/* Option 2: recv() */
nr = recv(sockfd, buf, sizeof(buf), 0);

/* Option 3: read() */
nr = read(sockfd, buf, sizeof(buf));

For a datagram socket that is already “connected” (peer address recorded via connect()), read() and write() are sufficient. This is covered in Part 5.

Note: For sending, there is no equivalent shortcut on an unconnected datagram socket. You must use sendto() and provide the destination address. You cannot use write() on an unconnected datagram socket โ€” it will fail with ENOTCONN.

Interview Questions โ€” Datagram Sockets

Q1. What is the difference between a stream socket and a datagram socket?

Stream socket (SOCK_STREAM): connection-oriented, reliable, ordered, byte-stream, no message boundaries. Datagram socket (SOCK_DGRAM): connectionless, unreliable (under UDP), unordered, message-oriented, preserves boundaries. Stream sockets require connect/accept. Datagram servers need only bind and then call recvfrom in a loop.

Q2. Does a UDP datagram server need to call listen()?

No. listen() is only for stream (TCP-like) sockets to queue incoming connection requests. A datagram server only needs socket() and bind() to be ready to receive datagrams from any client. There is no concept of a connection queue in datagram communication.

Q3. What happens if the receive buffer in recvfrom() is smaller than the incoming datagram?

The excess bytes are silently discarded. The datagram is consumed from the kernel’s receive queue, and the bytes that don’t fit in the buffer are lost permanently. To detect this, use recvmsg() and check for MSG_TRUNC in msg_flags. Always size the receive buffer to accommodate the maximum expected datagram.

Q4. Why does sendto() require the destination address on every call?

Datagram sockets are connectionless. There is no persistent peer relationship. Each datagram is independent and can go to a different destination. The kernel uses the address you provide in sendto() to route each individual datagram. This is different from stream sockets where connect() establishes a permanent peer.

Q5. Can you use read() and write() on a datagram socket?

read() works the same as recv() (flags=0) on a datagram socket. write() works only if the socket has been connected (connect() called to record a default peer). On an unconnected datagram socket, write() fails with ENOTCONN because there is no destination address. For unconnected datagrams, use sendto() which lets you specify the destination per call.

Q6. Are AF_UNIX datagram sockets reliable?

Yes. Unlike UDP (AF_INET + SOCK_DGRAM), AF_UNIX datagram sockets are reliable. The kernel never drops them (as long as the receiver’s buffer is not full). This makes AF_UNIX datagrams attractive for local IPC when you need message boundaries but also need reliability without TCP’s connection overhead.

Q7. How does a datagram server know which client to reply to?

By using the src_addr argument of recvfrom(). The kernel fills this with the sender’s address (IP + port for UDP, or socket path for AF_UNIX). The server saves this address and passes it as dest_addr to sendto() to send the reply back. Without recvfrom()’s src_addr, the server has no way to reply to the right client.

Q8. What does a recvfrom() return value of 0 mean for a datagram socket?

It means a zero-length datagram was received โ€” not EOF. Datagram sockets don’t have a connection to close, so they don’t get EOF in the TCP sense. A zero-length datagram is valid (on Linux). -1 means an error occurred. This is different from stream sockets where read() returning 0 means the peer has closed the connection.

Next: Part 5 โ€” Connected Datagram Sockets

Learn how connect() simplifies datagram I/O when communicating with a fixed peer.

Leave a Reply

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