Connected Datagram Sockets
In Part 4 we saw that datagram sockets are connectionless and require the destination address on every sendto(). But what if you always send to the same server? Specifying the address every call is repetitive and error-prone.
connect() can be called on a datagram socket to record a default peer address in the kernel. After this, you can use write() instead of sendto() and the kernel fills in the destination automatically. The socket is now called a connected datagram socket.
This is especially useful for datagram clients that send many requests to a single server (like a DNS client sending multiple queries to the same resolver).
Calling connect() on a datagram socket does not send any packets. It only tells the kernel: “from now on, treat this socket as if its default destination is this address.” The kernel records the peer address internally.
After connect():
- You can use write() instead of sendto() โ no need to specify dest_addr on every send.
- You can use read() instead of recvfrom() โ no need for src_addr argument.
- The socket will only receive datagrams from the connected peer. Datagrams from any other address are silently discarded by the kernel.
An important subtlety: calling connect() on a datagram socket only affects that socket. The remote socket is completely unaware of it. This is fundamentally different from TCP’s connect() which creates a two-sided connection.
โ can use write()
โ only receives from server
โ ๏ธ still must use sendto()
โ ๏ธ can receive from ANY client
The server’s socket is not affected. The server still uses recvfrom()/sendto() and can receive from any client.
If the server also calls connect() pointing to the client, then the server socket also gets filtered and can only talk to that specific client. In practice, servers rarely connect() their datagram sockets unless building a peer-to-peer protocol.
Unlike TCP, you can change or remove the peer of a connected datagram socket without closing and recreating it.
To change the peer: call connect() again with the new address. The old association is replaced.
To dissolve the peer (revert to unconnected): call connect() with an address structure whose address family is set to AF_UNSPEC.
/* Change peer address */
struct sockaddr_in new_server;
memset(&new_server, 0, sizeof(new_server));
new_server.sin_family = AF_INET;
new_server.sin_port = htons(9001);
inet_pton(AF_INET, "10.0.0.2", &new_server.sin_addr);
connect(sockfd, (struct sockaddr *)&new_server, sizeof(new_server));
/* Socket now talks to 10.0.0.2:9001 */
/* Dissolve peer โ revert to unconnected datagram socket */
struct sockaddr addr_unspec;
memset(&addr_unspec, 0, sizeof(addr_unspec));
addr_unspec.sa_family = AF_UNSPEC;
connect(sockfd, &addr_unspec, sizeof(addr_unspec));
/* Socket is now unconnected again โ must use sendto() */
Note: The AF_UNSPEC dissolve method is defined by SUSv4. Many older Unix implementations do not support it. SUSv3 was vague about this (called it “null address” without defining what that means). For maximum portability, close and recreate the socket instead of dissolving.
Here is a complete client that calls connect() once and then uses write()/read() instead of sendto()/recvfrom():
/* udp_connected_client.c โ Connected datagram client using write/read */
#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;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
/*
* connect() on a SOCK_DGRAM socket:
* - Does NOT send any data
* - Records server_addr as the default destination
* - Restricts received datagrams to those from server_addr
*/
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("connect");
exit(EXIT_FAILURE);
}
printf("Connected datagram client ready (server: %s:%d)\n", SERVER_IP, PORT);
printf("Type messages (Ctrl+D to exit):\n");
while (fgets(send_buf, sizeof(send_buf), stdin) != NULL) {
size_t len = strlen(send_buf);
/* Use write() instead of sendto() โ kernel adds server address */
if (write(sockfd, send_buf, len) == -1) {
perror("write");
break;
}
/* Use read() instead of recvfrom() โ we don't need sender's address */
nr = read(sockfd, recv_buf, sizeof(recv_buf) - 1);
if (nr == -1) {
perror("read");
break;
}
recv_buf[nr] = '\0';
printf("Server: %s", recv_buf);
}
close(sockfd);
return 0;
}
This is cleaner than the unconnected version in Part 4. The trade-off is that this client can only communicate with the one server it connected to.
- Client sending many requests to one server
- Want simpler write()/read() API
- Want kernel to filter incoming datagrams (security)
- DNS resolver clients
- NTP clients
- Server receiving from multiple clients
- Need sender’s address to reply
- Client sending to different servers per request
- Broadcast/multicast scenarios
- DHCP servers
On some TCP/IP implementations (historically on BSD), connecting a UDP socket to a peer improves performance because the kernel can skip address lookup per packet. On Linux, the performance difference is minimal. The primary benefit on Linux is code simplicity and the automatic peer filter.
| Function | Purpose | Server/Client | Stream/Dgram |
|---|---|---|---|
socket() |
Create socket, get fd | Both | Both |
bind() |
Assign address to socket | Server (required), Client (optional) | Both |
listen() |
Mark as passive, set backlog | Server only | Stream only |
accept() |
Accept incoming connection, get new fd | Server only | Stream only |
connect() |
Connect to peer (stream) or record peer (dgram) | Client | Both |
read() / write() |
Transfer data (generic fd I/O) | Both | Both (connected only for dgram write) |
recvfrom() |
Receive + get sender address | Server (typical) | Datagram (typical) |
sendto() |
Send to specific address | Both | Datagram (unconnected) |
shutdown() |
Half-close connection (SHUT_RD/SHUT_WR) | Both | Stream |
close() |
Release socket fd | Both | Both |
Q1. What does connect() do differently on SOCK_DGRAM vs SOCK_STREAM?
For SOCK_STREAM, connect() initiates a TCP three-way handshake and establishes a full bidirectional connection with the server. For SOCK_DGRAM, connect() sends no packets at all. It simply records the peer address in the kernel. Subsequent write() calls use this address automatically, and the socket only accepts incoming datagrams from that peer.
Q2. What is an “unconnected datagram socket”?
Any datagram socket that has not had connect() called on it. This is the default state of a new SOCK_DGRAM socket. You must use sendto() with an explicit destination address for each send. You can receive from any sender. Most datagram servers use unconnected sockets so they can respond to multiple clients.
Q3. How do you dissolve a peer association from a connected datagram socket?
Call connect() with an address structure whose sa_family field is set to AF_UNSPEC. This tells the kernel to remove the peer association, reverting the socket to unconnected state. After this, sendto() with explicit address must be used again. Note that AF_UNSPEC dissolve is a SUSv4 feature and not universally supported on older systems.
Q4. Can a connected datagram socket receive datagrams from a different sender?
No. Once connect() is called on a datagram socket, the kernel filters incoming datagrams and only delivers those from the connected peer. Datagrams from any other address are silently discarded. This filtering happens in the kernel before the application ever sees the data.
Q5. What is the performance benefit of connect() on a datagram socket on Linux?
On Linux, the performance difference is minimal. Some other TCP/IP implementations (like BSD) show an improvement because the kernel can skip per-packet address resolution after connect(). The main benefit on Linux is code simplicity โ using write()/read() instead of sendto()/recvfrom() โ and the automatic peer filtering for security.
Q6. What are the three domains specified by SUSv3 for sockets?
AF_UNIX (or AF_LOCAL) for same-machine IPC using file system paths. AF_INET for IPv4 networking with IP address and port. AF_INET6 for IPv6 networking with 128-bit address and port. These are the three domains mandated by SUSv3; many implementations add more (like AF_NETLINK on Linux for kernel-userspace communication).
Q7. A datagram client wants to send many requests to two different servers alternately. Should it use connect()?
It can, but it must call connect() again each time it wants to switch servers, which effectively re-associates the socket. Alternatively, it can stay unconnected and use sendto() with the appropriate server address per request โ simpler when the destination changes frequently. Connected datagram sockets are mainly beneficial when the destination is fixed for the socket’s lifetime.
Q8. What is MSG_TRUNC and when does it appear?
MSG_TRUNC is a flag returned in the msg_flags field of msghdr when using recvmsg(). It indicates that the received datagram was larger than the supplied buffer and the excess bytes were discarded. It helps the application detect that it received an incomplete datagram. Ordinary recvfrom() provides no such notification โ truncation is silent.
You have covered all of Chapter 56 โ Sockets Introduction. Next up: Unix Domain Sockets (Chapter 57) for high-performance local IPC.
