I’ve worked on ads at Meta, Snap, and Nextdoor. I also consult companies on building ads. If you are building your ad stack, feel free to reach out — let’s chat!
I worked on this small project to better understand how kqueue works. Let’s implement a bidirectional streaming server using kqueue only in 100 lines (in C)!
Note: If you want to learn more about kqueue fundamentals, read this post first.
Design
The server starts up on port 8080
Enters the event loop where we handle:
New connections
Disconnections
Sending/receiving messages
Starting the server
In order to start the server we need to:
Create a socket
Bind it to an address (<localhost, port 8080>)
Listen on the socket for incoming connections
Next,
create an empty kqueue
create an eventSet for READs on the socket
add evSet to the kqueue
Note: for (2), refer to the man page for kevent (EVFILT_READ section):
Sockets which have previously been passed to listen() return when there is an incoming connection pending.
Then we enter the event loop where we handle incomming connections and send/receive messages.
Accepting Connections
Each time we receive a new connection from a client, we accept the connection. The accept(..) system call basically does the tcp 3-way handshake and creates a socket for further communication with that client and returns the file descriptor of that socket. We need to store these file descriptors for each client so we can communicate with them.
Connection Pooling
Let’s store an array of client_data (which contains the socket’s fd), and initially all of them have fd = 0, which means “unused”.
Operations:
Lookup: Given a fd, we can find the corresponding client_data by simply iterating over the array
Add: For new connections, we find the first free item (fd = 0) in the array to store the client’s fd
Delete: When the connection is lost, we free that item in the array by setting it’s fd to 0
Below is the code for these three operations:
Event Loop
Now, we create an infinite loop where we call kevent(..) to receive incoming events and process them.
So far, we have registered to receive incoming connections on the main socket. When we receive such event, we accept() the connection and store the new socket’s fd in our connection pool. We also register for the incoming messages from that client (on the same kqueue). We also send them a welcome message on this new socket!
When a client disconnects, we receive an event where the EOF flag is set on the socket. We simply free up that connection in the pool and remove the event from kqueue (via EV_DELETE).
Finally, we handle incoming data from clients and receive their message.
Complete Code
All Done!
This is a simple implementation just to demonstrate how kqueue works. It lacks many things including handling sys call failures, handling large messages correctly, etc.
As an exercise, add a feature where server can also read messages from stdin and broadcast them to all clients.
Everything seems rather efficient except the fact that the call to accept does a tcp 3-way handshake, which incurs an extra roundtrip. You may wonder if that’s necessary and can we avoid that round trip fully or partially. While the 3-way handshake is necessary for tcp connections, this external post explains some alternatives such as TCP Fast Open (TFO) or QUIC (TLS over UDP).
Below is a demo where I run the server on the lift and 4 clients on the right.
Note: For the client side, I use the linux utility nc(netcat). You can run nc -l PORT to listen on a port or run nc HOST PORT to connect to a server and send data (once connected, type your msg and press enter to send).