如何正确地为 ncurses 进行 epoll

How to correctly epoll for ncurses

原始问题:我写了一些客户端代码来监视键盘按下和服务器向其发送消息的 sockfd。问题是在服务器发出第一条消息后,epoll 不再由服务器消息触发。此外,每次我输入大约 10 次按键时,都会为 sockfd 触发 epoll 并读取一组字符(即使服务器已经发送了很多消息)。让我更加困惑的是,如果我一次只发送一个字符,epoll 能够做出正确的反应。多于一个的结果将与以前相同(epoll 没有反应)。

编辑:我意识到如果我将 STDIN_FILENO 设置为非阻塞,我将在适当的时候从服务器获取消息。但是,程序也会进入无限循环,STDIN_IN 总是被触发。我想现在的问题是如何正确地将 epoll 与 ncurses 一起使用,这样我们就不会陷入无限循环。

这是我的代码:

使用方法:

  1. clang++ client.cpp -lncurses -o cli
  2. clang++ server.cpp -o ser
  3. ./ser 8080
  4. 打开另一个终端
  5. ./cli 127.0.0.1 8080

我对 epoll 很陌生,所以恐怕我可能错过了一些东西。请让我知道我的代码有什么问题!

server.cpp:


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <chrono>
#include <errno.h>
#include <ifaddrs.h>
#include <sys/epoll.h>

#define TO_CLI_BUF_SIZE 32
#define FROM_CLI_BUF_SIZE 8

int main(int argc, char ** argv){

  //seed rand
  srand(time(NULL));

  int sockfd; // socket
  int port; // my port to listen on
  struct sockaddr_in serveraddr; // server's address
  struct sockaddr_in clientaddr;
  socklen_t clientlen;
  int currentAddrMax = 0;
  struct hostent * hostp; //host info
  char * hostaddrp; // host adddr string
  char toClientBuf[TO_CLI_BUF_SIZE];
  char fromClientBuf[FROM_CLI_BUF_SIZE];

  if(argc != 2){
    perror("usage: file <port>");
    exit(1);
  }
  port = atoi(argv[1]);

  // create socket
  sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  if(sockfd<0){
    perror("ERROR: opening socket.");
    exit(1);
  }

  bzero((char*) &serveraddr, sizeof(serveraddr));
  serveraddr.sin_family = AF_INET;
  serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
  serveraddr.sin_port = htons((unsigned short)port);

  if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0){
    perror("ERROR on bind");
    exit(1);
  }

  bzero(fromClientBuf, FROM_CLI_BUF_SIZE);
  clientlen = sizeof(clientaddr);
  int n = recvfrom(sockfd, fromClientBuf,FROM_CLI_BUF_SIZE, 0, (struct sockaddr*) &clientaddr, &(clientlen));

  while (1){ 
    bzero(toClientBuf, TO_CLI_BUF_SIZE);
    strcpy(toClientBuf, "alkjhkfqulw8fl128lh1oufo183hf1l[=10=]"); // I want to send 32 TO_CLI_BUF_SIZE
    int amountOfBytes = TO_CLI_BUF_SIZE; // anything greater than 1 will not work
    int n = sendto(sockfd, toClientBuf, amountOfBytes, 0, (struct sockaddr *) &clientaddr, clientlen);
    if(n < 0) {
      perror("ERROR in sendto");
      exit(1);
    }

    sleep(1); // sleep 1 sec
  }

  return 0;

}

client.cpp:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h> 
#include <ncurses.h>
#include <sys/epoll.h>
#include <sys/fcntl.h>
#define FROM_SER_BUF_SIZE 32
#define TO_SER_BUF_SIZE 8

int main(int argc, char **argv){

  int sockfd, portno, n;
  socklen_t serverlen;
  struct sockaddr_in serveraddr;
  struct hostent *server;
  char *hostname;

  char toServerBuf[TO_SER_BUF_SIZE];
  char fromServerBuf[FROM_SER_BUF_SIZE];

  if (argc != 3) {
    perror("usage: filename <hostname> <port>\n");
    exit(0);
  }
  hostname = argv[1];
  portno = atoi(argv[2]);

  sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sockfd < 0) {
    perror("ERROR: opening sockets\n");
    exit(0);
  }

  server = gethostbyname(hostname);
  if (server == NULL) {
    fprintf(stderr,"ERROR, no such host as %s\n", hostname);
    exit(0);
  }

  bzero((char *) &serveraddr, sizeof(serveraddr));
  serveraddr.sin_family = AF_INET;
  bcopy((char *)server->h_addr, 
  (char *)&serveraddr.sin_addr.s_addr, server->h_length);
  serveraddr.sin_port = htons(portno);
  serverlen = sizeof(serveraddr);
  bzero(toServerBuf, TO_SER_BUF_SIZE);
  n = sendto(sockfd, toServerBuf, TO_SER_BUF_SIZE, 0, ( struct sockaddr *) &serveraddr, serverlen);
  if (n < 0){
    perror("ERROR: sendto");
    exit(0);
  }

  if(connect(sockfd, (struct sockaddr *)&serveraddr, serverlen) < 0) { 
    printf("\n Error : Connect Failed \n"); 
    exit(0); 
  } 
  fcntl(sockfd, F_SETFL, O_NONBLOCK); 
  nodelay(stdscr, true);
  fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); 
  initscr();
  noecho();
  int keyboardtick = 0;
  int servertick = 0;
  int ep = epoll_create1(0);
  struct epoll_event e1,e2, e[2]; // e1 for serverfd, e2 for stdin
  memset(&e1, 0, sizeof(struct epoll_event));
  e1.events = EPOLLIN; 
  e1.data.fd = sockfd;
  epoll_ctl(ep, EPOLL_CTL_ADD, sockfd, &e1); 
  memset(&e2, 0, sizeof(struct epoll_event));
  e2.events = EPOLLIN; 
  e2.data.fd = STDIN_FILENO;
  epoll_ctl(ep, EPOLL_CTL_ADD, STDIN_FILENO, &e2);

  mvprintw(0,0,"ticks from server: %d",servertick);
  mvprintw(2,0,"ticks from keyboard: %d",keyboardtick);
  while (1){ 
    int n = epoll_wait(ep, e, 2, -1);
    for(int i = 0; i < n; i++){
      if (e[i].data.fd == sockfd) { // from server
        //
        bzero(fromServerBuf, FROM_SER_BUF_SIZE);
        n = recvfrom(sockfd, fromServerBuf, FROM_SER_BUF_SIZE, 0,( struct sockaddr *) &serveraddr, &serverlen);
        if(n < 0) {
          perror("ERROR in recv");
          exit(1);
        }
        servertick+=n;
        mvprintw(0,0,"ticks from server: %d",servertick);
      }else if(e[i].data.fd == STDIN_FILENO){
        char c = getch();
        keyboardtick++;
        mvprintw(2,0,"ticks from keyboard: %d",keyboardtick);
      }
    }
    refresh();
  }


  endwin();

  return 0;
}
e1.events = EPOLLIN|EPOLLET;

您已选择使用边沿触发的 epoll 事件,在此设置下。

epoll 手册页对它的含义以及如何正确使用边缘触发的 epoll 事件进行了长时间的讨论。您应该查看该说明。

让我们跟随:

    int n = epoll_wait(ep, e, 2, -1);

// ...
        n = recvfrom(sockfd, fromServerBuf, FROM_SER_BUF_SIZE, 0,( struct sockaddr *) &serveraddr, &serverlen);

如果 epoll 是由这个套接字接收超过 FROM_SER_BUF_SIZE 个字节触发的,这只会读取第一个 FROM_SER_BUF_SIZE 个字节——换句话说,发送方足够快地发送足够的数据,这将读取前 FROM_SER_BUF_SIZE 个字节就好了。当这个returns到epoll_wait()时,又会等待。即使套接字上有更多数据要读取。这就是边缘触发 epoll 事件的工作原理。您在下面描述了边缘触发的 epoll 事件:

The problem is after the first message from the server, epoll is no longer triggered by messages from the server. Also, every time I enter key-presses around 10 times, epoll is triggered for sockfd and one pack of characters are read (even though the server has already sent alot of messages).

正确。您的 epoll 是边沿触发的。所示逻辑仅使用第一个数据报并再次调用 epoll,即使套接字有更多未读数据报也是如此。

What adds to my confusion is the if I only send one character at a time, epoll is able to react properly. Anything more than one, will have the same result as before (epoll doesn't react).

再次更正。如果只发送了一条消息,它会按照显示的逻辑进行处理,然后 epoll 再次等待。

边缘触发的epoll 将等待事件再次在套接字上发生。句号。故事结局。如果事件已经发生在套接字上,边缘触发的 epoll 将等待而不是立即 returning。 TLDR:如果套接字有未读数据,epoll_waiting for POLLIN 不会立即 return。它仅在接收到更多数据后 returns。如果曾经。这就是边缘触发 epoll 的工作原理。

如果你打算使用边缘触发的epoll逻辑,手册页说明你必须使用非阻塞套接字,只有在套接字完全为空后才再次调用epoll_wait,或完整(换句话说,反复循环读取或写入东西,直到你不能)。

或者,不要使用边缘触发的 epoll 事件。然而你应该使用非阻塞套接字,即使是非边缘触发的epolls。您必须深入研究手册页,但您会发现 epoll 包含 poll,而 poll 包含 select,您将在其中找到找到以下 gem:

   Under Linux, select() may report a socket file descriptor as "ready for
   reading", while nevertheless a subsequent read blocks.  This could  for
   example  happen  when  data  has arrived but upon examination has wrong
   checksum and is discarded.  There may be other circumstances in which a
   file  descriptor is spuriously reported as ready.  Thus it may be safer
   to use O_NONBLOCK on sockets that should not block.

因此,实际上,为了获得最大的可移植性,事实证明使用边缘触发还是水平触发的 epoll 似乎并不重要。为了避免追逐幽灵,最好的做法似乎是(根据可用文档),始终使用 select/poll/epoll 的非阻塞套接字并继续向前推动铲子,直到用完铲子为止,并且只然后召唤一个select/poll/epoll.