Kqueue EVFILT_READ Example

chatserv.c

This example is a network chat server, though not a terribly useful one. For simplicity, it only supports a maximum of 10 users and with no nick support more than two users will get confusing. Clients can connect using something like netcat.

The basic idea is to bind a listen socket that will accept new clients, and an EVFILT_READ kevent for each client that returns when there is data to be read and then write that data to other clients.

There are three things to take care of:

We use the following array of struct’s to represent each client:

struct uc {
    int uc_fd;
    char *uc_addr;
} users[NUSERS];

where uc_fd is the client file descriptor and uc_addr is the clients IP address. The function mksock returns a socket that has been bound and had listen() called on. It is this socket that will listen for new connections.

Lines 86 to 91 have the following:

EV_SET(&ke, servsock, EVFILT_READ, EV_ADD, 0, 5, NULL);

/* set the event */
i = kevent(kq, &ke, 1, NULL, 0, NULL);
if (i == -1)
    err(1, "set kevent");

the arguments to EV_SET break down as such:

At line 89 we set the kevent passing it the changelist. When EVFILT_READ is used on a listening socket, kevent will return when there is a client to accept().

We now enter the main loop, waiting in a blocking call to kevent() for events to be returned. Two thins may happen, we either have a new client to accept, or have to deal with a client issue (data to read or client exit). The call to kevent will return with ke.ident as either the servers listen socket or a client socket. Based on this we work out what to do

/* recieve an event, a blocking call as timeout is NULL */
i = kevent(kq, NULL, 0, &ke, 1, NULL);
if (i == -1)
    err(1, "kevent!");
...
...
if (ke.ident == servsock) {
    ...
    accept the new client
    ...
} else {
    ...
    handle some client issue, ke.ident is a client socket
    ...
}

When there is a client ready to accept the following steps take place

i = accept(servsock, (struct sockaddr *)&c, &len);

accept the client, i now holds the clients file descriptor

for (uidx = 0; uidx < NUSERS; uidx++)
    if (users[uidx].uc_fd == 0)
        break;

Find an empty slot in our array of users. If there is no empty slot the client is informed and the connection closed (lines 123 - 128), otherwise we keep a copy of their address and their file descriptor (lines 130 - 133)

EV_SET(&ke, i, EVFILT_READ, EV_ADD, 0, 0, NULL);
i = kevent(kq, &ke, 1, NULL, 0, NULL);
if (i == -1)
    err(1, "kevent add user!");

Here we add a kevent to return when there is data to be read from the clients file descriptor (stored in i at the call to EV_SET).

When a client descriptor is returned, we look them up in the users array and try to read their message. If the read returns data, it is written to the other clients. If read returns EOF, we remove the client with the following code:

EV_SET(&ke, users[uidx].uc_fd, EVFILT_READ,
    EV_DELETE, 0, 0, NULL);

i = kevent(kq, &ke, 1, NULL, 0, NULL);
if (i == -1)
    err(1, "remove user from kq");

The two things to note here is the user of EV_DELETE as the action flag and users[uidx].uc_fd which is the clients file descriptor as the ident. In the subsequent call to kevent, this users file descriptor kevent will be removed.

Thats about all there is to this, hopefully the rest of the code is clear enough.