Warning
This page is located in archive. Go to the latest version of this course pages.

Event Handling (non-blocking I/O)

Task assignment

Applications usually need to simultaneously handle many event sources, such as keyboard, GUI interface, interprocess or network communication. Although many high-performance libraries and language features exist to solve that, it is useful to understand how those libraries work internally and which operating system interfaces they use. Therefore, we will create a light-weight event loop based application, where multiple various events can be handled in a single thread. Such an event loop is always a base of async/await style of programming.

Your goal is to implement an event loop-based application (epoll system call), which can handle timers, keyboard input, and TCP connections simultaneously in a single thread. The base of the application (epoll loop, timer, and keyboard input) is already implemented in the provided template. Don't use multiple threads!

Steps:

  1. Download the epoll application template from our git repository:
    git clone https://gitlab.fel.cvut.cz/matejjoe/epoll_server.git
  2. Compile and run the program:
    cd epoll_server
    mkdir build && cd build
    cmake .. && make
    ./epoll_server
  3. Implement a TCP server, which will listen on port 12345, into the application (hints below). Use epoll mechanism (a few related system calls) to handle events on TCP. Take into account multiple simultaneous connections. If you don't like our implementation, feel free to write the application yourself (but keep existing functionality).
  4. For each TCP connection, your application should calculate the length of incoming lines and send the length back as an ASCII string (immediately after receiving '\n') eg:
    receive: "Test string\n"
    send: "11\n" // keep connection established
  5. You can test basic functionality by using the netcat program:
    nc localhost 12345
  6. Upload your solution as an archive to the BRUTE. Extracted files must be compilable with the `make`, CMake, or Meson build systems. The upload system will test your solution, but the evaluation is manual.

Using epoll

In UNIX, “everything” is represented as a file descriptor, and therefore we are interested in monitoring these file descriptors for events such as “key was pressed”, “new client is connecting” or “a client sent us data”. All those events are represented by “file descriptor is readable/writable” events. Epoll is high performance mechanism, which enables waiting for multiple events on multiple file descriptors.

  • First, it is necessary to create epoll instance (context) using epoll_create1().
  • Then, file descriptors to be monitored can be added to or removed from the given epoll instance. The epoll_event structure and epoll_ctl() function are used for that. Each event can be edge or level triggered. When edge-triggered behavior is chosen (events |= EPOLLET), the program is notified about each event only once – e.g. you need to read all data from a given file – you will not be notified again, if data are not read completely.
  • Finally, you can wait for events on all monitored file descriptors by calling epoll_wait() function in the main loop.

Creating TCP server in C

TCP provides a reliable, ordered, and error-checked transport of data streams. Streams do not preserve boundaries between messages. It means, that in one read you can receive only part of a sent message or multiple messages.

First of all, we have to include needed headers:

#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <fcntl.h>

Then create a TCP socket (AF_INET → IPv4, SOCK_STREAM → TCP):

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

Create a socket address and bind the socket to this address:

short int port = 12345;
struct sockaddr_in saddr;

memset(&saddr, 0, sizeof(saddr));
saddr.sin_family      = AF_INET;              // IPv4
saddr.sin_addr.s_addr = htonl(INADDR_ANY);    // Bind to all available interfaces
saddr.sin_port        = htons(port);          // Requested port

bind(sfd, (struct sockaddr *) &saddr, sizeof(saddr))

Our socket should be non-blocking. Why? By default, sockets are blocking, meaning that a call to read() blocks the calling thread until data is received. While the thread is blocked (which can be for long time), events from other sources cannot be handled. (fnctl):

int flags = fcntl(sfd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(sfd, F_SETFL, flags);
When you try to read from a non-blocking socket which has no data available, you receive en error (EAGAIN or EWOULDBLOCK) instead of blocking.

We would like to create a server, and therefore we need to listen for connections:

listen(sfd, SOMAXCONN);

Now, we are able to be notified about incoming connections. In order to accept the incoming connection, call function accept:

cfd = accept(sfd, NULL, NULL);

Set the new file descriptor for non-blocking operations (same as above).

Then, we can communicate with the client using read() and write() system calls:

#define BUF_SIZE 42
char buffer[BUF_SIZE];
int count;
count = read(cfd, buffer, BUF_SIZE-1);
count = write(cfd, buffer, strlen(buffer));

After the end of communication, close the connection:

close(cfd);

Hints

  • You probably want to create two new classes
  • You can use delete an object via its this pointer: https://isocpp.org/wiki/faq/freestore-mgmt#delete-this
  • Consider fragmentation (“Hello World!\n” can arrive as two different messages – “He” “llo World\n”)
  • Don't close the connection after receiving first '\n'
  • Read status of all system calls (especially read) – if an error occurs, read errno variable
  • What do epoll edge and level triggers mean?
  • Read can block if respective file descriptor is not set to O_NONBLOCK mode
courses/b4m36esw/labs/lab03.txt · Last modified: 2022/03/28 09:00 by cuchymar