In this tutorial, we will discuss how to get a basic UDP client and server up and running using C/C++
. The User Datagram Protocol (UDP) is a transport layer networking protocol. Unlike TCP it does not offer any reliability guarantees, nor does it respond to congestion on the network. It is simply a way of addressing network packets to a specific process which is often running on another machine. That packet is addressed by a port number ranging from 1 to 65535, though some of the port numbers are reserved for specific applications (e.g., ssh
, http
, https
, etc.). Any single port can only have one process listening on it, but a single process can listen on many ports.
UDP is a transport layer protocol. This means that is one layer above the Internet Protocol (IP). IP is used for addressing physical and virtual machines. When a process wants to send a packet to another machine, it creates an IP packet containing an IP address. Inside the body of that packet, there will be an UDP packet containing source and destination port numbers among other things.
You may have heard the expression that "in Linux, everything is a file." That remains true for network programming. We use a special type of file called a socket. When we write data to a socket, Linux will take care of the details of sending that data out on the network (i.e., constructing the packets and shuttling the bits to the network card, etc.). Conversely, when we read from a socket, Linux will tell us a little bit of info about where the packet came from. But first we need to tell the OS a little bit of information about what kinds of messages the socket should expect.
We will need a few addrinfo
structs. We will be passing these to various functions which will fill them in with details that we can later use or retrieve.
struct addrinfo hints, *res, *p;
We will use the getaddrinfo()
function to setup all the structs we will need later. The things we need to pass the function are: the hostname/IP, the port, some details about packets, and a place where the function can store the information we care about. In the following, the hostname/IP is set to NULL
meaning that function does not need to set the destination IP because this is a listener connection.
The PORT
is just the port number, written as a string. The pointer to hints
is where we tell the OS some details about the packets. In the first four lines below, we are telling the OS that both versions of IP (IPv4 and IPv6) are fine. The next line, we use SOCK_DGRAM
to indicate that we want to use UDP---if we wanted TCP, we would used SOCK_STREAM
. The AI_PASSIVE
tells the OS to fill in IP address automatically. This function returns a non-zero status upon failure. The res
pointer holds the results---all the structs that the function may have returned (it may return multiple if the machine has multiple IPs and/or if both IPv4 and IPv6 are being used).
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_DGRAM; // UDP datagram sockets
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
if ((status = getaddrinfo(NULL, PORT, &hints, &res)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
exit(1);
}
Assuming we're fine with using any of the returned structs, as long as they work, we can just iterate through the list stored in res
and try continue our setup with each set of connection information until one succeeds. In particular, the setup we need to do is
- Determine if the struct is for IPv4 or IPv6, and cast it to the appropriate struct.
- Convert the IP address into human readable form
- Create a socket with the provided info in the struct
- Bind the socket to the IP address
for (p = res; p != NULL; p = p->ai_next) {
void *ina;
/** step 1 **/
if (p->ai_family == AF_INET) { // IPv4
struct sockaddr_in * sa4 = (struct sockaddr_in *)p->ai_addr;
ina = &(sa4->sin_addr);
} else { // IPv6
struct sockaddr_in6 * sa6 = (struct sockaddr_in6 *)p->ai_addr;
ina = &(sa6->sin6_addr);
}
/** step 2 **/
inet_ntop(p->ai_family, ina, ipstr, sizeof ipstr);
printf("Listening on interface: %s\n", ipstr);
info(logger, "Listening on interface: %s\n", ipstr);
/** step 3 **/
if ((listener_fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
perror("server: socket");
continue;
}
/** step 4 **/
if (bind(listener_fd, p->ai_addr, p->ai_addrlen) == -1) {
perror("server: bind");
continue;
}
break;
}
In the code above listener_fd
is the file descriptor of the socket that we will ultimately want to read from.
Notice that unlike with TCP, we do not need to call listen()
because we are not waiting on a connection (UDP is connectionless).
We have already created the socket, so we will use this to get the system to fill in some information about received packets. We will also declare two char[]
for storing
struct sockaddr_storage peer_addr;
char host[MAXBUF];
char service[20];
Assign the same values to hints
.
len = sizeof(peer_addr);
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_DGRAM; // UDP datagram sockets
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
Clear the memory comprising the structs.
memset(&peer_addr, 0, sizeof peer_addr);
memset(buffer, 0, sizeof buffer);
Then use recvfrom()
to read from socket listener_fd
into buffer
.
bytes_read = recvfrom(listener_fd,
(char *)buffer, // buffer where results are stored
MAXBUF, // size of "buffer"
NULL,
(struct sockaddr *)&peer_addr, // put sender details here
&len); // size of peer_addr struct
The function recvfrom()
returns -1
if no bytes were read, i.e., no message was received. If a message was received, we can extract info about the message and the sender. The function getnameinfo()
will return the hostname of the sender.
if (bytes_read != -1) {
if (peer_addr.ss_family == AF_INET) { // IPv4 */
size = sizeof(struct sockaddr_in);
} else {
size = sizeof(struct sockaddr_in6);
}
memset(host, 0, sizeof host);
memset(service, 0, sizeof service);
debug(&logger, "Received message: %s\n", buffer);
if ((status = getnameinfo((struct sockaddr *)&peer_addr, size, host, sizeof host, service, sizeof service, 0)) != 0) {
fprintf(stderr, "getnameinfo error: %s\n", gai_strerror(status));
exit(1);
}
debug(&logger, "Received message from %d\n", host);
for (i = 0; i < host_count; i++) {
if (host == hosts[i]) pings_received[i] = 1;
}
if (check_pings(pings_received, host_count) == 0) {
fprintf(stderr, "READY\n");
ready = 1;
}
}
The setup for sending is much the same as with receiving. First, fill the structs using getaddrinfo()
. Note that this time you'll want to provide the destination hostname instead of NULL
as the first argument.
if ((status = getaddrinfo(hostname, PORT, &hints, &res)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
exit(1);
}
Next, configure the socket based on the structs from the previous step. You can again iterate through the results, and again you can stop as soon as you find one that works.
// Configure socket
for (p = res; p != NULL; p = p->ai_next) {
if ((sender_fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
perror("peer: socket");
continue;
}
break;
}
To send the actual message, use the sendto()
function, which accepts the socket file descriptor (sender_fd
) from the previous step, the message (ping
in the example below), and some flags and structs.
// send ping
if ((status = sendto(sender_fd, ping, ping_len, 0, p->ai_addr, p->ai_addrlen)) == -1) {
fprintf(stderr, "Error pinging host: %s\n", hosts[i]);
perror("peer: sending");
}
Note that this is not a broadcast, this only sends to the one host
from getaddrinfo()
. If you want to send to multiple hosts, you will need to perform this setup for each host.
By default, the recvfrom()
call is a blocking call. This means that if you try to send and receive messages in the same thread, you risk a deadlock. To avoid this, I recommend one of the following two approaches.
- If you provide
recvfrom()
with theMSG_DONTWAIT
flag instead ofNULL
, it won't block. So whether there is a message or not, you can move on to the sending part of your code. Since, the function is not blocking, it's smart to put a short sleep, so that the CPU isn't blasting at 100% for no reason. - You can create multiple threads: one that sends messages on a loop, and others that each receive messages from one peer (written to one socket).