Как сделать, чтобы epoll переключался между несколькими соединениями? - PullRequest
4 голосов
/ 17 января 2012

Я использую epoll, как я полагаю, типичным способом для сокетов TCP (в основном на этом примере , но немного адаптирован к C ++); один основной прослушивающий сокет привязан к порту, и каждый новый сокет подключения (от accept ()) также добавляется для оповещений, когда он готов для recv (). Я создал тестовый скрипт, который в основном забивает его соединениями и отправляет / получает. Когда подключен какой-либо один клиент, он будет работать безупречно, бесконечно.

Однако добавление второго тестового клиента одновременно приведет к зависанию одного из них. После пары дней отладки я, наконец, решил просто выдать идентификатор сокета, с которым он работает, в файл, и я озадачен тем, что нашел.

Когда запускается один скрипт, я получаю только поток, в данном случае, 6. Однако, когда запускается второй скрипт, я получаю поток 7. Просто 7. И он остается на 7 , исключительно общение со вторым клиентом, полностью игнорируя первого, пока первый не достигнет своего таймаута и не закроется. (Затем, когда клиент 2 переподключается, вместо этого он получает идентификатор 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. Удаление всех строк «Будет блокировать» уменьшает его примерно до ожидаемого числа ... и вот, результирующие строки создают точно такой же шаблон. Помещение спускового механизма обратно в останавливает поведение блокирующего блока, по-видимому, предотвращая его вращение колес настолько быстро, насколько это возможно без видимой причины. Все еще не решает исходную проблему.

РЕДАКТИРОВАТЬ: Просто чтобы покрыть мои базы, я решил сделать больше отладки на отправляющей стороне, так как зависший клиент, очевидно, ждет сообщения, которое он никогда не получит. Однако я могу подтвердить, что сервер отправляет ответ на каждый запрос, который он обрабатывает; запрос зависшего клиента просто полностью теряется и поэтому никогда не отвечает.

Я также удостоверился, что мой цикл приема читает до тех пор, пока он фактически не достигнет EWOULDBLOCK (это обычно не нужно, потому что первые два байта моего заголовка сообщения содержат размер сообщения), но это ничего не изменило.

'NED EDIT: Я, вероятно, должен уточнить, что эта система использует формат запроса / ответа, а получение, обработка и отправка выполняются за один раз.Как вы можете догадаться, это требует чтения приемного буфера, пока он не опустеет, что является основным требованием для режима запуска по фронту.Если полученное сообщение является неполным (что никогда не должно происходить), сервер в основном возвращает ложное значение клиенту, что, хотя технически ошибка все равно позволит клиенту продолжить другой запрос.

Отладка подтвердила, что клиентзависание отправит запрос и будет ждать ответа, но этот запрос никогда не вызывает ничего в 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 () не делает что-нибудь с сокетами, он принимает и возвращает только массив символов фиксированной длины.Опять же, эта программа отлично работает с одним клиентом, но не с двумя или более.

Последнее редактирование: После еще большей отладки я нашел источник проблемы.Я просто пойду и отвечу сам.

Ответы [ 3 ]

1 голос
/ 18 января 2012

1) Не используйте EPOLLET.Это способ более сложный.

2) В вашей функции receive или read убедитесь, что вы не вызываете read или receive снова после получения EWOULDBLOCK.Вернитесь к ожиданию удара epoll.

3) Не пытайтесь заглянуть в данные или измерить их объем.Просто прочитайте его как можно быстрее.

4) Удалите сокет из набора epoll перед его закрытием, если только вы не уверены, что нет другой ссылки на конечную точку базового соединения.

Это действительно настолько просто.Если вы сделаете эти четыре вещи правильно, у вас не будет проблем.Скорее всего, вы испортили 2.

Кроме того, как вы справляетесь с EWOULDBLOCK, когда вы отправляете?Как выглядит ваша sendOut функция?(Есть много правильных способов сделать это, но также есть много неправильных способов.)

1 голос
/ 19 января 2012

event.data.fd? Почему вы пытаетесь использовать это? events[iter].data.fd - это значение, которое вы хотите получить. Возможно, вы захотите назвать свои переменные более четко, чтобы избежать этой проблемы в будущем, чтобы не тратить время каждого. Это явно не проблема с epoll.

0 голосов
/ 17 января 2012

Пересмотр моего первоначального ответа.

Я вижу несколько подозрительных вещей, и у меня есть некоторые предложения.

  1. Когда сокет прослушивания сигнализируется, код переходит в бесконечный цикл, пока не произойдет сбой принятия. Интересно, отдает ли цикл приоритет принятию новых соединений вместо обработки событий epoll. То есть у вас всегда есть соединение, готовое к принятию, и вы никогда не выходите из внутреннего цикла while (1). Не зацикливайтесь на принятии. Вместо этого, сделайте слушающий сокет НЕ краем, срабатывающим при добавлении в epoll. Затем просто принимайте одно соединение за раз - так, чтобы последующие события epoll обрабатывались после возврата return. Другими словами, возьмите этот внутренний цикл while (1).

  2. После того, как ваш вызов на возврат вернет действительный сокет (и вы закончите, сделав его неблокирующим и добавив его в epoll с триггерным фронтом), продолжайте и вызовите функцию приема на принятом сокете. Я предполагаю, что ваша функция приема может обрабатывать ошибки EWOULDBLOCK и EAGAIN. Другими словами, для сокетов, запускаемых фронтом, не думайте, что вы получите уведомление EPOLLIN для нового сокета. Просто попробуйте получить его в любом случае. Если данных нет, вы получите уведомление EPOLLIN позже, когда данные поступят.

  3. Почему вы не слушаете EPOLLOUT в отношении вашей функции sendOut? SendOut меняет сокет обратно на блокировку? В любом случае, когда receive () возвращает успех, измените ваш слушатель epoll на сокете на EPOLLOUT, затем попробуйте произвольный вызов функции sendOut, как если бы вы только что получили уведомление EPOLLOUT.

  4. И если ничего не помогает, попробуйте полностью отключить поведение с триггером (EPOLLET). Возможно, ваша функция получения не использует все данные из первого уведомления EPOLLIN.

  5. Если epoll_ctl завершается неудачно при добавлении нового сокета, это кажется довольно резким, чтобы убить все приложение. Я бы просто закрыл поврежденный сокет, подтвердил и продолжил.

...