Network Socket Programming using UDP in C

1.0 Datagram sockets

There are two major types of network sockets, viz. Stream sockets (SOCK_STREAM) and Datagram sockets (SOCK_DGRAM). The socket type defines the semantics of communication between this socket and its remote counterpart. Stream sockets provide full-duplex reliable sequenced data streams. Datagram sockets support connection-less unreliable messages between the source and destination sockets. Datagram sockets use the User Datagram Protocol (UDP) for communication over the network.

2.0 User Datagram Protocol

User Data Protocol (UDP) is a fundamental network protocol for transmission of messages referred to as datagrams. UDP has minimal overheads. There are no acknowledgments and, therefore, delivery is not guaranteed. Datagrams may be delivered out of sequence. However, packets have a checksum and data integrity of a packet is guaranteed. Because of low overheads, UDP is fast and suitable in many situations, particularly in real time systems. For example, if some periodic data is being transmitted, a few lost packets might not matter because similar relevant and more current data is coming next at short intervals.

3.0 System calls

3.1 sendto

#include <sys/types.h> #include <sys/socket.h> ssize_t sendto (int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

The sendto system call is used to send a message to a remote unconnected socket. It can be used for sending a message to a connected socket also, provided the last two parameters are NULL and 0 respectively. The first parameter, sockfd is the file descriptor of the sending socket. The message is at buf and its length is the third parameter, len. The flags would mostly be zero for datagram sockets. The last two parameters are for the destination socket. dest_addr is the destination socket address and addrlen is its length.

sendto returns the number of bytes sent on success. In case of failure, -1 is returned and errno is set accordingly.

3.2 recvfrom

#include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom (int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

The recvfrom system call is for receiving a message from the socket identified by the file descriptor, sockfd. buf is the buffer in which the received message would be returned and len is the length of the buffer. flags are mostly 0 for datagram sockets. recvfrom blocks if there is no message. If non-blocking operation is required, you can specify MSG_DONTWAIT as flags. So, if MSG_DONTWAIT is specified as flags, and there is no message, recvfrom fails with the error EAGAIN or EWOULDBLOCK. The last two parameters are for the source socket. src_addr is a buffer, big enough to receive the socket address of the source socket. The system returns the source socket address in the src_addr buffer. addrlen is a value-result argument. Before the call, addrlen is set to the length of the buffer src_addr. After the call, the system returns the length of the source socket address written in src_addr in addrlen. If the caller does not need the source socket address, the last two parameters can be set to NULL and the system ignores them.

recvfrom returns the number of bytes received on success. In case of error, -1 is returned and errno is set to indicate the error.

For description of other network socket programming system calls and functions, please refer to Network Socket Programming using TCP in C.

4.0 A Client-server example

As an example we have a weather server, which keeps track of weather of cities around the world. To be fair, our weather server is rather simplistic. It only keeps track of temperatures of cities, as of now. There are clients, that report the temperature of cities to the server. Our server records the given temperature of a city. A client may also query the temperature of a city and our server graciously responds with the last recorded temperature value for that city. The client and server communicate using the UDP.

4.1 The server program

/* * server.c: Server program for storing and providing * temperatures of cities around the world. * (Uses UDP for communication with clients) */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <syslog.h> #include <unistd.h> #define SERVER_PORT "4357" #define STORE_TEMPERATURE 1 #define READ_TEMPERATURE 2 #define READ_TEMPERATURE_RESULT 3 #define CITY_NOT_FOUND 4 #define ERROR_IN_INPUT 9 void error (char *msg); struct message { long message_id; char city [100]; char temperature [16]; // degrees Celcius }; struct tnode { char *city; double temperature; // degrees Celcius struct tnode *left; struct tnode *right; }; struct message recv_message, send_message; int sock_fd; socklen_t client_addrlen; struct sockaddr_storage client_addr; struct tnode *add_to_tree (struct tnode *p, char *city, double temperature); struct tnode *find_city_rec (struct tnode *p, char *city); void print_tree (struct tnode *p); void error (char *msg); int main (int argc, char **argv) { const char * const ident = "temperature-server"; openlog (ident, LOG_CONS | LOG_PID | LOG_PERROR, LOG_USER); syslog (LOG_USER | LOG_INFO, "%s", "Hello world!"); struct addrinfo hints; memset (&hints, 0, sizeof (struct addrinfo)); hints.ai_family = AF_UNSPEC; /* allow IPv4 or IPv6 */ hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */ hints.ai_flags = AI_PASSIVE; /* for wildcard IP address */ struct addrinfo *result; int s; if ((s = getaddrinfo (NULL, SERVER_PORT, &hints, &result)) != 0) { fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s)); exit (EXIT_FAILURE); } /* Scan through the list of address structures returned by getaddrinfo. Stop when the the socket and bind calls are successful. */ struct addrinfo *rptr; for (rptr = result; rptr != NULL; rptr = rptr -> ai_next) { sock_fd = socket (rptr -> ai_family, rptr -> ai_socktype, rptr -> ai_protocol); if (sock_fd == -1) continue; if (bind (sock_fd, rptr -> ai_addr, rptr -> ai_addrlen) == 0) // Success break; if (close (sock_fd) == -1) error ("close"); } if (rptr == NULL) { // Not successful with any address fprintf(stderr, "Not able to bind\n"); exit (EXIT_FAILURE); } freeaddrinfo (result); ssize_t numbytes; struct tnode *root = NULL; while (1) { client_addrlen = sizeof (struct sockaddr_storage); if ((numbytes = recvfrom (sock_fd, &recv_message, sizeof (struct message), 0, (struct sockaddr *) &client_addr, &client_addrlen)) == -1) error ("recvfrom"); double temperature; switch (ntohl (recv_message.message_id)) { case STORE_TEMPERATURE : sscanf (recv_message.temperature, "%lf", &temperature); if (strlen (recv_message.city) > 99 || temperature > 99) { send_message.message_id = htonl (ERROR_IN_INPUT); send_message.city [0] = '\0'; send_message.temperature [0] = '\0'; if (sendto (sock_fd, &send_message.message_id, sizeof (struct message), 0, (struct sockaddr *) &client_addr, client_addrlen) == -1) error ("sendto"); } root = add_to_tree (root, recv_message.city, temperature); break; case READ_TEMPERATURE : if (strlen (recv_message.city) > 99) { send_message.message_id = htonl (ERROR_IN_INPUT); send_message.city [0] = '\0'; send_message.temperature [0] = '\0'; if (sendto (sock_fd, &send_message.message_id, sizeof (struct message), 0, (struct sockaddr *) &client_addr, client_addrlen) == -1) error ("sendto"); } struct tnode *tmp = find_city_rec (root, recv_message.city); break; default : fprintf (stderr, "Unknown message\n"); } } syslog (LOG_USER | LOG_INFO, "%s", "Bye."); closelog (); exit (EXIT_SUCCESS); } // record temperature of a city in data structure struct tnode *add_to_tree (struct tnode *p, char *city, double temperature) { int res; if (p == NULL) { // new entry if ((p = (struct tnode *) malloc (sizeof (struct tnode))) == NULL) error ("malloc"); p -> city = strdup (city); p -> temperature = temperature; p -> left = p -> right = NULL; } else if ((res = strcmp (city, p -> city)) == 0) // entry exists p -> temperature = temperature; else if (res < 0) // less than city for this node, put in left subtree p -> left = add_to_tree (p -> left, city, temperature); else // greater than city for this node, put in right subtree p -> right = add_to_tree (p -> right, city, temperature); return p; } // find node for the city whose temperature is queried struct tnode *find_city_rec (struct tnode *p, char *city) { int res; if (p == NULL) { send_message.message_id = htonl (CITY_NOT_FOUND); strcpy (send_message.city, city); send_message.temperature [0] = '\0'; if (sendto (sock_fd, &send_message.message_id, sizeof (struct message), 0, (struct sockaddr *) &client_addr, client_addrlen) == -1) error ("sendto"); return NULL; } else if ((res = strcmp (city, p -> city)) == 0) { // entry exists send_message.message_id = htonl (READ_TEMPERATURE_RESULT); strcpy (send_message.city, p -> city); sprintf (send_message.temperature, "%4.1lf", p -> temperature); if (sendto (sock_fd, &send_message, sizeof (struct message), 0, (struct sockaddr *) &client_addr, client_addrlen) == -1) error ("sendto"); return p; } else if (res < 0) // less than city for this node, search left subtree p -> left = find_city_rec (p -> left, city); else // greater than city for this node, search right subtree p -> right = find_city_rec (p -> right, city); } // print_tree: print the tree (in-order traversal) void print_tree (struct tnode *p) { if (p != NULL) { print_tree (p -> left); printf ("%s: %4.1lf\n\n", p -> city, p -> temperature); print_tree (p -> right); } } void error (char *msg) { perror (msg); exit (1); }

The server keeps a binary tree with record of each city. When a message to store temperature for a city comes, the server checks if there is already a record for that city. If it exists, the server updates the temperature for that city. If there is no record, the server creates a new record for that city. If a client queries the temperature for a city, the server looks for the record for that city. If it finds the record, it responds with the temperature for that city to the client. If there is no record, the server tells the client that it does not have temperature for that city. Note that the server keeps the temperature as double precision floating point in its data structure but text format is used for the same in messages between the clients and the server. There might be differences in floating point representation in different computers and we avoid the problem by transmitting the temperature in text format.

4.2 The client program

/* * client.c: Client program for reporting and querying * temperatures of cities around the world. * (Uses UDP for communication with the server.) * */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <pthread.h> #include <unistd.h> #define SERVER_PORT "4357" #define STORE_TEMPERATURE 1 #define READ_TEMPERATURE 2 #define READ_TEMPERATURE_RESULT 3 #define CITY_NOT_FOUND 4 #define ERROR_IN_INPUT 9 #define QUIT 0 struct message { long message_id; char city [100]; char temperature [16]; // degrees Celcius }; void *client_send (void *arg); void *client_receive (void *arg); int get_option (char *city, char *temperature); void error (char *msg); pthread_t client_send_thread, client_receive_thread; int sock_fd; struct addrinfo *rptr; int main (int argc, char **argv) { if (argc != 2) { fprintf (stderr, "Usage: client hostname\n"); exit (EXIT_FAILURE); } struct addrinfo hints; memset(&hints, 0, sizeof (struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; struct addrinfo *result; int s; if ((s = getaddrinfo (argv [1], SERVER_PORT, &hints, &result)) != 0) { fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s)); exit (EXIT_FAILURE); } /* Scan through the list of address structures returned by getaddrinfo. Stop when the the socket and connect calls are successful. */ socklen_t length; for (rptr = result; rptr != NULL; rptr = rptr -> ai_next) { sock_fd = socket (rptr -> ai_family, rptr -> ai_socktype, rptr -> ai_protocol); if (sock_fd == -1) continue; break; } if (rptr == NULL) { fprintf(stderr, "Not able to make socket\n"); exit (EXIT_FAILURE); } freeaddrinfo (result); // create threads if (pthread_create (&client_receive_thread, NULL, client_receive, NULL) != 0) error ("pthread_create"); if (pthread_create (&client_send_thread, NULL, client_send, NULL) != 0) error ("pthread_create"); // wait for threads if (pthread_join (client_receive_thread, NULL) != 0) error ("pthread_join"); if (pthread_join (client_send_thread, NULL) != 0) error ("pthread_join"); close (sock_fd); exit (EXIT_SUCCESS); } void *client_send (void *arg) { int option; struct message message; while (1) { option = get_option (message.city, message.temperature); if (option == QUIT) break; else if (option == STORE_TEMPERATURE) { message.message_id = htonl (STORE_TEMPERATURE); if (sendto (sock_fd, &message, sizeof (struct message), 0, rptr -> ai_addr, rptr -> ai_addrlen) == -1) error ("sendto"); } else if (option == READ_TEMPERATURE) { message.message_id = htonl (READ_TEMPERATURE); if (sendto (sock_fd, &message, sizeof (struct message), 0, rptr -> ai_addr, rptr -> ai_addrlen) == -1) error ("sendto"); } } if (pthread_cancel (client_receive_thread) != 0) error ("pthread_cancel"); return NULL; } int get_option (char *city, char *temperature) { int option; char buffer [10]; double temp; while (1) { printf ("1: Store temperature\n"); printf ("2: Read temperature\n"); printf ("0: Quit\n\n"); printf ("Your option: "); if (fgets (buffer, 10, stdin) == NULL) error ("fgets"); sscanf (buffer, "%d", &option); if (option == 0) return option; else if (option == 1) { // get city, temperature printf ("City: "); if (fgets (city, 100, stdin) == NULL) error ("fgets"); int len = strlen (city); if (city [len - 1] == '\n') city [len - 1] = '\0'; printf ("Temperature: "); if (fgets (buffer, 10, stdin) == NULL) error ("fgets"); sscanf (buffer, "%lf", &temp); if (temp > 99) { printf ("\nError in temperature, try again\n\n"); continue; } sprintf (temperature, "%4.1lf", temp); return option; } else if (option == 2) { // get city printf ("City: "); if (fgets (city, 100, stdin) == NULL) error ("fgets"); int len = strlen (city); if (city [len - 1] == '\n') city [len - 1] = '\0'; return option; } else printf ("\nInvalid Option, try again\n\n"); } } void *client_receive (void *arg) { struct message message; ssize_t numbytes; while (1) { if ((numbytes = recvfrom (sock_fd, &message, sizeof (struct message), 0, NULL, NULL)) == -1) error ("recvfrom"); switch (ntohl (message.message_id)) { case READ_TEMPERATURE_RESULT: printf ("\n(Receive): City: %s Temperature: %s\n\n", message.city, message.temperature); break; case CITY_NOT_FOUND: printf ("\n(Receive): City: %s, not found.\n\n", message.city); break; case ERROR_IN_INPUT: printf ("\n(Receive): Error in message sent to the server\n\n"); break; default: fprintf (stderr, "\n(Receive): Unexpected message\n\n"); } } } void error (char *msg) { perror (msg); exit (1); }

A client has two threads. One thread is for accepting user commands and sending relevant messages to the server. The second thread waits for messages from the server. As soon as a message comes, it prints it on the output terminal.

We can compile and execute the server and client programs.

$ gcc server.c -o server $ gcc client.c -o client -lpthread $ ./server & [1] 15197 $ temperature-server[15197]: Hello world! $ ./client 192.168.0.103 1: Store temperature 2: Read temperature 0: Quit Your option: 1 City: New Delhi Temperature: 39 1: Store temperature 2: Read temperature 0: Quit Your option: 1 City: Ghaziabad Temperature: 38.5 1: Store temperature 2: Read temperature 0: Quit Your option: 2 City: New Delhi 1: Store temperature 2: Read temperature 0: Quit Your option: (Receive): City: New Delhi Temperature: 39.0 ...

Software: