Intro to UDP Socket Programming in C


Introduction and Context

Last week, we discussed and utilized Transmission Control Protocol (TCP). This week, we'll be discussing the Universal Datagram Protocol, or UDP. Remember that both of these operate on top of the internet protocol (IP) but under other protocols like DNS, HTTP, SSH, etc. For this lab, we'll actually create an application that forwards video packets between two video sources using the RTP protocol on top of UDP.

A couple key points about UDP:

With that said, let's work on creating a simple UDP server in C.

The Essential Server

The first thing you'll notice when writing a UDP server is that there's decidedly fewer steps than with TCP:

  1. Create our socket
  2. Bind it to a port on our computer
  3. Receive messages

To create a socket, we once again use the socket function from the <sys/socket.h> header. This time, we'll use the SOCK_DGRAM constant to create a datagram socket. This is Linux's abstraction of UDP. Just like "stream" meant TCP, "datagram" generally just means UDP.

// my_server.c
#include <sys/socket.h>

int main(){
    int sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(sock_fd <= 0){
        // uh-oh! failed to get a socket and file descriptor
        // the `perror` command can be helpful here
    }
}

As before, we'll need to bind our socket to an address. Remember that this tells the OS that any messages delivered to that address should be transferred to our server program's socket buffer. This is different than, say, the char[] or uint8_t[] you define inside your program.

Also, while TCP and UDP share the same general notation for ports, TCP ports and UDP ports are in fact separate. That is to say, for example, tcp/8080 and udp/8080 can be used simultaneously by two different programs on the same system.

That said, everyything here looks the same as with TCP. We once again use INADDR_ANY to bind to all available interfaces on the system.

// my_server.c
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 4890

int main(){
    int sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(sock_fd <= 0){
        // uh-oh! failed to get a socket and file descriptor
        // the `perror` command can be helpful here
    }

    // build the socket address we'll listen on
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if(bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        // important! if we fail to bind, it's good to close the socket and exit
        close(sock_fd);
        exit(1);
    }
}

Now we have a socket bound to an address. The next step is to open for business! With TCP, we'd use the read and write functions, which are respectively equivalent to the recv and send functions when no options are set. This week, with UDP, we'll instead use recvfrom and sendto.

Let's begin with receiving. The way recvfrom works is by us telling the socket we'd like to move the most recent length bits from the socket into our own program's buffer. The last two arguments are mostly for recvfrom to tell us information. We supply a pointer to a sockaddr and the length of that struct, then recvfrom will fill out both parameters with the sockaddr of the client and the length of that sockaddr. This lets us know what address and port the client is sending from; it also lets us know where to send any responses back to. Finally, recvfrom returns the number of bytes actually read from the socket (for cases where we ask to read, say, 12 bytes but only get 6). If there's an error receiving, recvfrom will return -1 (hence why we use ssize_t and not size_t).

// my_server.c
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 4890
#define BUFFER_SIZE 4096 // we'll just use a buffer of 4096 bytes
int main(){
    int sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(sock_fd <= 0){
        // uh-oh! failed to get a socket and file descriptor
        // the `perror` command can be helpful here
    }

    // build the socket address we'll listen on
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if(bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        // important! if we fail to bind, it's good to close the socket and exit
        close(sock_fd);
        exit(1);
    }

    // prepare the variables for client address and length
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    // and our buffer and read size
    char buffer[BUFFER_SIZE];
    ssize_t num_read_bytes;

    // receive!
    num_read_bytes = recvfrom(sock_fd,
                              buffer,
                              BUFFER_SIZE - 1,
                              0,
                              (struct sockaddr*)&client_addr, // (this is just a cast)
                              &client_addr_len);
    if(num_read_bytes < 0) {
        // error! close and exit the socket, and maybe perror, too!
    }

    // null-terminate and print, assuming what we received is a string...
    buffer[num_read_bytes] = '\0';
    printf("Received: %s", buffer);
}

The Essential UDP Client

Right now, nobody is talking to our server. Let's fix that and make our own UDP client! The good news is that the client requires less setup than the server. In fact, we can begin by reusing the server code and stripping away binding (since we often don't care what address we send from) and receiving. While we're at it, we'll change the server_addr to 127.0.0.1, or localhost, so that our client tries to connect to the server running on our own machine.

The new part here is sendto. The sendto function takes the socket we're sending from, the message, the length of the message, flags (we don't need any here), the destination sock_addr, and the length of that sock_addr struct. We'll hardcode a message for this example. Similar to recvfrom, sendto returns the number of bytes sent or -1 in case of an error.

// my_client.c
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 4890 // the port that the server lives on
#define BUFFER_SIZE 4096 // we'll just use a buffer of 4096 bytes
#define MESSAGE "Hello, server!"
int main()
{
    int sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock_fd <= 0)
    {
        // uh-oh! failed to get a socket and file descriptor
        // the `perror` command can be helpful here
    }

    // build the socket address for the server we'll talk to
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(SERVER_PORT);

    // send a message!
    int bytes_sent = sendto(sock_fd,
                            MESSAGE,
                            strlen(MESSAGE),
                            0,
                            (struct sockaddr *)&server_addr,
                            sizeof(server_addr));

    if (bytes_sent < 0)
    {
        // error! use perror or errno to decipher why
    }
}

}

Ground Control to Major Tom!

It's finally time to use both our server and our client together! Let's first compile them both:

$ gcc /path/to/server.c -o server
$ gcc /path/to/client.c -o client

And then start up the server:

$ ./server

Next, we'll run the client (you can use another terminal or run the server in the background if you'd like).

$ ./client

If all goes well, we should see the following message printed by our server:

Received: Hello, server!

Next Steps

And there we have it, a simple socket server and client in C. In all practicality, we'd like to add a lot more to each of these programs. First, it may be helpful to add some error and info messages so we know when our server has completed each step (getting the socket, binding, listening, etc.), what errors occur and why (use perror for this), etc.

We also should make sure our server responds to each request appropriately. Try using sendto from the server after receiving a message to send a response back to the client. Remember that recvfrom tells us the address of the sender, so we can use that information for the reponse. Remember that the client also will need to receive this message using recvfrom, this is nearly identical to the server's recvfrom. You may be wondering, "How does Linux know to route that incoming packet to our program when we never used bind?!" The answer is pretty straightforward: when we use that unbound socket for the first time, Linux will automatically assigne an interface and port to that socket, then remember that information.

Once you have one full round-trip of communication done, you'll want to make sure your server stays open and willing to receive requests. A while loop works great here! Best practice would also include handling signals (such as those emitted by ctrl+c) so that the program closes the socket and exits cleanly. If not, sometimes Linux may see your port as still in use. This can cause issues when restarting the program. This signal handling isn't required for the lab, but is a challenge for the extra-curious!

Good luck!