如何在多个连接之间进行epoll切换?

时间:2012-01-17 07:31:45

标签: c++ sockets epoll

我正在使用epoll,我认为这是TCP套接字的典型方式(主要基于this example,但略微适应C ++);绑定到端口的一个主侦听套接字,每个新的连接套接字(来自accept())也会在它准备好recv()时添加警报。我创建了一个测试脚本,基本上用连接和发送/接收来锤击它。当任何一个客户端连接时,它将完美无缺地工作。

但是,添加第二个同步测试客户端会导致其中一个挂起并失败。经过几天的调试,我终于决定将它正在处理的套接字ID吐出到一个文件中,我对我发现的东西感到困惑。

当一个脚本启动时,我只得到一个流,在这种情况下,6。然而,当第二个脚本启动时,我得到一个7的流。只是 7.它仍然在7,专门与第二个客户端进行通信,完全忽略第一个客户端,直到第一个客户端达到超时并关闭。 (然后,当客户端2重新连接时,它会获得ID 6。)

值得注意的是,此测试脚本不使用持久连接,它会在一些消息来回传递后断开连接并重新连接(以获得更准确的模拟)。但即使这样,客户端1也会被忽略。如果我将超时设置得足够高以至于客户端2实际上有时间退出,那么仍然不会继续使用客户端1,因为它等待的只是有点丢失。

这是一种正常的行为,对于epoll(或一般的套接字)来说,当一个新任务出现时完全放弃先前的任务?我必须指定一些选项吗?

编辑:这是我能展示的代码数量;我不一定期待"这就是你做错了什么",更多的是"这些是一些会破坏/解决类似情况的事情"。

#define EVENTMODE (EPOLLIN | EPOLLET | EPOLLRDHUP | EPOLLHUP)
#define ERRCHECK (EPOLLERR | EPOLLHUP | EPOLLRDHUP)

//Setup event buffer:
struct epoll_event* events = (epoll_event*)calloc(maxEventCount, sizeof(event));

//Setup done, main processing loop:
int iter, eventCount;
while (1) {

    //Wait for events indefinitely:
    eventCount = epoll_wait(pollID, events, maxEventCount, -1);
    if (eventCount < 0) {
        syslog(LOG_ERR, "Poll checking error, continuing...");
        continue;
    }
    for (iter = 0; iter<eventCount; ++iter) {
        int currFD = events[iter].data.fd;
        cout << "Working with " << events[iter].data.fd << endl;
        if (events[iter].events & ERRCHECK) {
            //Error or hangup:
            cout << "Closing " << events[iter].data.fd << endl;
            close(events[iter].data.fd);
            continue;
        } else if (!(events[iter].events & EPOLLIN)) {
            //Data not really ready?
            cout << "Not ready on " << events[iter].data.fd << endl;
            continue;
        } else if (events[iter].data.fd == socketID) {
            //Event on the listening socket, incoming connections:
            cout << "Connecting on " << events[iter].data.fd << endl;

            //Set up accepting socket descriptor:
            int acceptID = accept(socketID, NULL, NULL);
            if (acceptID == -1) {
                //Error:
                if (!(errno == EAGAIN || errno == EWOULDBLOCK)) {
                    //NOT just letting us know there's nothing new:
                    syslog(LOG_ERR, "Can't accept on socket: %s", strerror(errno));
                }
                continue;
            }
            //Set non-blocking:
            if (setNonBlocking(acceptID) < 0) {
                //Error:
                syslog(LOG_ERR, "Can't set accepting socket non-blocking: %s", strerror(errno));
                close(acceptID);
                continue;
            }
            cout << "Listening on " << acceptID << endl;
            //Add event listener:
            event.data.fd = acceptID;
            event.events = EVENTMODE;
            if (epoll_ctl(pollID, EPOLL_CTL_ADD, acceptID, &event) < 0) {
                //Error adding event:
                syslog(LOG_ERR, "Can't edit epoll: %s", strerror(errno));
                close(acceptID);
                continue;
            }

        } else {
            //Data on accepting socket waiting to be read:
            cout << "Receive attempt on " << event.data.fd << endl;
            cout << "Supposed to be " << currFD << endl;
            if (receive(event.data.fd) == false) {
                sendOut(event.data.fd, streamFalse);
            }
        }
    }
}

编辑:代码已经修改,删除边缘触发确实会阻止epoll锁定到一个客户端。客户端无法接收数据仍存在问题;正在进行调试以查看它是否是同一个问题或其他问题。

编辑:在不同的诉讼中似乎是同样的错误。它尝试在第二个套接字上接收,但是进一步的日志报告几乎每次都报告它实际上都会触发EWOULDBLOCK。有趣的是,日志报告的活动比保证的要多得多 - 超过150,000行,当我预计大约有60,000行时。删除所有&#34;将阻止&#34;线将它减少到大约我期望的数字......并且看,结果线创建完全相同的模式。将边缘触发放回会停止阻塞行为,显然会阻止它在没有明显原因的情况下尽可能快地旋转其轮子。仍然没有解决原来的问题。

编辑:为了掩盖我的基础,我想我会在发送端做更多调试,因为挂起的客户端显然正在等待它永远不会得到的消息。但是,我可以确认服务器为它处理的每个请求发送响应;挂起的客户的请求完全丢失,因此从未回复过。

我还确保我的接收循环读取,直到它实际命中EWOULDBLOCK(这通常是不必要的,因为我的消息头的前两个字节包含消息大小),但它没有改变任何东西。

&#39; Nother EDIT:我应该澄清一下,这个系统使用请求/回复格式,接收,处理和发送都是一次性完成的。正如您可能猜到的,这需要读取接收缓冲区,直到它为空,这是边沿触发模式的主要要求。如果收到的消息不完整(绝不应该发生),服务器基本上会向客户端返回false,虽然从技术上讲,错误仍然允许客户端继续进行另一个请求。

调试已确认要挂起的客户端会发出请求,并等待响应,但该请求永远不会触发epoll中的任何内容 - 完全在第二个客户端连接后忽略第一个客户端

我也接受了接受后立即收到的企图;在十万次尝试中,它还没有准备好一次。

更多编辑:很好,很好 - 如果有一件事可以让我进入任意任务,那就是质疑我的能力。所以,在这里,一切都必须出错的功能:

bool receive(int socketID)
{
short recLen = 0;
char buff[BUFFERSIZE];
FixedByteStream received;
short fullSize = 0;
short diff = 0;
short iter = 0;
short recSoFar = 0;

//Loop through received buffer:
while ((recLen = read(socketID, buff, BUFFERSIZE)) > 0) {
    cout << "Receiving on " << socketID << endl;
    if (fullSize == 0) {
        //We don't know the size yet, that's the first two bytes:
        fullSize = ntohs(*(uint16_t*)&buff[0]);
        if (fullSize < 4 || recLen < 4) {
            //Something went wrong:
            syslog(LOG_ERR, "Received nothing.");
            return false;
        }
        received = FixedByteStream(fullSize);
    }
    diff = fullSize - recSoFar;
    if (diff > recLen) {
        //More than received bytes left, get them all:
        for (iter=0; iter<recLen; ++iter) {
            received[recSoFar++] = buff[iter];
        }
    } else {
        //Less than or equal to received bytes left, get only what we need:
        for (iter=0; iter<diff; ++iter) {
            received[recSoFar++] = buff[iter];
        }
    }
}
if (recLen < 0 && errno == EWOULDBLOCK) {
    cout << "Would block on " << socketID << endl;
}
if (recLen < 0 && errno != EWOULDBLOCK) {
    //Had an error:
    cout << "Error on " << socketID << endl;
    syslog(LOG_ERR, "Connection receive error: %s", strerror(errno));
    return false;
} else if (recLen == 0) {
    //Nothing received at all?
    cout << "Received nothing on " << socketID << endl;
    return true;
}
if (fullSize == 0) {
    return true;
}

//Store response, since it needs to be passed as a reference:
FixedByteStream response = process(received);
//Send response:
sendOut(socketID, response);
return true;
}

如您所见,遇到错误后无法循环播放。我可能不会使用很多C ++,但是我已经编写了足够长的代码来检查这些错误,然后再寻求帮助。

bool sendOut(int socketID, FixedByteStream &output)
{
cout << "Sending on " << socketID << endl;
//Send to socket:
if (write(socketID, (char*)output, output.getLength()) < 0) {
    syslog(LOG_ERR, "Connection send error: %s", strerror(errno));
    return false;
}

return true;
}

如果它是EWOULDBLOCK的话怎么办?就像我的主板融化一样 - 我会解决它。但它还没有发生,所以我不打算解决它,我只是确保我知道它是否需要修复。

不,process()不会使用套接字任何,它只接受并返回一个固定长度的char数组。同样,这个程序与一个客户端完美配合,而不是两个或更多。

上次编辑:经过更多调试后,我找到了问题的根源。我会继续自己回答。

3 个答案:

答案 0 :(得分:1)

1)不要使用EPOLLET。这是方式更复杂。

2)在receiveread功能中,确保在获得EWOULDBLOCK后不再呼叫readreceive。返回等待epoll点击。

3)不要试图查看数据或测量数据量。请尽快阅读。

4)在关闭套接字之前从epoll集中移除套接字,除非你肯定没有其它对底层连接端点的引用。

真的很简单。如果你做正确的四件事,你就不会有问题。最有可能的是,你拙劣2

另外,当你去发送时,你如何应对'EWOULDBLOCK'?你的sendOut函数是什么样的? (有很多正确的方法可以做到,但也有很多错误的方法。)

答案 1 :(得分:1)

event.data.fd?你为什么要用它? events[iter].data.fd 是您希望收到的值。您可能希望更明确地命名变量以避免将来出现此问题,这样您就不会浪费每个人的时间。这显然不是epoll的问题。

答案 2 :(得分:0)

修改原来的答案。

我看到一些可疑的东西,我有一些建议。

  1. 当发出侦听套接字信号时,代码进入无限循环,直到接受失败。我想知道循环是否优先接受新连接而不是处理epoll事件。也就是说,你总是有一个准备接受的连接,你永远不会打破内部while(1)循环。不要循环接受。相反,当添加到epoll时,使侦听套接字不会被边缘触发。然后一次只接受一个连接 - 这样后续的epoll事件将在接受返回后得到处理。换句话说,将内部“while(1)”循环出来。

  2. 在您的接受调用返回一个有效的套接字后(并且您完成了非阻塞并通过边缘触发添加到epoll中),继续并在接受的套接字上调用您的接收函数。我假设你的接收函数可以处理EWOULDBLOCK和EAGAIN错误。换句话说,对于边缘触发的套接字,不要假设您将获得新套接字的EPOLLIN通知。无论如何,试着接受它。如果没有数据,您将在数据到达时收到EPOLLIN通知。

  3. 为什么你没有收听关于sendOut功能的EPOLLOUT? sendOut会将套接字更改回阻止吗?在任何情况下,当receive()返回成功时,将套接字上的epoll侦听器更改为EPOLLOUT,然后尝试对sendOut函数进行伺机调用,就好像刚刚收到EPOLLOUT通知一样。

  4. 如果所有其他方法都失败了,请考虑完全关闭边缘触发(EPOLLET)行为。也许您的接收函数没有消耗第一个EPOLLIN通知中的所有数据。

  5. 如果在添加新套接字时epoll_ctl失败,那么杀死整个应用程序似乎有点苛刻。我只是关闭有问题的套接字,断言并继续。