C Server
Eric Miranda / November 2022 (1894 Words, 11 Minutes)
I remember one of the first times I truly understood something in my comp sci studies - finding out what a client / server actually meant.
Up until then, I had used high level frameworks like Django; my idea of servers being some complicated networked program serving all types of files and text, magically responding to client web requests. Come one day in my OS class, I see a server implemented in plain ol’ Java.
Click! I was amazed at the simplicity of this overloaded term, server.
Let’s go lower
Now, ~3 years later, inspired by this amazing video, I felt the urge to understand client / server networking at a deeper, lower level. I’m talking C! A language I haven’t touched in quite a while, but always drawn to, for whatever reasons.
A server
We start off by creating a file, server.c
vim server.c
Scaffolding, with some comments on the process we’ll follow
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
// 1. init some necessary data structures
// 2. grab ourselves a socket
// 3. bind
// 4. listen
// 5. accept
// 6. respond
// 7. close
return 0;
}
So, what’s a socket ?
Drawing on wikipedia
A (network) socket is a software structure within a networked node (essentially a computer) that serves as a point for receiving data
- don’t quote me on this
The text in bold gives us an idea of what a socket really is. Some further googling will tell you that a socket is just a file - inline with the unix philosophy.
1. Data Structures
Let’s skip this for now
2. Creating a socket
We here love man
, not google.
int socket(int domain, int type, int protocol)
- man 7 ip
Return type
int socket(...
the return type is an integer, the lowest number signifying an available file descriptor for the process. I find that this article explains FDs really well, btw
Parameters
int domain
the “communication domain”. There are different types of sockets depending on whether we’re communicating between processes (IPC) or networks. In our case, network, the domain is AF_INET - just an int constant that’s understood by the kernel to be a network socketint type
the communication type, which could be connection-oriented or connectionless. In our case, connection-oriented, the type isSOCK_STREAM
- once again, an int constant understood by the kernelint protocol
the protocol used (TCP, UDP etc) In our case there exists only one protocol with a connection-oriented type within the network domain, and that’s TCP. As perman 7 ip
, we can pass 0 here and the kernel would understand
Code time!
#include <stdio.h>
int main()
{
// 1. init some necessary data structures
// 2. grab ourselves a socket
int sfd = socket(AF_INET, SOCK_STREAM, 0);
// ...
}
After writing the socket function call, I couldn’t help but notice how arguments and return values were simply just int
. Coming from high level languages, I’m used to complicated, data-stuffed objects returned and passed in left right and center!
3. Bind
We now have a socket file descriptor, but what good is that? When we visit a link in the browser, we enter an IP address (ultimately, thanks DNS). So how will the kernel know to route the data coming in on a specific network interface to our socket file?
That’s where bind()
comes in.
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Return type
int bind(...
yet again, a lovelyint
constant ❤️ 0 is returned on success, -1 on failure
Parameters
int sockfd
the socket file descriptorconst struct sockaddr *addr
ah, we finally come to (1), the data structure. On to that soon…socklen_t addrlen
simply the size of the above data structure, in bytes
1. (Back to) Data Structures
Little on sockets, first. A socket is uniquely identified by a interface IP address and a port number1. The data structure that contains this information is, you guessed it, struct sockaddr
// typedefs
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address (ip) */
};
/* Internet address (ip) */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
It’s all pretty self-explanatory, except for the network byte order. Using the network byte order, we ensure that our network stack can remain architecture agnostic (by only using big endian byte order).
We can achieve network order using library functions, htons()
& htonl()
int main()
{
// 1. init some necessary data structures
struct in_addr ip_address = {0}; // effectively 0.0.0.0, i.e, listen on all interfaces
const struct sockaddr_in sock_addr = {0}; // zeroize the struct's memory
sock_addr.sin_family = AF_INET;
sock_addr.sin_port = htons(8999);
sock_addr.in_addr = ip_address;
// ..
Great that we’ve got that sorted. Moving back to the bind call, we can now do
// 3. bind
int bind_error = bind(sfd, &sock_addr, sizeof(sock_addr));
if (0 != bind_error)
{
perror("Shucks, couldn't bind. Is the port in use ?");
return bind_error;
}
// ...
4. Listen
A socket truly receives data only when it’s listening, so let’s talk about that
int listen(int sockfd, int backlog)
Return Type
int listen(...
same as bind (and sever other programs in general) - 0 on success, -1 on failure
Parameters
int backlog
seems like the kernel ignores this value anyway so we’ll set it to 0
Let’s add it to our code
// 4. listen
int listen_error = listen(sfd, 0);
if (0 != listen_error)
{
perror("I'm listening to another process. Not you");
return bind_error;
}
5. Accept
Like we’ve seen before, TCP is connection oriented. This means that every new client trying to connect
to our server must be accept()
ed by us first!
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
Return type
int accept(...
returns a new socket file descriptor number, especially for dealing with the particular connecting client. -1 on error
Parameters
struct sockaddr *addr
a structure similar to the one in the bind call, except that it contains the client address details. We’ll set it toNULL
, as we don’t need it in our basic examplesocklen_t *addrlen
length of the previous data structure, in our caseNULL
// 5. accept
int cfd = accept(sfd, NULL, NULL);
if (0 > cfd)
{
perror("I won't accept you for who you are");
return cfd;
}
6. Respond
Finally, tangible communication with the client! Let’s send him a hello 👋.
To really dig it in that a socket is just a file, we’ll use the write
system call instead of send
which is normally used in network programming. There’s probably some RED_FLAGS here, but we’ll ignore
them in the spirit of pedagogy
ssize_t write(int fd, const void *buf, size_t count);
Return type
ssize_t write(...
on success, the number of bytes written. -1 on failureint fd
not the usual sfd, but again, it’s just a file descriptor ultimately. Note that we specify our client sfd hereconst void *buf
a pointer to our buffer, which is a fancy term for something that contains our messagesize_t count
the number of bytes to be read from buffer. Remember that by default we don’t have an in-band signal for the end of the buffer (in our case, some memory containing a string). So we tell the system call to only readcount
bytes in order to avoid buffer overflow attacks
// 6. respond
char *message = "Hi there";
ssize_t bytes_sent = write(cfd, message, strlen(message));
// finally close the socket file
if (0 > close(cfd))
{
perror("Couldn't close !\n");
}
7. Close
The close()
system call, used on files
// close
if (0 > close(sfd))
{
perror("Couldn't close !\n");
}
The end
And that’s it! But these last few steps, particularly (5) and (6), got me wondering, do we create a new connection for every HTTP request? It turns out, kinda2
A client in c is implemented similarly (and “easily-er”), so in the interest of time (it’s a 10 minute read already), we’ll skip over the code. But the way you’d do it is
- create a socket with
socket()
- connect to a socket with
connect()
, passing in the server IP:PORT details - receive a message from the server using
recv()
, passing in a buffer
Wait I wanna see it in action
Ok! Instead of writing our own client, we’ll use a handy command-line network tool called netcat
In one terminal, run
gcc server.c && ./a.out
In another, let’s start a client!
nc 0.0.0.0 8999
And you’ll see a beautiful “Hi there”
Phew! I think I’ve developed an interest for network programming. I still have that naggy feeling (or fear) of not knowing what’s happening at the protocol level (I’m talking TCP/IP stack baby).
My brother once suggested I implement the stack on some cheap board. Maybe I’ll do that sometime :)