home..

C Server

c client server

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

Parameters

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

Parameters

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

Parameters

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

Parameters

  // 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

  // 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

  1. create a socket with socket()
  2. connect to a socket with connect(), passing in the server IP:PORT details
  3. 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 :)


Amazing refs

  1. A nice historic overview of socket implementations

Footnotes

© 2025 Eric Miranda   •  Powered by Soopr   •  Theme  Moonwalk