Your First UDP Exchange in C++
Send hello to a local server and get the same bytes back. The two programs make the difference between sending a datagram and receiving an application reply visible.
Before you start
Complete Addresses, Ports and Sockets. Keep two terminals open in the same examples/network directory on the Pi.
Outcome: run a UDP request and response, explain the sender address returned by recvfrom(), and observe a timeout.
Follow the exchange
sequenceDiagram
accTitle: UDP echo exchange on loopback
accDescr: The server binds port 9000. A client sends hello from a temporary port. The server receives the sender address and echoes the bytes to that address. The client checks the reply.
participant C as Client
participant S as Server
Note over S: bind 127.0.0.1:9000
Note over C: Temporary port chosen<br/>on the first send
C->>S: sendto: hello
S->>S: recvfrom: bytes + peer
S->>C: sendto: hello, to peer
C->>C: recvfrom: check reply
Note over S: Exit after one datagram
Both programs use 127.0.0.1. Traffic stays on the Pi; this example is not listening on its LAN address.
Build and run
In either terminal, build once:
| cmake --build --preset native-debug --parallel 2
|
In terminal A:
| ./build/native/debug/udp_echo_server 9000
|
It prints listening 127.0.0.1:9000. Within 30 seconds, run in terminal B:
| ./build/native/debug/udp_echo_client 9000 hello
|
Client output:
The server prints echoed bytes=5 and exits. It handles one datagram. Start it again for each experiment.
The server
Complete file: app/udp-echo/server.cpp. This is a standalone C++ file; every socket operation is shown here.
| #include <arpa/inet.h>
#include <array>
#include <cerrno>
#include <charconv>
#include <cstdint>
#include <iostream>
#include <poll.h>
#include <stdexcept>
#include <string_view>
#include <sys/socket.h>
#include <system_error>
#include <unistd.h>
int main(int argc, char** argv) {
int fd = -1;
int status = 0;
try {
if (argc > 2) {
throw std::invalid_argument("usage: udp_echo_server [port]");
}
unsigned int port = 9000;
if (argc == 2) {
const std::string_view text = argv[1];
const auto [end, error] =
std::from_chars(text.data(), text.data() + text.size(), port);
if (error != std::errc{} || end != text.data() + text.size() ||
port > 65535) {
throw std::invalid_argument("port must be 0..65535");
}
}
fd = ::socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0) {
throw std::system_error(errno, std::generic_category(), "socket");
}
sockaddr_in local{};
local.sin_family = AF_INET;
local.sin_port = htons(static_cast<std::uint16_t>(port));
local.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (::bind(fd, reinterpret_cast<const sockaddr*>(&local),
sizeof(local)) < 0) {
throw std::system_error(errno, std::generic_category(), "bind");
}
socklen_t local_size = sizeof(local);
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&local),
&local_size) < 0) {
throw std::system_error(errno, std::generic_category(),
"getsockname");
}
std::cout << "listening 127.0.0.1:" << ntohs(local.sin_port)
<< std::endl;
// Wait for one datagram for at most 30 seconds.
pollfd input{fd, POLLIN, 0};
const int ready = ::poll(&input, 1, 30000);
if (ready < 0) {
throw std::system_error(errno, std::generic_category(), "poll");
}
if (ready == 0) {
throw std::runtime_error("receive timeout");
}
if (!(input.revents & POLLIN)) {
throw std::runtime_error("socket error while waiting");
}
std::array<char, 1024> buffer{};
sockaddr_in peer{};
socklen_t peer_size = sizeof(peer);
const auto size =
::recvfrom(fd, buffer.data(), buffer.size(), MSG_TRUNC,
reinterpret_cast<sockaddr*>(&peer), &peer_size);
if (size < 0) {
throw std::system_error(errno, std::generic_category(), "recvfrom");
}
if (static_cast<std::size_t>(size) > buffer.size()) {
throw std::runtime_error("datagram too large");
}
// Reply to the sender, not to the server's own port.
const auto sent = ::sendto(
fd, buffer.data(), static_cast<std::size_t>(size), MSG_DONTWAIT,
reinterpret_cast<const sockaddr*>(&peer), peer_size);
if (sent < 0) {
throw std::system_error(errno, std::generic_category(), "sendto");
}
if (sent != size) {
throw std::runtime_error("short datagram send");
}
std::cout << "echoed bytes=" << size << '\n';
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
status = 1;
}
if (fd >= 0) {
::close(fd);
}
return status;
}
|
socket() creates a raw descriptor. The sockaddr_in fields set IPv4, a port and the loopback address; htons() and htonl() put the integers into network byte order. bind() selects that local endpoint. recvfrom() fills peer with the sender's endpoint, allowing the server to reply to the temporary client port. The payload is a byte count plus a buffer, not a null-terminated C string.
MSG_TRUNC lets this Linux example detect a datagram larger than its buffer. The server rejects that case instead of echoing a misleading partial message.
The client
Complete file: app/udp-echo/client.cpp.
| #include <arpa/inet.h>
#include <array>
#include <cerrno>
#include <charconv>
#include <cstdint>
#include <iostream>
#include <poll.h>
#include <stdexcept>
#include <string_view>
#include <sys/socket.h>
#include <system_error>
#include <unistd.h>
int main(int argc, char** argv) {
int fd = -1;
int status = 0;
try {
if (argc < 2 || argc > 3) {
throw std::invalid_argument(
"usage: udp_echo_client port [message]");
}
unsigned int port = 0;
const std::string_view text = argv[1];
const auto [end, error] =
std::from_chars(text.data(), text.data() + text.size(), port);
if (error != std::errc{} || end != text.data() + text.size() ||
port == 0 || port > 65535) {
throw std::invalid_argument("port must be 1..65535");
}
const std::string_view message = argc == 3 ? argv[2] : "hello";
if (message.size() > 1024) {
throw std::invalid_argument("message exceeds 1024 bytes");
}
fd = ::socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0) {
throw std::system_error(errno, std::generic_category(), "socket");
}
sockaddr_in server{};
server.sin_family = AF_INET;
server.sin_port = htons(static_cast<std::uint16_t>(port));
server.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
const auto sent = ::sendto(
fd, message.data(), message.size(), MSG_DONTWAIT,
reinterpret_cast<const sockaddr*>(&server), sizeof(server));
if (sent < 0) {
throw std::system_error(errno, std::generic_category(), "sendto");
}
if (static_cast<std::size_t>(sent) != message.size()) {
throw std::runtime_error("short datagram send");
}
pollfd input{fd, POLLIN, 0};
const int ready = ::poll(&input, 1, 3000);
if (ready < 0) {
throw std::system_error(errno, std::generic_category(), "poll");
}
if (ready == 0) {
throw std::runtime_error("receive timeout");
}
if (!(input.revents & POLLIN)) {
throw std::runtime_error("socket error while waiting");
}
std::array<char, 1024> buffer{};
sockaddr_in peer{};
socklen_t peer_size = sizeof(peer);
const auto size =
::recvfrom(fd, buffer.data(), buffer.size(), MSG_TRUNC,
reinterpret_cast<sockaddr*>(&peer), &peer_size);
if (size < 0) {
throw std::system_error(errno, std::generic_category(), "recvfrom");
}
if (static_cast<std::size_t>(size) > buffer.size()) {
throw std::runtime_error("reply too large");
}
if (peer.sin_addr.s_addr != server.sin_addr.s_addr ||
peer.sin_port != server.sin_port) {
throw std::runtime_error("reply from unexpected endpoint");
}
const std::string_view reply(buffer.data(),
static_cast<std::size_t>(size));
if (reply != message) {
throw std::runtime_error("echo mismatch");
}
std::cout << "reply bytes=" << size << " data=" << reply << '\n';
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
status = 1;
}
if (fd >= 0) {
::close(fd);
}
return status;
}
|
sendto() submits one datagram, and its return value is checked directly. The operating system chooses a local client port on its first send. poll() waits up to three seconds for input; a nonblocking socket ensures the later receive cannot wait indefinitely if input is no longer available.
The reply's endpoint and bytes are checked. This validates the exercise's echo, not cryptographic peer identity. Each program keeps its descriptor as an integer. Its final close(fd) releases the socket after success or an error.
A negative system-call return is checked at the call site. std::system_error records errno and passes control to catch; the code then sets a failure exit status and reaches the same explicit cleanup. The initial -1 means that no descriptor has been acquired yet.
Compile each file directly
Each file also builds without CMake or any course header. From examples/network:
| mkdir -p build/direct
c++ -std=c++20 -Wall -Wextra -Wpedantic app/udp-echo/server.cpp -o build/direct/udp_echo_server
c++ -std=c++20 -Wall -Wextra -Wpedantic app/udp-echo/client.cpp -o build/direct/udp_echo_client
|
Run these two executables in separate terminals with the same arguments as above. CMake is an alternative way to compile the same files and run the checks.
Change one condition
Restart the server and send a message containing a space:
| ./build/native/debug/udp_echo_client 9000 "hello Pi"
|
Expect reply bytes=8 data=hello Pi. Quoting keeps the message as one command-line argument.
After the server has exited, run the client once more. It exits with an error: depending on local error reporting, this may be a receive timeout or a socket error. A successful sendto() only means the kernel accepted the datagram for sending.
If the server reports Address already in use, choose another unprivileged port, such as 19000, in both commands. Do not start another permanent service or stop an unrelated process.
Check your understanding
Why does the server reply to peer rather than sending to port 9000 again?
Compare your reasoning
Port 9000 is the server's endpoint. The client's request came from a different local port, supplied by recvfrom. Replies must target that client endpoint.
Continue the course
Previous: Addresses, Ports and Sockets · Course overview · Next: Datagram Boundaries and Buffer Sizes
References: Linux UDP behaviour, poll.