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.