Writing a network protocol for fun

It's been 12 years since I've had to type gcc -o into the terminal. Surprisingly, I'm enjoying it a lot more than I thought I would. I stopped using it because I thought it was old and boring. Turns out I was just naive and wrong. In the last month, using C has helped me better understand garbage collection, improve my go programs by reducing heap allocations, using the stack more, understanding the tradeoffs with sharing up vs down etc. Moreover, I truly appreciate "Everything is a file" in Linux. If you understand how to read and write to a file, writing decent network code in C was surprisingly quite intuitive.

This is the most basic version of a network protocol, but programming it was a lot of fun nonetheless. I heavily relied on strace which lets see the system calls made by the program, which was quite useful in understanding when the program just stalled.

Type, Length, Value

That's all that my protocol sends over, once a TCP connection is established. So, I've just called the protocol PROTO_TLV, no points for originality.

typedef enum { PROTO_TLV } proto_type_e;

The protocol header is next, well the header is all there is to the protocol :D. It's the simplest I needed:

typedef struct {
  proto_type_e type;
  unsigned short len;
} proto_hdr_t;

Creating a socket

The next step is to create a socket, bind, listen and accept connections.

Creating a Socket

Quite simple in C, you call the socket function with a domain, type and protocol. You can read more about it in the man page (man socket).

int sfd = socket(AF_INET, SOCK_STREAM, 0);

The domain here is IPv4, and the protocol is TCP. Calling this function returns a file descriptor which I will refer as the listen file descriptor or listenfd from here on.

Binding & Listening

Bind requires the server info to connect to. This is what I've used:

struct sockaddr_in serverInfo = {0};
serverInfo.sin_family = AF_INET;
serverInfo.sin_addr.s_addr = INADDR_ANY;
serverInfo.sin_port = htons(8080);

sockaddr_in needs to be cast to the generic sockaddr when passed to bind.

Listening is quite straightforward, simply pass the listenfd. Now the socket is ready to accept connections from the client.

Accepting Connections

int cfd = accept(sfd, (struct sockaddr *)&clientInfo, &clientAddrLen);

Now we are ready to accept connections, the accept method returns a client descriptor, which can be used to read from and write to, just like a file. I've defined a simple handler that simple writes the header to any incoming connections.

void handle_client(int fd) {
  unsigned short headerlen = sizeof(int);
  char buf[4096] = {0};
  proto_hdr_t *hdr = (proto_hdr_t *)buf;

  hdr->type = htonl(PROTO_TLV);
  hdr->len = htons(headerlen);

  int *data = (int *)&hdr[1];
  *data = htonl(1);

  if (write(fd, hdr, sizeof(proto_hdr_t) + headerlen) == -1) {
    perror("write");
    return;
  }

  return;
}

That's it! Now, you can write a client if you want that pretty much does the same but verifies the protocol sent from the server.

You can find the complete code here.

Make a protocol of your own

Even though this protocol will never see any use, I have got some interesting insights on how networks protocols work and how the system handles it, which you can see when running strace. I had a lot of fun doing it. Hope you do too.

I'm working on a rudimentary database and I wanted to create a protocol to be able to communicate with it over a network. This was step zero. I'll probably write about it in the future once I get it working.

You can find my database here, and the network code for it here.

Thank you for reading.