I wrote a simple client-server application. When I started testing, I noticed that not all events are processed properly when the EPOLLET flag is set for socket fd.
In the loop, I connected to the server and sent some data to it. For the test, I made 10,000 connections, and from the server side I counted each event, whether it was an event on a socket descriptor or a client one. And always according to the logs, the server took less than expected. (for 10000 iterations approximately (~ 9200). And I don’t understand what this could be connected with.
Am I handling the events correctly or am I missing something? Maybe my approach to testing is not quite right (I counted output of lines every time something happened on socket fd)
client.c:
#include <assert.h>
#include <netdb.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define DEF_IP "127.0.0.1"
#define DEF_PORT "3940"
#define DEF_MESSAGES_COUNT 10000
typedef struct addrinfo addrinfo;
static int conn_init();
int main() {
int i, fd_socket;
ssize_t bytes_send;
uint64_t num;
for (i = 0; i < DEF_MESSAGES_COUNT; i++) {
fd_socket = conn_init();
num = htobe64(i);
bytes_send = send(fd_socket, &num, sizeof(num), 0);
assert(bytes_send > -1);
printf("I! Client: sent [%d], bytes: [%ld]\n", i, bytes_send);
close(fd_socket);
}
printf("sended: %d messages ([0] - [%d])\n", i, i - 1);
return 0;
}
static int conn_init() {
addrinfo info_hints = {0};
addrinfo *info_server;
int fd_socket, ret;
info_hints.ai_family = AF_UNSPEC;
info_hints.ai_socktype = SOCK_STREAM;
ret = getaddrinfo(DEF_IP, DEF_PORT, &info_hints, &info_server);
assert(ret == 0);
fd_socket = socket(info_server->ai_family, info_server->ai_socktype, info_server->ai_protocol);
assert(fd_socket > -1);
ret = connect(fd_socket, info_server->ai_addr, info_server->ai_addrlen);
assert(ret == 0);
freeaddrinfo(info_server);
return fd_socket;
}
server.c:
#include <assert.h>
#include <fcntl.h>
#include <netdb.h>
#include <stdbool.h>
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct addrinfo addrinfo;
typedef struct epoll_event epoll_event;
typedef struct sockaddr_storage sockaddr_storage;
typedef struct sockaddr sockaddr;
#define DEF_IP "127.0.0.1"
#define DEF_PORT "3940"
#define DEF_MAX_EVENTS 1000
#define DEF_BACKLOG 1000
static int conn_handle_socket(int fd_socket);
static void epoll_add(int fd_epoll, int fd, uint32_t flag);
int main() {
int fd_socket, fd_connect, fd_epoll, ret;
epoll_event events[DEF_MAX_EVENTS];
addrinfo info_hints = {0};
addrinfo * info_server;
info_hints.ai_family = AF_UNSPEC; // IPv4 или IPv6
info_hints.ai_socktype = SOCK_STREAM; // TCP
info_hints.ai_flags = AI_PASSIVE; // use my IP
ret = getaddrinfo(DEF_IP, DEF_PORT, &info_hints, &info_server);
assert(ret == 0);
fd_socket = socket(info_server->ai_family, info_server->ai_socktype, info_server->ai_protocol);
assert(fd_socket > -1);
ret = setsockopt(fd_socket, SOL_SOCKET, SO_REUSEADDR, (const char *)&(int){1}, sizeof(int));
assert(ret == 0);
ret = bind(fd_socket, info_server->ai_addr, info_server->ai_addrlen);
assert(ret == 0);
freeaddrinfo(info_server);
ret = listen(fd_socket, DEF_BACKLOG);
assert(ret == 0);
// init epoll
fd_epoll = epoll_create1(0);
assert(fd_epoll > 0);
// monitor socket fd
epoll_add(fd_epoll, fd_socket, EPOLLET);
printf("I! Server: is ready\n");
while (1) {
int num_events = epoll_wait(fd_epoll, events, DEF_MAX_EVENTS, -1);
assert(num_events > -1);
for (int i = 0; i < num_events; i++) {
int fd_tmp = events[i].data.fd;
if (fd_tmp == fd_socket) {
printf("I! Server: SOCKET FD\n");
fflush(stdout);
// accept client
fd_connect = conn_handle_socket(fd_socket);
epoll_add(fd_epoll, fd_connect, EPOLLONESHOT);
} else if (events[i].events & EPOLLIN) {
printf("I! Server: CLIENT FD\n");
fflush(stdout);
// 'processing' clients
assert(epoll_ctl(fd_epoll, EPOLL_CTL_DEL, fd_tmp, NULL) == 0);
close(fd_tmp);
}
}
}
assert(epoll_ctl(fd_epoll, EPOLL_CTL_DEL, fd_socket, NULL) == 0);
assert(epoll_ctl(fd_epoll, EPOLL_CTL_DEL, STDIN_FILENO, NULL) == 0);
close(fd_socket);
close(fd_epoll);
return 0;
}
static int conn_handle_socket(int fd_socket) {
int fd_connect;
sockaddr_storage addr_connected;
socklen_t sin_size;
sin_size = sizeof(addr_connected);
fd_connect = accept(fd_socket, (sockaddr *)&addr_connected, &sin_size);
assert(fd_connect > -1); // ignore EAGAIN || EWOULDBLOCK
return fd_connect;
}
static void epoll_add(int fd_epoll, int fd, uint32_t flag) {
int flags, ret;
epoll_event ev = {0};
ev.events = EPOLLIN;
if (flag == EPOLLET || flag == EPOLLONESHOT) {
ev.events |= flag;
}
ev.data.fd = fd;
ret = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd, &ev);
assert(ret == 0);
flags = fcntl(fd, F_GETFL, 0);
assert(flags > -1);
flags |= O_NONBLOCK;
ret = fcntl(fd, F_SETFL, flags);
assert(ret > -1);
}
In ET(edge-triggered) mode, if multiple events generated on the same fd, it will only trigger once.
for (int i = 0; i < num_events; i++) {
int fd_tmp = events[i].data.fd;
if (fd_tmp == fd_socket) {
printf("I! Server: SOCKET FD\n");
fflush(stdout);
// accept client
fd_connect = conn_handle_socket(fd_socket);
epoll_add(fd_epoll, fd_connect, EPOLLONESHOT);
} else if (events[i].events & EPOLLIN) {
printf("I! Server: CLIENT FD\n");
fflush(stdout);
// 'processing' clients
assert(epoll_ctl(fd_epoll, EPOLL_CTL_DEL, fd_tmp, NULL) == 0);
close(fd_tmp);
}
}
it only accepts one client for each epoll_wait returns. The other clients are still in the backlog of listenfd.
There are two solutions:
An application that employs the EPOLLET flag should use nonblocking file descriptors to avoid having a blocking read or write starve a task that is handling multiple file descriptors. The suggested way to use epoll as an edge-triggered (EPOLLET) interface is as follows: a) with nonblocking file descriptors; and b) by waiting for an event only after read(2) or write(2) return EAGAIN. And accept is a kind of read as well.