Как активировать select () при закрытии сокета? - PullRequest
7 голосов
/ 25 августа 2009

В настоящее время я использую цикл выбора для управления сокетами в прокси. Одним из требований этого прокси-сервера является то, что если прокси-сервер отправляет сообщение на внешний сервер и не получает ответ в течение определенного времени, прокси-сервер должен закрыть этот сокет и попытаться подключиться к вторичному серверу. Закрытие происходит в отдельном потоке, в то время как поток выбора блокирует ожидание активности.

У меня проблемы с выяснением того, как определить, что этот сокет специально закрыт, чтобы я мог справиться с ошибкой. Если я вызываю close () в другом потоке, я получаю EBADF, но не могу сказать, какой сокет закрыт. Я пытался обнаружить сокет через исключение fdset, думая, что он будет содержать закрытый сокет, но я ничего не возвращаю туда. Я также слышал, что вызов shutdown () отправит FIN на сервер и вернет FIN, чтобы я мог закрыть его; но в том-то и дело, что я пытаюсь закрыть это из-за отсутствия ответа в течение периода ожидания, поэтому я тоже не могу этого сделать.

Если мои предположения здесь неверны, дайте мне знать. Любые идеи будут оценены.

EDIT: В ответ на предложения об использовании тайм-аута выбора: мне нужно выполнить закрытие асинхронно, потому что время ожидания подключения клиента к прокси-серверу истекло, и я не могу дождаться, когда выбор будет опрошен. Это сработало бы только в том случае, если бы я сделал тайм-аут выбора очень маленьким, что потом постоянно будет опрашивать и тратить ресурсы, которые мне не нужны.

Ответы [ 7 ]

9 голосов
/ 25 августа 2009

Обычно я просто отмечаю сокет для закрытия в другом потоке, а затем, когда select () возвращается из активности или тайм-аута, я запускаю очистку и закрываю все мертвые соединения и обновляю fd_set. Выполнение этого каким-либо другим способом открывает вам условия состязания, когда вы отказались от соединения, точно так же, как select (), наконец, распознал некоторые данные для него, затем вы закрываете его, но другой поток пытается обработать обнаруженные данные и получает расстроен, чтобы найти соединение закрыто.

Да, и poll () обычно лучше, чем select (), с точки зрения того, что не нужно копировать столько данных.

3 голосов
/ 16 сентября 2011

Вы не можете освободить ресурс в одном потоке, когда другой поток использует или может использовать его. Вызов close для сокета, который может использоваться в другом потоке, никогда не будет работать правильно. Всегда будут потенциально катастрофические условия гонки.

Есть два хороших решения вашей проблемы:

  1. Пусть поток, вызывающий select, всегда использует тайм-аут, не превышающий самый длинный, который вы готовы ждать, чтобы обработать тайм-аут. Когда происходит тайм-аут, укажите, что какое-то место поток, который вызывает select, заметит, когда он вернется из select. Пусть этот поток выполняет close промежуточных вызовов сокета на select.

  2. Есть поток, который обнаруживает вызов тайм-аута shutdown на сокете. Это заставит select вернуться, а затем этот поток сделает close.

1 голос
/ 26 августа 2009

Как справиться с EBADF при выборе ():

int fopts = 0;
for (int i = 0; i < num_clients; ++i) {
    if (fcntl(client[i].fd, F_GETFL, &fopts) < 0) {
        // call close(), FD_CLR(), and remove i'th element from client list
    }
}

В этом коде предполагается, что у вас есть массив клиентских структур, которые имеют члены "fd" для дескриптора сокета. Вызов fcntl () проверяет, является ли сокет все еще «живым», и если нет, мы делаем то, что должны, чтобы удалить мертвый сокет и связанную с ним информацию о клиенте.

0 голосов
/ 21 декабря 2012

Просто запустите "тест выбора" на каждом сокете, который мог закрыться с нулевым тайм-аутом, и проверяйте результат выбора и ошибочно, пока не найдете тот, который закрылся.

Следующий фрагмент демонстрационного кода запускает два сокета сервера в отдельных потоках и создает два сокета клиента для подключения к любому из сокетов сервера. Затем он запускает другой поток, который случайным образом уничтожит один из клиентских сокетов через 10 секунд (он просто закроет его). Закрытие любого клиентского сокета приводит к сбою select с ошибкой в ​​основном потоке, и код ниже теперь проверяет, какой из двух сокетов действительно закрылся.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <pthread.h>
#include <stdbool.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/socket.h>


static void * serverThread ( void * threadArg )
{
    int res;
    int connSo;
    int servSo;
    socklen_t addrLen;
    struct sockaddr_in soAddr;
    uint16_t * port = threadArg;

    servSo = socket(PF_INET, SOCK_STREAM, 0);
    assert(servSo >= 0);

    memset(&soAddr, 0, sizeof(soAddr));
    soAddr.sin_family = AF_INET;
    soAddr.sin_port = htons(*port);

    // Uncommend line below if your system offers this field in the struct
    // and also needs this field to be initialized correctly.
//  soAddr.sin_len = sizeof(soAddr);

    res = bind(servSo, (struct sockaddr *)&soAddr, sizeof(soAddr));
    assert(res == 0);

    res = listen(servSo, 10);
    assert(res == 0);

    addrLen = 0;
    connSo = accept(servSo, NULL, &addrLen);
    assert(connSo >= 0);

    for (;;) {
        char buffer[2048];
        ssize_t bytesRead;

        bytesRead = recv(connSo, buffer, sizeof(buffer), 0);
        if (bytesRead <= 0) break;

        printf("Received %zu bytes on port %d.\n", bytesRead, (int)*port);
    }
    free(port);
    close(connSo);
    close(servSo);
    return NULL;
}

static void * killSocketIn10Seconds ( void * threadArg )
{
    int * so = threadArg;

    sleep(10);
    printf("Killing socket %d.\n", *so);
    close(*so);
    free(so);
    return NULL;
}


int main ( int argc, const char * const * argv )
{
    int res;
    int clientSo1;
    int clientSo2;
    int * socketArg;
    uint16_t * portArg;
    pthread_t killThread;
    pthread_t serverThread1;
    pthread_t serverThread2;
    struct sockaddr_in soAddr;

    // Create a server socket at port 19500
    portArg = malloc(sizeof(*portArg));
    assert(portArg != NULL);
    *portArg = 19500;
    res = pthread_create(&serverThread1, NULL, &serverThread, portArg);
    assert(res == 0);

    // Create another server socket at port 19501
    portArg = malloc(sizeof(*portArg));
    assert(portArg != NULL);

    *portArg = 19501;
    res = pthread_create(&serverThread1, NULL, &serverThread, portArg);
    assert(res == 0);

    // Create two client sockets, one for 19500 and one for 19501
    // and connect both to the server sockets we created above.

    clientSo1 = socket(PF_INET, SOCK_STREAM, 0);
    assert(clientSo1 >= 0);

    clientSo2 = socket(PF_INET, SOCK_STREAM, 0);
    assert(clientSo2 >= 0);

    memset(&soAddr, 0, sizeof(soAddr));
    soAddr.sin_family = AF_INET;
    soAddr.sin_port = htons(19500);
    res = inet_pton(AF_INET, "127.0.0.1", &soAddr.sin_addr);
    assert(res == 1);

    // Uncommend line below if your system offers this field in the struct
    // and also needs this field to be initialized correctly.
//  soAddr.sin_len = sizeof(soAddr);

    res = connect(clientSo1, (struct sockaddr *)&soAddr, sizeof(soAddr));
    assert(res == 0);

    soAddr.sin_port = htons(19501);
    res = connect(clientSo2, (struct sockaddr *)&soAddr, sizeof(soAddr));
    assert(res == 0);

    // We want either client socket to be closed locally after 10 seconds.
    // Which one is random, so try running test app multiple times.
    socketArg = malloc(sizeof(*socketArg));
    srandomdev();
    *socketArg = (random() % 2 == 0 ? clientSo1 : clientSo2);
    res = pthread_create(&killThread, NULL, &killSocketIn10Seconds, socketArg);
    assert(res == 0);

    for (;;) {
        int ndfs;
        int count;
        fd_set readSet;

        // ndfs must be the highest socket number + 1
        ndfs = (clientSo2 > clientSo1 ? clientSo2 : clientSo1);
        ndfs++;

        FD_ZERO(&readSet);
        FD_SET(clientSo1, &readSet);
        FD_SET(clientSo2, &readSet);

        // No timeout, that means select may block forever here.
        count = select(ndfs, &readSet, NULL, NULL, NULL);

        // Without a timeout count should never be zero.
        // Zero is only returned if select ran into the timeout.
        assert(count != 0);

        if (count < 0) {
            int error = errno;

            printf("Select terminated with error: %s\n", strerror(error));

            if (error == EBADF) {
                fd_set closeSet;
                struct timeval atonce;

                FD_ZERO(&closeSet);
                FD_SET(clientSo1, &closeSet);
                memset(&atonce, 0, sizeof(atonce));
                count = select(clientSo1 + 1, &closeSet, NULL, NULL, &atonce);
                if (count == -1 && errno == EBADF) {
                    printf("Socket 1 (%d) closed.\n", clientSo1);
                    break; // Terminate test app
                }

                FD_ZERO(&closeSet);
                FD_SET(clientSo2, &closeSet);
                // Note: Standard requires you to re-init timeout for every
                // select call, you must never rely that select has not changed
                // its value in any way, not even if its all zero.
                memset(&atonce, 0, sizeof(atonce));
                count = select(clientSo2 + 1, &closeSet, NULL, NULL, &atonce);
                if (count == -1 && errno == EBADF) {
                    printf("Socket 2 (%d) closed.\n", clientSo2);
                    break; // Terminate test app
                }
            }
        }
    }
    // Be a good citizen, close all sockets, join all threads
    close(clientSo1);
    close(clientSo2);
    pthread_join(killThread, NULL);
    pthread_join(serverThread1, NULL);
    pthread_join(serverThread2, NULL);

    return EXIT_SUCCESS;
}

Пример вывода для запуска этого тестового кода дважды:

$ ./sockclose 
Killing socket 3.
Select terminated with error: Bad file descriptor
Socket 1 (3) closed.

$  ./sockclose 
Killing socket 4.
Select terminated with error: Bad file descriptor
Socket 1 (4) closed.

Однако, если ваша система поддерживает poll(), я настоятельно рекомендую вам использовать этот API вместо select(). Select - довольно уродливый, устаревший API из прошлого, оставленный там только для обратной совместимости с существующим кодом. У опроса гораздо лучший интерфейс для этой задачи, и у него есть дополнительный флаг, чтобы напрямую сигнализировать вам, что сокет закрыт локально: POLLNVAL будет установлен на revents, если этот сокет был закрыт, независимо от того, какие флаги вы запрашивали в событиях. , поскольку POLLNVAL является флажком только для вывода, это означает, что он игнорируется при установке на events. Если сокет не был закрыт локально, но удаленный сервер только что закрыл соединение, флаг POLLHUP будет установлен в revents (также флаг только для вывода). Еще одним преимуществом опроса является то, что тайм-аут представляет собой просто значение типа int (миллисекунды, достаточно мелкозернистые для реальных сетевых сокетов) и что нет никаких ограничений на количество отслеживаемых сокетов или диапазон их числовых значений.

0 голосов
/ 26 августа 2009

Если вы используете poll (2), как предложено в других ответах, вы можете использовать состояние POLLNVAL, которое по сути является EBADF, но для каждого дескриптора файла, а не для всего системного вызова, как для select ( 2).

0 голосов
/ 25 августа 2009

Трудно комментировать, когда видишь только небольшую часть слона, но, может быть, ты слишком усложняешь вещи?

Предположительно, у вас есть некоторая структура для отслеживания каждого сокета и его информации (например, оставшееся время для получения ответа). Вы можете изменить цикл выбора (), чтобы использовать тайм-аут. В нем проверьте, не пора ли закрыть сокет. Делайте то, что вам нужно сделать для закрытия и не добавляйте это в наборы fd в следующий раз.

0 голосов
/ 25 августа 2009

Используйте тайм-аут для выбора, и, если последовательности чтения-готовности / записи-готовности / наличия-ошибки все пусты (с этим сокетом), проверьте, был ли он закрыт.

...