07 Mar 2020
In this post we’ll discuss sockets, more precisely Network Sockets. It’s based on two excellent articles from Brian “Beej Jorgensen” Hall. We’ll first cover some basic OS and Network concepts and then go over some important functions needed to create a simple socket server that can handle requests from multiple clients.
This post assumes basic familiarity with the Linux operating system and the C programming language.
A file descriptor is basically a numerical identifier (id) to a lookup table a given process contains. It is used to model not only files, but also things like stdin (special id = 0), stdout (id = 1) and stderr (id = 2). Sockets are also represented as file descriptors as we’ll see later.
fork() is a system call the current process can use to generate copies processes (known as children), that run the same program. One observation is that t**he child process gets a copy of the parent’s data.
This will be important for our example where the main process uses
fork() to generate children to handle connections. The child needs to inherit some of the file descriptors from the parent.
fork() returns 0 if it’s the current process executing the code or a non-zero value corresponding to the process id (pid) of the child. A common way to use
fork() is the following:
The return value can be used to distinguish between a parent and child and hence we can have them execute different code.
Signals are one of the simplest ways to communicate with a process. We just send a code that the process knows how to handle. There are OS-level signals like SIGKILL and SIGTERM and user-defined such as SIGUSR1. It’s possible to override how a process handles specific signals via
sigaction() which take a structure that points to a handler:
Beej describes the Layered Network Model but follows with this interesting observation:
Now, this model is so general you could probably use it as an automobile repair guide if you really wanted to. A layered model more consistent with Unix might be:
- Application Layer (telnet, ftp, etc.)
- Host-to-Host Transport Layer (TCP, UDP)
- Internet Layer (IP and routing)
- Network Access Layer (Ethernet, wi-fi, or whatever)
In this model, sockets are in the application layer since it relies on TCP or UDP on top of IP. We’ll go over these 3 things next.
IP the Internet Protocol
The guide discusses some details of the IP, including the IPv4 and IPv6 distinction and different address types. The flags
AF_INET6 are part of the socket API and associated to these 2 types.
One interesting detail is the byte order (Little-Endian and Big-Endian): while each computer systems can represent data in different ways, the order is standardized for the internet, and is Big-Endian. This is also known as Network Byte Order.
UDP and TCP
The User Datagram Protocol is connectionless (stateless) and provides no guarantees on the order of the datagrams, their delivery or that duplicates are avoided. The messages sent via UDP are known as datagrams.
The Transmission Control Protocol relies on a connection (via a 3-way handshake), guarantees order and perform retries. The messages sent via TCP are known as data stream.
The socket types
SOCK_DGRAM are associated to the TCP and UDP protocols respectively.
Before we proceed with our example, we’ll cover the C functions in
sys/socket.h that correspond to the Linux socket APIs:
getaddrinfo()is a relatively high-level function that is capable of resolving an address (e.g. google.com) to an actual IP and port. An example of use is (fullcode):
servinfo is of type
addrinfo, which is a node of a linked list:
getaddrinfo() returns a list of such values, any that match the criteria from the input parameters. In the Beej’s client code, we’ll see iterates over that list until it finds a set of parameters that it can connect to.
socket() returns a file descriptor. It’s basically creating a register with a given ID in a table and it returns that identifier we’ll use to establish a connection later. The interface is as follows:
int socket(int domain, int type, int protocol);
AF_INET6(IPv4 / PIv6)
SOCK_DGRAM(TCP / UDP)
AF_INETis the same as
PF_INET. Beej says:
PF_INETthing is a close relative of the
AF_INET(…) they’re so closely related that they actually have the same value (…), it was thought that maybe an address family (what the “AF” in “
AF_INET” stands for) might support several protocols that were referred to by their protocol family (what the “PF” in “
PF_INET” stands for). That didn’t happen. More conveniently we can also use the results of
getaddrinfo()to fill these for us:
bind() binds a socket to a specific hostname and port. It is not always needed (for example in the client case). The client doesn’t usually care which port is used on its own side, so it can let the OS choose. For the server case it’s important because it defines the IP address and port the socket will listen to.
This information can be more easily provided via the struct returned from
getaddrinfo(). By providing null to
ai_flags, we’ll have this function fill the IP in res for us:
connect()is the function a client can use to indicate the desire to establish a connection with a given server. Similarly to
bind() we can use the results from
Note how we didn’t need to
connect() filled the local host and a random port for us.
listen() is the function a server calls to define how many connections it can listen to at a specific socket. Beej’s article mentions a system limit of 20, but using 5 to 10 seems to work in practice.
accept() is the function that is actually connects with a specific client. So that the original socket can keep listening to other incoming connections,
accept() returns a new socket descriptor which will be used to send and receive messages.
send() / recv() are used to send and receive messages via the established connection. One important aspect is that while you specify the size of the data being sent, the API does not guarantee it will send the whole data, so you need to write a loop to make sure all data is sent/received.
The high-level sequence of calls for the API above is:
recv()/send()For the client we have:
send()/recv()Beej provides examples for server and client codes in C: server.c and client.c.
Beej has an entire guide dedicated to inter-process communication. The guide covers the basic concepts such as creating new processes via fork() and handling signals; synchronization mechanisms suck as locks and semaphores; and communication mechanisms such as pipes, message queues and sockets. I like the conversational style of his writings.
I probably wrote code using sockets a long time ago. I didn’t have time to dig deep on this subject so I didn’t feel like I learned a ton. It was a good refresher nevertheless.