在epoll事件循环中阻止操作的解决方案

时间:2017-08-23 03:24:02

标签: c sockets concurrency server epoll

我的TCP服务器中有一个epoll事件循环来处理客户端连接和从客户端读取数据。

while(1) {
    int n, i;

    n = epoll_wait(efd, events, 64, -1); // This is blocking. It waits till new events arrive

    for(i = 0; i < n; i++) {
        if((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN))) {
          /* An error has occured on this fd, or the socket is not
             ready for reading */
      dzlog_error("epoll error: %s", strerror(errno));
      close(events[i].data.fd);

      continue;
    } else if(sock == events[i].data.fd) { // Event on the server socket. Accept client connection
            while(1) {
                if((cli = accept(sock, (struct sockaddr *)&their_addr, &addr_size)) == -1) {
                    if((errno == EAGAIN) || (errno == EWOULDBLOCK)) { // We have processed all incoming connections
                        break;
                    } else {
                        dzlog_error("accept: %s", strerror(errno));
                        break;
                    }
                }

                dzlog_info("Client connected: Identifier - %d", cli);

                s = fcntl(cli, F_SETFL, O_NONBLOCK); // Make client socket non-blocking
               if(s == -1) {
                    dzlog_error("Client no block: %s", strerror(errno));
                    close(cli);
                    break;
                }

                event.data.fd = cli;
                event.events = EPOLLIN | EPOLLET;
                s = epoll_ctl (efd, EPOLL_CTL_ADD, cli, &event); // Add the client socket to the list of file descriptors to poll
                if(s == -1) {
                    dzlog_error("epoll_ctl: %s", strerror(errno));
                    close(cli);
                    break;
                }
            }

            continue;
        } else {
            readClientData(events[i].data.fd);
        }
    }
}

当从客户端套接字读取数据时,将调用readClientData函数。让我们假设在该函数内部,我们调用一个从表中获取一些数据的数据库。如果由于某种原因,对数据库的调用挂起或花费的时间超过预期,则其他等待连接或发送数据的客户端也将被阻止。

例如,请考虑以下情形:

  1. 客户端1连接到服务器
  2. 客户端2连接到服务器
  3. 客户端1向服务器发送数据(这将导致调用readClientData函数来处理数据)
  4. readClientData函数调用数据库并等待响应。 (等待10秒或可能无限期挂起)
  5. 客户端2发送数据。当服务器仍在等待客户端1 readClientData完成时,无法处理此数据
  6. 新客户端3尝试连接但必须等待其连接被接受,因为服务器仍在处理来自客户端1的数据
  7. 有没有办法解决这个问题?

    由于

2 个答案:

答案 0 :(得分:1)

您可以为数据库读取,侦听套接字等等等操作专门设置单独的进程,以便您可以使用事件循环来检查数据库进程的发送/接收完成情况

在主进程中保持事件循环: 从客户端读取,在nowait模式下写入DB处理过程,返回事件循环以检查DB进程回复或来自客户端的新请求

答案 1 :(得分:0)

不必要的顺序化?

假设您提及readClientData()进行数据库查询是密切相关的;数据库非常聪明,现在任何一半不错的引擎都可以很愉快地为一个以上的客户服务。如果客户端不应该直接与数据库通信没有特别的理由,那么让他们这样做可能会使系统更快。

需要多个数据库垫片

如果这不是一个选项,那么使用进程/线程作为主事件循环和数据库之间的垫片(如Pras所描述的那样)是保持事件循环响应时间较短的一种方法。

但是,您可能不止一个,因此发出简短请求的客户端不会被另一个刚刚发出长请求的客户端阻止。只需一个垫片,它就会被迫等待长时间的请求,甚至在开始新的短请求之前完成。

这开始变得复杂;你需要多个进程/线程,你必须找到一种方法来在垫片之间分配传入的请求等。这是很多代码开始编写。

<强> ZeroMQ

幸运的是有一个答案;如果你使用ZeroMQ进行主事件循环和数据库填充程序之间的通信,你可以开始利用它的模式(=你自己没有编写大量的代码)。想到PUSH / PULL;可用于在填充程序中自动神奇地释放客户端请求。

如果你得到正确的高水位标记,新的客户端请求可能会“超过”长时间运行的请求,因为长时间运行的填充程序根本不会被赋予新的客户端请求。当然,前提是数据库引擎本身可以同时为所有垫片提供服务。