Simple UDP Server and Client

Introduction

What is a Datagram Socket?

Creating a datagram socket is similar to creating a stream socket.

The main difference is that datagram sockets use UDP instead of TCP, which means that the client and server do not need to establish a connection before sending data.

This is useful for applications that do not need to maintain a connection beacuse the client and the server can send and receive data without having to establish a connection first.

For example, the DNS server uses datagram sockets to send and receive DNS queries and responses.

image

Source: UDP Server-Client implementation in C

Server - Listener

How Listener Works

The listener works by creating a socket, binding it to a port, and waiting for incoming messages.

  1. The listener creates a struct addrinfo called hints. This struct is used to specify what kind of address the listener wants to use. In this case, the listener wants to use IPv6 and UDP.

  2. The listener calls the getaddrinfo function to get a list of addresses that match the hints struct. The listener then loops through the list of addresses and tries to create a socket for each one. If the socket creation succeeds, the listener breaks out of the loop.

  3. The listener calls the bind function to bind the socket to the specified port. This allows the listener to receive messages sent to that port.

  4. Once the socket is bound, the program will listen for incoming messages from the recvfrom function. This recvfrom function will blocks the program until a message is received, and then stores the message in the buf buffer and the sender’s address information in their_addr.

  5. The program prints the message and the sender’s IP address to the console.

  6. The program will then close the socket and exit.

Listener Code Example

/*
** listener.c -- a datagram sockets "server" demo
*/

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

#define MYPORT "4950" // the port users will be connecting to

#define MAXBUFLEN 100 // max number of bytes we can get at once

// get IP address from sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
	// check if IPv4 or IPv6
	if (sa->sa_family == AF_INET)
	{
		// return IPv4 address
		return &(((struct sockaddr_in *)sa)->sin_addr);
	}
	// return IPv6 address
	return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}

// listener main function
int main(void)
{
	int sockfd;							// socket file descriptor
	struct addrinfo hints;				// hints for getaddrinfo(), to specify what kind of address we want
	struct addrinfo *servinfo;			// results from getaddrinfo()
	struct addrinfo *p;					// used to loop through results
	int rv;								// return value from getaddrinfo(), used to check for errors
	int numbytes;						// number of bytes received
	struct sockaddr_storage their_addr; // connector's address information
	char buf[MAXBUFLEN];				// buffer to store received data
	socklen_t addr_len;					// length of connector's address
	char s[INET6_ADDRSTRLEN];			// string to store IP address

	memset(&hints, 0, sizeof hints); // initialize hints to 0
	hints.ai_family = AF_INET6;		 // set to AF_INET6 to use IPv6
	hints.ai_socktype = SOCK_DGRAM;	 // set to SOCK_DGRAM to use UDP
	hints.ai_flags = AI_PASSIVE;	 // use my IP

	// get address information and store in servinfo
	if ((rv = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0)
	{
		// gai_strerror() returns a string describing the error
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
		return 1;
	}

	// loop through all the results and bind to the first we can
	for (p = servinfo; p != NULL; p = p->ai_next)
	{
		// create socket
		if ((sockfd = socket(p->ai_family, p->ai_socktype,
							 p->ai_protocol)) == -1)
		{
			perror("listener: socket");
			continue;
		}

		// bind socket to port
		if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1)
		{
			close(sockfd);
			perror("listener: bind");
			continue;
		}

		break;
	}

	// check if p is NULL, if so, no address was bound
	if (p == NULL)
	{
		fprintf(stderr, "listener: failed to bind socket\n");
		return 2;
	}

	// free servinfo
	freeaddrinfo(servinfo);

	printf("listener: waiting to recvfrom...\n");

	// addr_len is used to store the size of the address
	addr_len = sizeof their_addr;
	if ((numbytes = recvfrom(sockfd, buf, MAXBUFLEN - 1, 0,
						Improved	 (struct sockaddr *)&their_addr, &addr_len)) == -1)
	{
		perror("recvfrom");
		exit(1);
	}

	// inet_ntop() converts IP address to string
	printf("listener: got packet from %s\n",
		   inet_ntop(their_addr.ss_family,
					 get_in_addr((struct sockaddr *)&their_addr),
					 s, sizeof s));

	// print number of bytes received and the data
	printf("listener: packet is %d bytes long\n", numbytes);

	// add null terminator to end of string
	buf[numbytes] = '\0';

	// print received data
	printf("listener: packet contains \"%s\"\n", buf);

	// close socket
	close(sockfd);

	return 0;
}

Client - Talker

How Talker Works

The talker works by setting up a socket, sending a message to the listener, and then closing the socket.

  1. The talker first creates a struct addrinfo called hints, which is used to specify what kind of address we want. The talker sets the ai_family field to AF_INET6 to use IPv6, the ai_socktype field to SOCK_DGRAM to use UDP, and the ai_flags field to AI_PASSIVE to use my IP.

  2. The talker then calls the getaddrinfo() function to get a list of addresses that match the criteria specified in the hints struct. The talker then loops through the list of addresses and tries to create a socket using the socket() function. If the socket is successfully created, the talker breaks out of the loop.

  3. After the socket established, the talker calls the sendto() function to send the message though the socket,and returns the number of bytes sent if successful, -1 if an error occurred.

  4. Finally, the talker closes the socket using the close() function and exits the program.

Talker Code Example

/*
** talker.c -- a datagram "client" demo
*/

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

#define SERVERPORT "4950" // the port users will be connecting to

int main(int argc, char *argv[])
{
	int sockfd;				   // socket descriptor
	struct addrinfo hints;	   // hints for getaddrinfo(), to specify what kind of address we want
	struct addrinfo *servinfo; // servinfo will point to the results from getaddrinfo()
	struct addrinfo *p;		   // used to loop through all the results and connect to the first we can
	int rv;					   // rv will store the return value from getaddrinfo(), used to check if there is an error
	int numbytes;			   // numbytes will store the number of bytes sent

	// check if the number of arguments is correct
	if (argc != 3)
	{
		fprintf(stderr, "usage: talker hostname message\n");
		exit(1);
	}

	memset(&hints, 0, sizeof hints); // initialize hints to 0
	hints.ai_family = AF_INET6;		 // set to AF_INET6 to use IPv6
	hints.ai_socktype = SOCK_DGRAM;	 // set to SOCK_DGRAM to use UDP

	// get address information and store in servinfo
	if ((rv = getaddrinfo(argv[1], SERVERPORT, &hints, &servinfo)) != 0)
	{
		// gai_strerror() returns a string describing the error
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
		return 1;
	}

	// loop through all the results and make a socket
	for (p = servinfo; p != NULL; p = p->ai_next)
	{
		// create a socket
		if ((sockfd = socket(p->ai_family, p->ai_socktype,
							 p->ai_protocol)) == -1)
		{
			perror("talker: socket");
			continue;
		}

		break;
	}

	// if p is NULL, it means that we have looped through all the results and none of them worked
	if (p == NULL)
	{
		fprintf(stderr, "talker: failed to create socket\n");
		return 2;
	}

	// send the message
	if ((numbytes = sendto(sockfd, argv[2], strlen(argv[2]), 0,
						   p->ai_addr, p->ai_addrlen)) == -1)
	{
		perror("talker: sendto");
		exit(1);
	}

	// free the servinfo linked list
	freeaddrinfo(servinfo);

	// print the number of bytes sent
	printf("talker: sent %d bytes to %s\n", numbytes, argv[1]);

	// close the socket
	close(sockfd);

	return 0;
}

Compiling and Running the Code

Compiling

gcc -o talker talker.c
gcc -o listener listener.c

Running and output

./listener           
listener: waiting to recvfrom...
# after running talker
listener: got packet from ::1
listener: packet is 5 bytes long
listener: packet contains "hello"

# In another terminal
./talker localhost hello 
talker: sent 5 bytes to localhost

Conclusion

Summary

Run the listener program first, then run the talker program. The listener program will wait for a message from the talker program. When the talker program is run, it will send a message to the listener program, which will print the message.

If no listener program is running, the talker program will sned the message and then exit. Those messages will be lost, since there is no program to receive them.

The UDP protocol is a simple, lightweight protocol that is easy to use. However, it does not provide any error checking or recovery and not guaranteed to deliver messages to the destination, so it is not suitable for applications that require reliable communication.

References

  1. Beej’s Guide to Network Programming Using Internet Sockets

  2. UDP Server-Client implementation in C


Last modified on 2023-01-16


Thank you for your support!