kuniga.me > NP-Incompleteness > Sockets
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.
File Descriptors
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
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:
if (!fork()) {
printf("I'm the child!\n");
} else {
printf("I'm the parent!\n");
}
The return value can be used to distinguish between a parent and child and hence we can have them execute different code.
Signal
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:
void sigint_handler(int sig) {
write(0, "Ahhh! SIGINT!\n", 14);
}
int main(void) {
void sigint_handler(int sig); /* prototype */
struct sigaction sa;
sa.sa_handler = sigint_handler;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
...
}
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_INET
and 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_STREAM
and 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):
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_INET6; // IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo("www.google.com", "https", &hints, &servinfo);
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)servinfo->ai_addr;
void *addr = &(ipv6->sin6_addr);
char ipstr[INET6_ADDRSTRLEN];
inet_ntop(servinfo->ai_family, addr, ipstr, sizeof ipstr);
printf("IP: %s\n", ipstr);
The variable servinfo
is of type addrinfo
, which is a node of a linked list:
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
int ai_protocol; // use 0 for "any"
size_t ai_addrlen; // size of ai_addr in bytes
struct sockaddr *ai_addr; // struct sockaddr_in or _in6
char *ai_canonname; // full canonical hostname
struct addrinfo *ai_next; // linked list, next node
};
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_INET
and AF_INET6
(IPv4 / PIv6)SOCK_STREAM
or SOCK_DGRAM
(TCP / UDP)PF_INET
and PF_INET6
In practice AF_INET
is the same as PF_INET
. Beej says:
This
PF_INET
thing is a close relative of theAF_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 ofgetaddrinfo()
to fill these for us:
getaddrinfo("www.example.com", "http", &hints, &res);
s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
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 getaddrinfo()
and AI_PASSIVE
to ai_flags
, we’ll have this function fill the IP in res for us:
hints.ai_family = AF_INET6;
hints.ai_socktype = SOCK_STREAM;
// Important! Fill in my IP for me
hints.ai_flags = AI_PASSIVE;
// Use the address from localhost
getaddrinfo(NULL, "3490", &hints, &res);
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
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 getaddrinfo()
:
getaddrinfo("www.example.com", "3490", &hints, &res);
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
connect(sockfd, res->ai_addr, res->ai_addrlen);
Note how we didn’t need to bind()
. 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:
getaddrinfo()
socket()
bind()
accept()
recv()/send()
For the client we have:getaddrinfo()
socket()
connect()
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.