An Introduction to Network Programming in UNIX
These notes aim at giving a brief introduction to building network applications in the UNIX environment. Most of the concepts described in these notes are taken from UNIX Network Programming: The Sockets Networking API (Vol 1, 3rd Ed., W. Stevens, B. Fenner and A. Rudoff, Addison-Wesley, 2004). UNIX refers to a family of operating systems. In general, you can think of the operating system as the software that controls the hardware resources of the computer providing the environment under which programs can run. At its core, the operating system includes the kernel whose main role is to interact with the hardware to provide the necessary abstractions for processes, file systems, memory, etc. The kernel offers its functionalities to applications via a software layer called system calls. Therefore, many network related functions are available in the form of system calls. On UNIX systems, each system call has a corresponding C function that is provided as part of the standard C library (so you don’t need to link a separate library). In practice, to use some specific networking functions, one only needs to include the specific header files that declare those functions. The main objective of this document is to provide an introduction to some of the elementary network-related functions available on most UNIX systems.
Socket Address Structures
A socket address identifies a communication end-point. Typically, that means an application, or one of potentially many end-points used by an application. We now describes some of the structures, as well as the related functions needed to represent and process socket addresses. For our purposes, we only cover socket addresses for IPv4 and IPv6. However, there are many more types of addresses for the various types of communication mechanisms.
UNIX network functions are by design independent of the type of
communication mechanisms they are based on. Most network functions
take a pointer to a struct sockaddr, which is as generic socket
address structure. You can think of it as a super-class for all
socket address structures. struct sockaddr is defined in the
sys/socket.h header as follows:
struct sockaddr { sa_family_t sa_family; /* Address family */ char sa_data[]; /* Protocol-dependent address */ };
The sa_family field refers to the “protocol family” of the socket,
meaning the type of its underlying communication mechanism. For
instance, AF_INET identifies the IPv4 family of protocols, while
AF_INET6 identifies IPv6.
An IPv4 socket address is represented by a struct sockaddr_in
object, declared in netinet/in.h as follows:
typedef uint32_t in_addr_t; typedef uint16_t in_port_t; struct in_addr { in_addr_t s_addr; /* The IPv4 address in network byte order */ }; struct sockaddr_in { sa_family_t sin_family; /* AF_INET */ in_port_t sin_port; /* The port number for TCP or UDP in network byte order */ struct in_addr sin_addr; /* IPv4 address */ };
An IPv6 socket address is represented by a struct sockaddr_in6
object declared in netinet/in.h as follows:
typedef uint16_t in_port_t; struct in6_addr { uint8_t s6_addr[16]; /* The IPv6 address in network byte order */ }; struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* The port number for TCP or UDP in network byte order */ struct in6_addr sin6_addr; /* IPv6 address */ };
Notice that, for both structures, we show only the basic, mandatory
fields that are relevant for the implementation of basic network
applications. The actual declaration may contain additional fields.
In both address structures, the numeric values are stored in memory in
network byte order, which corresponds to the big-endian representation
of numbers. The host, CPU-specific byte order might be different. In
general, the arpa/inet.h header defines the following functions to
convert a number from host byte order to network byte order, and
vice-versa:
uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
The htonl and htons functions convert a number from host byte
order to network byte order, and the ntohl and ntohs functions
convert a number from network byte order to host byte order.
Finally, we describe a group of functions that convert between the
numeric (memory) and string representations of IP addresses. These
functions are also defined in the arpa/inet.h header, as follows:
int inet_pton(int af, const char *src, void *dst); const char *inet_ntop(int af, const void * src, char *dst, socklen_t size);
The inet_pton function converts from the string representation of an
IP address to its numeric representation. If the conversion is
successful, inet_pton returns 1. The af argument indicates the
address family, so the only possible values are either AF_INET or
AF_INET6. On success, dst will contain the numeric value of the IP
address given in src, so dst should point to a struct in_addr or
a struct in6_addr for IPv4 or IPv6 addresses, respectively.
Similarly, inet_ntop converts form the numeric representation of an
IP address to its string representation. On success, inet_ntop
returns a pointer to dst. The string representation will be
contained in dst, and the size parameter is needed to avoid
overflowing the caller’s buffer. The netinet/in.h header also
contains the following two definitions that allow the application to
declare large-enough buffers to hold IPv4 or IPv6 addresses:
#define INET_ADDRSTRLEN 16 /* The maximum length of an IPv4 address in decimal dotted notation */ #define INET6_ADDRSTRLEN 46 /* The maximum length of an IPv6 address in hex */
TCP Sockets
Now that we know how to define IPv4 and IPv6 addresses and also port numbers to identify applications within a host, we turn to using these addresses to then send and receive information through the network. We do that with an example. In particular, we will develop a simple “echo” server and client. An echo client reads a line of text from standard input, and sends it to the echo server. The echo server reads the line from the client, and echoes it back to the client. Upon receiving the echoed line back, the client will display it to its standard output. For both the echo server and client, we will use TCP as a communication protocol.
Whether you use TCP or UDP—and in fact even for non-IP
networks—the communication happens through objects called sockets.
The application can create such objects with the socket function
declared in sys/socket.h as follows:
int socket(int domain, int type, int protocol);
The socket function returns the socket in the form of a file
descriptor. A file descriptor is a non-negative number used to
identify a file. In UNIX systems, almost everything is a file, so also
a socket is a type of file. On success, the function returns a
non-negative number corresponding to the file descriptor identifying
the socket. On error, the function returns -1. (Recall that file
descriptors are non-negative numbers, so a negative number can be
used to notify the caller about an error.) The domain argument
refers to the protocol family, that is, AF_INET, AF_INET6, etc.
The type argument refers to the communication semantics. The
semantics we will consider in this document are “stream” and
“datagram.” A stream socket establishes a reliable, full duplex data
stream between two end-points. A full duplex stream between \(A\) and
\(B\) consists in fact of two completely independent streams: one
carries a sequence of bytes from \(A\) to \(B\), and the other one carries a
sequence of bytes from \(B\) to \(A\). In practice, on most platforms, a
stream socket uses TCP. A datagram socket instead provides
message-oriented communication with only minimal reliability
guarantees, and it is most often based on UDP. Note that not all
socket domain and type combinations are valid. Finally, the
protocol refers to the particular protocol that should be used for
the socket. Usually, only a single protocol exists to support a
particular socket type within a given protocol family, so we will be
be setting it to 0 in our examples to get the default value.
For both TCP and UDP sockets, the socket object must be set up with the addresses of its two end-points. This can be done automatically by the operating system or explicitly by your application. For a server socket, meaning a socket used to receive UDP messages or to accept incoming TCP connections, you definitely want to set the address of the local end-point, since that is the address that your server socket will be reachable at. Then, when you use the socket to receive messages (UDP) or to accept a connection (TCP), the remote address will be automatically set by the operating system to report the address of the actual source of those messages or connections.
Conversely, for a client socket—meaning a socket used to send UDP messages or to create a TCP connection through which you can then send and receive data—you have control of both the local and remote address. You most definitely want to set the address of the remote end-point, and for the local end-point, you can choose one yourself or you can let the operating system do that for you.
You can set the local end-point address of a socket using the bind
function declared in sys/socket.h as follows:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
The bind function returns 0 on success, and -1 on error. The
sockfd argument refers to the socket we want to bind the address
to. The addr argument is a pointer to the socket address structure
containing the address we want to assign to the socket. The addrlen
argument is simply the length in bytes of the structure pointed by
addr. Note that you are not forced to call bind. In case of not
calling bind, the OS will assign an address to the socket for
you. In fact, most client applications do not call bind.
The remaining steps for a TCP application differs for a server and a
client. We will start our discussion with the server side of the
application. Usually, a TCP server waits for connections and serves
them. The listen function defined as follows in the sys/socket.h
header allows a TCP server to start listening for incoming connection
from the clients with a maximum number of connection that the kernel
should queue:
int listen(int sockfd, int backlog);
The listen function returns 0 on success, and -1 on error. The
sockfd argument refers to the server socket, and the backlog
argument defines the maximum number of pending connection that the
kernel should queue. The kernel maintains two queues for a TCP socket:
a queue containing incomplete connections (the server only received a
SYN segment), and a queue containing completed connections (the
three-way handshake was completed). The backlog argument represents
the maximum length for the sum of the length of both queues.
Finally, a server can accept a connecting client using the accept
function declared in the sys/socket.h header as follows:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
The accept function returns a file descriptor identifying the socket
that the server can use to communicate with the client. On failure,
accept returns a negative number. The addr and addrlen
arguments are output parameters. In fact, upon a successful connection
from a client, they will contain respectively the address structure
and the length for the connecting client. More specifically,
addrlen is an input/output parameter. That is, the initial value
(in the integer object pointed by addrlen) is the maximal size of
the socket address structure pointed by addr, so accept will not
write any address longer than that value. Then, accept will use
addrlen to return to the caller the actual length of the address
stored (by accept) in addr.
Since a socket is a file descriptor, we can then use the usual I/O
functions to communicate. Namely, we can receive and send data from
and to an open socket using the read and write functions,
respectively. These two functions are declared in unistd.h as
follows:
ssize_t read(int fd, void *buf, size_t nbytes); ssize_t write(int fd, const void *buf, size_t nbytes);
Both functions return an ssize_t value to indicate the number of
bytes that were actually transferred (received or sent). In case of
error, read and write return -1. For both read and write, the
fd argument identifies the sending or receiving socket. The use of
the buf and nbytes arguments differ slightly for the two
functions. The read function uses buf as the destination for the
bytes received, and nbytes to indicate the maximum amount of bytes
to receive and copy into buf from the socket. Conversely, write
uses buf as a source for the data to be sent, and nbytes to
indicate the number of bytes to be sent from buf onto the socket.
Notice that both read and write operations may transfer less than
nbytes bytes, in which case, the return value of read and write
will be less than nbytes. For example, a read operation may return
less than nbytes simply because the receiver has not received
nbytes from the sender. However, in general, there is no guarantee
that read or write operations would transfer the requested amount of
data even if that amount of data is actually available. It is
therefore the responsibility of the programmer to keep track of the
progress made in reading and writing.
As usual, a program that acquires a resource must also properly
release it. For sockets, this means closing them. An open TCP
connection can be closed with the close function, declared in the
unistd.h header as follows:
int close(int fd);
The return value is 0 on success, and -1 on error. The fd argument
is the file descriptor identifying the socket.
The following code implements a simple TCP “echo” server:
#include <string.h> #include <sys/socket.h> /* For struct sockaddr, socket, accept, listen, bind */ #include <netinet/in.h> /* For struct sockaddr_in */ #include <arpa/inet.h> /* For htons, htonl, inet_pton */ #include <stdlib.h> #include <stdio.h> #include <errno.h> /* For errno */ #include <stdint.h> #include <unistd.h> /* For close, read, write */ /* * Defining a utility function to write exactly n bytes on a file * descriptor. */ ssize_t writen(int fd, const void *buf, size_t n) { size_t nleft = n; const char *ptr = buf; ssize_t nwritten; while (nleft > 0) { if ((nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; ptr += nwritten; } return n; } int main(int argc, char *argv[]) { /* * Declare the socket address structure which will use for the * bind call on the the echo server socket. Also, declare a * socket address structure that will contain the address of a * connecting client. */ struct sockaddr_in servaddr, addr; socklen_t len; char addr_buf[INET_ADDRSTRLEN]; char buf[1024]; const uint16_t port = 5000; int n; /* Create the TCP socket for the server. */ int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("failed to create server socket"); return EXIT_FAILURE; } memset(&servaddr, 0, sizeof(servaddr)); /* Since the address is an IPv4, set the address family to AF_INET */ servaddr.sin_family = AF_INET; /* * We want to accept connections incoming on any interface, so * using INADDR_ANY which is the equivalent of binding on 0.0.0.0. * INADDR_ANY is in host byte hoder, and we have to conver it to * network byte order. We use htonl since it is a 32 bit number. */ servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* * The port number is in host byte order, so convert it to network * byte order using htons. We use htons since it is a 16 bit * number. */ servaddr.sin_port = htons(port); /* Bind the socket to the given address. */ if (bind(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) { perror("failed to bind socket"); goto error_handling; } /* Put the server socket in listening state. */ if (listen(sockfd, 128) < 0) { perror("failed to bind socket"); goto error_handling; } /* Sit and wait for incoming connections from the clients */ while (1) { len = sizeof(addr); /* * Accept a client connection and start serving it. The accept * function is blocking. Meaning that the execution will block * here until a client connects. */ int clientfd = accept(sockfd, (struct sockaddr *) &addr, &len); if (clientfd < 0) goto error_handling; /* Convert the address of the client to a string */ if (inet_ntop(AF_INET, &addr.sin_addr, addr_buf, sizeof(addr_buf)) == NULL) { perror("failed to get client address"); close(clientfd); continue; } /* * The port of the client is in network byte order, so convert * it to host byte order before printing it. */ printf("connection from %s:%d\n", addr_buf, ntohs(addr.sin_port)); /* Receive some data from the client. */ while ((n = read(clientfd, buf, sizeof(buf))) != 0) { if (n < 0) { if (errno == EINTR) continue; perror("failed to receive from client"); break; } /* Echo the data received back to the client. */ if (writen(clientfd, buf, n) != n) { perror("failed to send data to the client"); break; } } /* Close the client connection once done with this client. */ close(clientfd); } close(sockfd); return EXIT_SUCCESS; error_handling: close(sockfd); return EXIT_FAILURE; }
For a client the steps are a bit different. Usually, a client connects
to a server to send its requests. The connect function, declared as
follows in sys/socket.h, allows a TCP client to establish a TCP
connection with a server:
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
The connect function returns 0 on success, and -1 on error. The
sockfd argument refers to the client socket.
The servaddr argument is a pointer to the socket address structure
containing the address of the TCP server. The addrlen
argument is simply the length in bytes of the structure pointed by
servaddr.
The following code implements a simple TCP echo client:
#include <sys/socket.h> /* For struct sockaddr, socket, connect */ #include <netinet/in.h> /* For struct sockaddr_in */ #include <arpa/inet.h> /* For htons, htonl, inet_pton */ #include <stdlib.h> #include <stdio.h> #include <errno.h> /* For errno */ #include <stdint.h> #include <sys/types.h> #include <unistd.h> /* For close, read, write */ /* * Defining a utility function to read exactly n bytes from a file * descriptor. */ ssize_t readn(int fd, void *buf, size_t n) { size_t nleft = n; ssize_t nread; char *ptr = buf; while (nleft > 0) { if ((nread = read(fd, ptr, nleft)) < 0) { if (errno == EINTR) continue; else return -1; } else if (nread == 0) break; nleft -= nread; ptr += nread; } return n-nleft; } /* * Defining a utility function to write exactly n bytes on a file * descriptor. */ ssize_t writen(int fd, const void *buf, size_t n) { size_t nleft = n; const char *ptr = buf; ssize_t nwritten; while (nleft > 0) { if ((nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; ptr += nwritten; } return n; } int main(int argc, char *argv[]) { /* * Declare the socket address structure which will contain the * address of the echo server. */ struct sockaddr_in servaddr = { 0 }; int n; char line[1024]; if (argc != 3) { fprintf(stderr, "usage: %s server_ip server_port\n", argv[0]); return EXIT_FAILURE; } /* * The first command line argument will contain the IPv4 address * of the echo server. Use inet_pton to obtain the numeric * representation of the server address. */ if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) != 1) { fprintf(stderr, "provide a valid IPv4 address for the server\n"); return EXIT_FAILURE; } /* * The second command line argument will contain the port * number. Convert the port number to its numeric representation * in host byte order. */ long port = strtol(argv[2], NULL, 10); if (errno || port <= 0 || port > UINT16_MAX) { fprintf(stderr, "provide a valid port number\n"); return EXIT_FAILURE; } /* Since the address is an IPv4, set the address family to AF_INET */ servaddr.sin_family = AF_INET; /* * The port number is in host byte order, so convert it to network * byte order using htons. We use htons since it is a 16 bit * number. */ servaddr.sin_port = htons(port); /* Create a socket for the client. */ int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("failed to create socket"); return EXIT_FAILURE; } /* Connect to the server. */ if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) { perror("failed to connect to the server"); goto error_handling; } while (1) { /* Read some data from the user. */ n = read(STDIN_FILENO, line, sizeof(line)); if (n < 0) { if (errno == EINTR) continue; goto error_handling; } else if (n == 0) break; /* Send the data read to the server. */ if (writen(sockfd, line, n) != n) { perror("failed to send data to the server"); goto error_handling; } /* Read the reply from the server. */ if (readn(sockfd, line, n) != n) { fprintf(stderr, "failed to receive all data sent from the server\n"); goto error_handling; } /* Output the reply received from the server. */ if (writen(STDOUT_FILENO, line, n) != n) { perror("failed to write data sent received from the server into stdout"); goto error_handling; } } close(sockfd); return EXIT_SUCCESS; error_handling: close(sockfd); return EXIT_FAILURE; }
The echo server we present as an example above works well for one
client, but can only handle one client at the time. The problem is
that accept, read and write are blocking operations, meaning
that an accept call returns one connection at a time, and the
following read call returns only when data becomes available on that
one connection, and similarly write blocks until some data can be
written onto the connection. However, while the server is suspended
on those read and write operations, other connections might be
available and ready to transfer data. In other words, a single
inactive client would block every other client that might have data to
send and receive.
One solution to this problem is to use I/O multiplexing. The idea of
I/O multiplexing is that the server would “select” one or more file
descriptors for which data is indeed available, and then read from (or
write to) those files without blocking. This can be done with the
select function declared in sys/select.h as follows:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
The select function takes three sets of file descriptors,
readfds for reading, writefds for writing, and exceptfds for
error conditions, plus a timeout interval. The select call blocks
until either data is available from one or more files in readfds,
data can be written into one or more files in readfds, an error
condition arises in one or more files in exceptfds, or the timeout
period elapses.
The return value is the total number of file descriptors ready for I/O
across the provided sets. Upon returning, select also changes the
file-descriptor sets to let the caller know exactly which file
descriptors are ready for their corresponding operations. On error,
the function returns -1. In case of a timeout, which means that no
file descriptor is ready for I/O, the function returns 0.
The nfds argument specifies the range of file descriptors to
monitor. In practice, it is the maximum file descriptor (value) plus
one. The readfds, writefds, and exceptfds arguments are pointers
to sets of file descriptors to monitor for reading, writing, and
exceptional conditions, respectively. These sets are manipulated using
the FD_SET, FD_CLR, FD_ISSET, and FD_ZERO macros. (The
example below illustrates the use of the file descriptors sets and
their macros.) The timeout argument is a pointer to a structure
defining the maximum time to wait for an event. If this parameter is
NULL, select does not use a timeout and therefore may block
indefinitely. The struct timeval is defined in the sys/time.h
header as follows:
struct timeval { long tv_sec; /* Seconds */ long tv_usec; /* Microseconds */ };
I/O multiplexing can also be done via the poll function. The poll
function is conceptually similar to select, and therefore can also
be used to check for the immediate availability of input/output data
from file descriptors. The declaration, which can be included from
poll.h, is as follows:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
The fds parameter points to an array of struct pollfd objects;
nfds is the length of the array. Each struct pollfd object
specifies a file descriptor together with the I/O events of interest
for it. Specifically, struct pollfd is defined as follows:
struct pollfd { int fd; /* File descriptor to monitor */ short int events; /* Events we monitor (POLLIN, POLLERR, etc.) */ short int revents; /* Occurred events (POLLIN, POLLERR, etc.) */ };
The poll functions also takes a timeout parameter that specifies
the maximum time to wait for an event in milliseconds. If timeout
is 0, poll returns immediately. If timeout is -1, poll blocks
indefinitely until an event occurs.
The return value is the total number of file descriptors ready for I/O
in the provided array. Specifically, poll sets the value of
revents for each struct pollfd object to indicate which file is
ready for I/O or shows an error condition. On error, poll returns
-1. In case of a timeout with no file descriptor ready for I/O, the
function returns 0.
Finally, we can use the poll function to implement a concurrent
version of our initial TCP echo server as follows:
#include <string.h> #include <sys/socket.h> /* For struct sockaddr, socket, listen, accept, bind */ #include <netinet/in.h> /* For struct sockaddr_in */ #include <arpa/inet.h> /* For htons, htonl, inet_pton */ #include <stdlib.h> #include <stdio.h> #include <errno.h> /* For errno */ #include <stdint.h> #include <unistd.h> /* For close, read, write */ #include <sys/poll.h> /* For poll, struct pollfs */ /* * Defining a utility function to write exactly n bytes on a file * descriptor. */ ssize_t writen(int fd, const void *buf, size_t n) { size_t nleft = n; const char *ptr = buf; ssize_t nwritten; while (nleft > 0) { if ((nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; ptr += nwritten; } return n; } /* An helper function to close a list of file descriptors. */ void close_all_fds(struct pollfd fds[], size_t count) { for (size_t j = 0; j < count; ++j) if (fds[j].fd >= 0) close(fds[j].fd); } int main(int argc, char *argv[]) { /* * Declare the socket address structure which will use for the * bind call on the the echo server socket. Also, declare a * socket address structure that will contain the address of a * connecting client. */ struct sockaddr_in servaddr, addr; socklen_t len = sizeof(addr); char addr_buf[INET_ADDRSTRLEN]; char buf[1024]; const uint16_t port = 5000; int n; /* Define the list of file descriptors we want to monitor. * FOPEN_MAX is a constant defining the maximum number of * open file descriptors. */ struct pollfd fds[FOPEN_MAX]; /* Create the TCP socket for the server. */ int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("failed to create server socket"); return EXIT_FAILURE; } memset(&servaddr, 0, sizeof(servaddr)); /* Since the address is an IPv4, set the address family to AF_INET */ servaddr.sin_family = AF_INET; /* * We want to accept connections incoming on any interface, so * using INADDR_ANY which is the equivalent of binding on 0.0.0.0. * INADDR_ANY is in host byte hoder, and we have to conver it to * network byte order. We use htonl since it is a 32 bit number. */ servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* * The port number is in host byte order, so convert it to network * byte order using htons. We use htons since it is a 16 bit * number. */ servaddr.sin_port = htons(port); /* Bind the socket to the given address. */ if (bind(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) { perror("failed to bind socket"); goto error_handling; } /* Put the server socket in listening state. */ if (listen(sockfd, 128) < 0) { perror("failed to bind socket"); goto error_handling; } /* * Register the server socket as the first item in the * list to be notified about new connecting clients. */ fds[0].fd = sockfd; fds[0].events = POLLIN; /* We have 1 file decriptor, i.e. the server socker. */ unsigned fds_count = 1; /* Sit and wait for incoming connections from the clients */ while (1) { /* Use the poll function to wait until some * file in the set is ready for I/O. Notice the * -1 argument indicates that we will wait until * some event happens. */ int nready = poll(fds, fds_count, -1); if (nready == -1) { perror("poll failed"); close_all_fds(fds, fds_count); return EXIT_FAILURE; } /* If we reach this point, it means that some file * descriptor in the fds list is ready for I/O. * We start by checking if there is any new connection * on the server socket. We can check if the file descriptor * is ready for I/O by looking at revents. */ if (fds[0].revents & POLLIN && fds_count < FOPEN_MAX) { len = sizeof(addr); /* * Accept a client connection and start serving it. The * accept function is blocking. Meaning that the execution * will block here until a client connects. */ int clientfd = accept(sockfd, (struct sockaddr *) &addr, &len); if (clientfd < 0) { perror("failed to accept client connection"); close_all_fds(fds, fds_count); return EXIT_FAILURE; } /* Convert the address of the client to a string */ if (inet_ntop(AF_INET, &addr.sin_addr, addr_buf, INET_ADDRSTRLEN) == NULL) { perror("failed to get client address"); close(clientfd); } else { /* * The port of the client is in network byte order, so convert * it to host byte order before printing it. */ printf("connection from %s:%d\n", addr_buf, ntohs(addr.sin_port)); fds[fds_count].fd = clientfd; fds[fds_count++].events = POLLIN; if (--nready <= 0) continue; } } else if (fds[0].revents & POLLIN && fds_count == FOPEN_MAX) fprintf(stderr, "too many files open\n"); // Read data from the connected clients for (unsigned i = 1; i < fds_count; ++i) { /* If the file descriptor is ready for I/O handle it. */ if (fds[i].revents & (POLLIN | POLLERR)) { reading: // Simply read data from the client and discard it. if ((n = read(fds[i].fd, buf, sizeof(buf))) < 0) { if (errno == ECONNRESET) { close(fds[i].fd); fds[i] = fds[--fds_count]; } else if (errno == EINTR) { goto reading; } else { close_all_fds(fds, fds_count); perror("failed reading from client"); return EXIT_FAILURE; } } else if (n == 0) { close(fds[i].fd); fds[i] = fds[--fds_count]; } /* Echo the data received back to the client. */ if (writen(fds[i].fd, buf, n) != n) { perror("failed to send data to the client"); close(fds[i].fd); fds[i] = fds[--fds_count]; } if (--nready <= 0) break; } } } close(sockfd); return EXIT_SUCCESS; error_handling: close(sockfd); return EXIT_FAILURE; }
UDP Sockets
This section provides a description about how to write some elementary UDP client and server. Again, we will be using the echo server and client example to show how to write a UDP application.
Compared to the TCP example, there are some fundamental
differences. The main differences are due to UDP being
connection-less, unreliable, and datagram-oriented. The creation of a
UDP socket is similar to the one of a TCP socket. We can create an UDP
socket by invoking the socket function with SOCK_DGRAM as value
for the type argument. However, a UDP server does not need the
listen and accept calls, and a UDP client does not need to invoke
the connect function. UDP is not connection oriented, so there is no
connection to establish. In fact, after creating a UDP socket, and
application can start communicating right away. The two functions
sendto and recvfrom defined in the sys/socket.h header as
follows to send and receive a datagram from a UDP socket:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
These two functions are similar to the read and write functions
respectively. The first three arguments, sockfd, buf, and len,
also have identical semantics to the three arguments of read and
write. The flags argument allow some customization on the
receive/send operations. For our purposes, we can consider the flags
argument to be always equal to 0. The src_addr and addrlen
arguments to the recvfrom function are output parameters which will
contain the socket address of the sender (you can think of them as the
addr and addrlen arguments of the accept function). The
dest_addr and addrlen arguments to the sendto function contain
the socket address of the destination for the given data.
Given an initial introduce about the main UDP functions, we can implement the UDP echo server as follows:
#include <string.h> #include <sys/socket.h> /* For struct sockaddr, socket, sendto, recvfrom */ #include <netinet/in.h> /* For struct sockaddr_in */ #include <arpa/inet.h> /* For htons, htonl, inet_pton */ #include <stdlib.h> #include <stdio.h> #include <stdint.h> #include <unistd.h> /* For close */ int main(int argc, char *argv[]) { /* * Declare the socket address structure which will use for the * bind call on the the echo server socket. Also, declare a * socket address structure that will contain the address of a * sending client. */ struct sockaddr_in servaddr, addr; char buf[1024]; socklen_t len; const uint16_t port = 5000; int n; /* Create the UDP socket for the server. */ int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("failed to create server socket"); return EXIT_FAILURE; } memset(&servaddr, 0, sizeof(servaddr)); /* Since the address is an IPv4, set the address family to AF_INET */ servaddr.sin_family = AF_INET; /* * We want to accept connections incoming on any interface, so * using INADDR_ANY which is the equivalent of binding on 0.0.0.0. * INADDR_ANY is in host byte hoder, and we have to conver it to * network byte order. We use htonl since it is a 32 bit number. */ servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* * The port number is in host byte order, so convert it to network * byte order using htons. We use htons since it is a 16 bit * number. */ servaddr.sin_port = htons(port); /* Bind the socket to the given address. */ if (bind(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) { perror("failed to bind socket"); goto error_handling; } /* Sit and wait for incoming connections from the clients */ while (1) { len = sizeof(addr); /* * UDP does not have the concept of connection. Therefore, we * can simply receive a datagram from a client. Note that * recvfrom can also return 0. In that case, it would be a * datagram with length 0, and it will not indicate that the * client has finished sending. In UDP there is no connection, * so how would you know when the client has finished sending? */ n = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len); if (n < 0) { perror("failed to receive data from client"); goto error_handling; } /* * Once we receive some datagram from a client we can simply * echo back the data we just received. */ if (sendto(sockfd, buf, n, 0, (struct sockaddr *) &addr, len) < 0) { perror("failed to send data to client"); goto error_handling; } } close(sockfd); return EXIT_SUCCESS; error_handling: close(sockfd); return EXIT_FAILURE; }
The following code presents the implementation for the UDP echo client:
#include <sys/poll.h> #include <sys/socket.h> /* For struct sockaddr, socket, sendto */ #include <netinet/in.h> /* For struct sockaddr_in */ #include <arpa/inet.h> /* For htons, htonl, inet_pton */ #include <stdlib.h> #include <stdio.h> #include <errno.h> /* For errno */ #include <stdint.h> #include <sys/types.h> #include <unistd.h> /* For close, read, write */ /* Defining a utility function to write exactly n bytes on a file * descriptor. */ ssize_t writen(int fd, const void *buf, size_t n) { size_t nleft = n; const char *ptr = buf; ssize_t nwritten; while (nleft > 0) { if ((nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; ptr += nwritten; } return n; } int main(int argc, char *argv[]) { /* Declare the socket address structure that will contain the * address of the echo server. */ struct sockaddr_in servaddr = { 0 }; socklen_t len = sizeof(servaddr); int n; char line[1024]; if (argc != 3) { fprintf(stderr, "usage: %s server_ip server_port\n", argv[0]); return EXIT_FAILURE; } /* The first command line argument will contain the IPv4 address * of the echo server. Use inet_pton to obtain the numeric * representation of the server address. */ if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) != 1) { fprintf(stderr, "provide a valid IPv4 address for the server\n"); return EXIT_FAILURE; } /* The second command line argument will contain the port * number. Convert the port number to its numeric representation * in host byte order. */ long port = strtol(argv[2], NULL, 10); if (errno || port <= 0 || port > UINT16_MAX) { fprintf(stderr, "provide a valid port number\n"); return EXIT_FAILURE; } /* Since we have an IPv4 address, set the address family to AF_INET */ servaddr.sin_family = AF_INET; /* The port number is in host byte order, so convert it to network * byte order using htons. We use `htons' ("host to network * short") because the port number is a 16-bit number. */ servaddr.sin_port = htons(port); /* Create a UDP socket for the client. */ int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("failed to create socket"); return EXIT_FAILURE; } /* Define the list of file descriptors we want to monitor. In * particular, we want to monitor the standard input of the client to detect * when the user inputs some data, and we want to monitor the file * descriptor of the UDP socket to detect when we receive some * data from the server. */ struct pollfd fds[2]; fds[0].fd = STDIN_FILENO; fds[0].events = POLLIN; fds[1].fd = sockfd; fds[1].events = POLLIN; while (1) { if (poll(fds, 2, -1) == -1) goto error_handling; /* Check if we received some data from the server. */ if (fds[1].revents & POLLIN) { /* Receive a datagram from the server */ n = recvfrom(sockfd, line, sizeof(line), 0, NULL, NULL); if (n < 0) goto error_handling; /* Write the data received from the server into stdout */ if (writen(STDOUT_FILENO, line, n) != n) { perror("failed to write data sent received from the server into stdout"); goto error_handling; } } /* Checking the user has inputted some data */ if (fds[0].revents & POLLIN) { n = read(STDIN_FILENO, line, sizeof(line)); if (n < 0) { if (errno == EINTR) continue; goto error_handling; } else if (n == 0) break; /* Send the data received from the user to the server. */ if (sendto(sockfd, line, n, 0, (struct sockaddr *) &servaddr, len) < 0) { perror("failed to send data to client"); goto error_handling; } } } close(sockfd); return EXIT_SUCCESS; error_handling: close(sockfd); return EXIT_FAILURE; }
Name/Address Conversions
The code we have written so far works with numeric IP addresses and port numbers. These are the same low-level data that are written directly into packets and that are processed by switches and other network devices. In practice, however, applications most often use host and service names, which are intended to be symbolic, mnemonic, and also more stable handles for hosts and network services. We therefore need to program applications to use host and service names. In particular, we need to find the IP address corresponding to a given host name, and we also need to map service names to port numbers.
A service name is a string like “ssh” (secure shell) and “imaps” (IMAP over SSL), which map to port numbers 22 and 993, respectively. Mapping service names to port numbers is easy. The mappings are standard, which means that they practically nevery change, and there aren’t so many service names anyway. So, a simple, local mapping file would suffice. Mapping host names to IP addresses is instead a bit more involved, because the name space much larger, and also because the assignment of names to addresses is decentralized. Fortunately, there is a distributed network service, called the Domain Name System (DNS), that is designed precisely to map host names to IP addresses. The problem, then, is how to access and query the DNS.
We do all of that through the getaddrinfo function declared in the
netdb.h header as follows:
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
The getaddrinfo function implements both hostname-to-address
translation and service-to-port translation. If the address
resolution succeeds, the return value is 0; otherwise, the return
value is non-zero. The node argument is a string containing a
hostname or an IP address. The service argument is a string
containing either a decimal port number, or a service name. The
hints argument is a pointer to a struct addrinfo that, if not
null, specifies various selection criteria. struct addrinfo is
defined in netdb.h as follows:
struct addrinfo { int ai_flags; /* Flags such as AI_PASSIVE, ac */ int ai_family; /* Address family */ int ai_socktype; /* Socket type */ int ai_protocol; /* Protocol such as IPPROTO_TCP, etc */ socklen_t ai_addrlen; /* Length of ai_addr */ struct sockaddr *ai_addr; /* Socket address structure */ char *ai_canonname; /* Canonical name for the host */ struct addrinfo *ai_next; /* Next addrinfo in the linked list */ };
For example, consider the case in which the caller specifies the
domain service. The domain service is provided on both TCP and
UDP. The caller can set the ai_socktype field of the hints struct
to SOCK_DGRAM, to instruct getaddrinfo to only return information
for datagram sockets.
For any query for host and service names, there can be multiple
answers. Therefore, getaddrinfo returns a linked list of struct
addrinfo objects through the res pointer.
To show how to use the getaddrinfo function, we can implement a
small application that, given a hostname, returns all the associated
addresses as follows:
#include <arpa/inet.h> /* For inet_ntop */ #include <netinet/in.h> /* For INET_ADDRSTRLEN */ #include <stdlib.h> #include <netdb.h> /* For getaddrinfo, freeaddinfo, gai_strerror */ #include <stdio.h> #include <sys/socket.h> int main(int argc, const char *argv[]) { struct addrinfo *res; int rc; char buf[INET6_ADDRSTRLEN]; if (argc != 2) { fprintf(stderr, "usage: %s name\n", argv[0]); return EXIT_FAILURE; } /* * We are interested in mapping the given hostname into one or * more IP addresses. Therefore, we do not need to pass any * service nor hints, and we set those as NULL. `res' will contain * all the resulting IP addresses. The host name is the first * command-line argument. */ if ((rc = getaddrinfo(argv[1], NULL, NULL, &res)) != 0) { /* * In case of error, getaddrinfo returns an error code. We * can then use `gai_strerror' to obtain a string * representation of that error. */ fprintf(stderr, "failed to resolve address '%s': %s\n", argv[1], gai_strerror(rc)); return EXIT_SUCCESS; } struct addrinfo *p; const char *proto; for (p = res; p; p = p->ai_next) { if (p->ai_protocol == IPPROTO_UDP) proto = "udp"; else if (p->ai_protocol == IPPROTO_TP) proto = "tcp"; else continue; /* * Each node of the res linked list will contain a socket * address structure that can also be used for other * networking functions, such as connect. */ if (p->ai_family == AF_INET) { struct sockaddr_in *addr = (struct sockaddr_in *)p->ai_addr; if (inet_ntop(p->ai_family, &addr->sin_addr, buf, sizeof(buf)) == NULL) continue; if (p->ai_canonname) printf("Canonical name: %s\n", p->ai_canonname); if (ntohs(addr->sin_port) == 0) printf("IPv4 address: %s\n", buf); else printf("Socket address: %s:%d/%s\n", buf, ntohs(addr->sin_port), proto); } else if (p->ai_family == AF_INET6) { struct sockaddr_in6 *addr = (struct sockaddr_in6 *)p->ai_addr; if (inet_ntop(p->ai_family, &addr->sin6_addr, buf, sizeof(buf)) == NULL) continue; if (p->ai_canonname) printf("Canonical name: %s\n", p->ai_canonname); if (ntohs(addr->sin6_port) == 0) printf("IPv6 address: %s\n", buf); else printf("Socket address: [%s]:%d/%s\n", buf, ntohs(addr->sin6_port), proto); } else continue; } /* * Free the memory allocated by getaddrinfo for the linked list of * addresses. */ freeaddrinfo(res); return EXIT_SUCCESS; }