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:
- It's very lightweight. The only contents in the UDP header are addresses,
protocol bits (set to
0x11
for UDP), a payload length, and an optional checksum. - It asks very little of the client and server. No handshaking is required.
- Nothing is guaranteed. If a packet is dropped, the receiver will not know it's missing unless the application itself implements a method of noticing.
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:
- Create our socket
- Bind it to a port on our computer
- 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
int
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
int
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
int
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
int
}
Ground Control to Major Tom!
It's finally time to use both our server and our client together! Let's first compile them both:
And then start up the server:
Next, we'll run the client (you can use another terminal or run the server in the background if you'd like).
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!