Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > C/C++: Сети > POLLHUP и SHUT_RD


Автор: NightGoblin 8.9.2008, 23:08
Даже не знаю с чего начать, но всё же попробую как-то упорядочить многомерную проблему smile

Общая ситуация:

Есть TCP мультиплексор соединений на C. Клиенты сервера посылают запрос и дальше по долговременному соединению получают данные.

Основной вопрос:

Как, игнорируя данные от клиентов, отследить их "корректное" отключение от сервера (TCP FIN)? О "вываливании", когда связь с клиентом просто потеряна (пропадание электричества, перегрызенный провод, упавший раутер, и т. п.) речи не идёт -- тут нужен другой механизм, и с ним я буду разбираться отдельно.

Проблема:

Зависит от того, с какой стороны смотреть. С одной стороны, poll(2) не возвращает POLLHUP когда отваливаются соединения. С другой стороны, посылаемые клиентами данные забивают буфер ввода или заставляют сервер читать ненужный мусор. С третьей, если сокет "прикрыть" по shutdown(fd, SHUT_RD), poll с events=POLLIN возвращается сразу, а read(2)/recv(2) читают 0 байт и, естественно, мы получаем все признаки закрытого соединения.

Более литературно:

Как уже, надеюсь, понятно, сокеты клиентов опрашиваются при помощи poll(2). Проблема в том, что хочется получить запрос от клиента и дальше игнорировать все данные -- тут вроде помогает shutdown на SHUT_RD. Пока всё хорошо. Читаем мануал к poll(2), в котором значится POLLHUP, который якобы возвращается при закрытии соединения. Так вот, он почему-то не возвращается. Гугление данного вопроса привело меня к тому, что разные ОС обрабатывают это событие по-разному: одни возвращают POLLHUP, другие POLLIN, который нужно проверять read(2)/recv(2) на наличие входных данных: если данных нет, то и толку от такого сокета -- тоже. Пока ожидается запрос от клиента, всё хорошо; только вот сокет, закрытый на чтение, заведомо вернёт POLLIN (ибо чтение из такого сокета блокировано не будет). Возвращаемся к прежней проблеме. Как сделать так, чтобы данные, приходящие от клиентов, игнорировались? Пока вижу три варианта -- ловить POLLIN и:
  •  читать всё что накопилось в буфере и сливать в /dev/null;
  •  отрубать клиентов нафиг, ибо протокол надо соблюдать;
  •  ничего не читать, пусть висит в буфере.

Первый вариант мне не нравится: почему процесс должен вообще дёргаться по поводу данных, приходящих от клиентов, когда они не нужны? Неужели нет способа их просто игнорировать? Второй какой-то "фошысский", хотя на данный момент самый симпатичный. Ну и третий тоже не подходит, потому что если буфер сокета -- пара килобайт, а клиентов -- пара тысяч, совершенно ненужный мусор съест 4 мегабайта памяти!

Код:

Естественно, весь код программы (хоть она и не дописана пока) приводить не буду, ниже с сокращениями:

Код

void
mux_monitor(Multiplexer *mux)
{
    size_t i;

    assert(mux != NULL);

    while (poll(mux->pool->pool, mux->pool->count, 5) >= 0)
    {
        for (i = 0; i < mux->pool->count; ++i)
        {
            if (mux->pool->pool[i].revents != 0)
                printf("Events recvd: %x\n", mux->pool->pool[i].revents);
            if ((mux->pool->pool[i].revents &
                 (POLLERR | POLLHUP | POLLNVAL)) != 0)
            {
                printf("POLLERR, POLLHUP, or POLLNVAL rcvd\n");
                mux_hangup(mux, i--);
            }
            else if ((mux->pool->pool[i].revents & POLLIN) != 0)
            {
                printf("POLLIN rcvd\n");
                if (mux->pool->pool[i].fd == mux->sockCtl ||
                    mux->pool->pool[i].fd == mux->sockClient)
                {
                    mux_accept(mux, i);
                }
                else
                {
                    mux_read(mux, i);
                }
            }
        }
        puts(".");
    }
}

void
mux_accept(Multiplexer *mux, size_t idx)
{
    int sock;
    int newIdx;

    assert(mux != NULL);

    puts("accept");
    sock = accept(mux->pool->pool[idx].fd, NULL, 0);

    newIdx = pool_slotReserve(mux->pool);
    if (newIdx >= 0)
    {
        mux->pool->pool[newIdx].fd = sock;
        mux->pool->pool[newIdx].events = POLLIN;
        mux->pool->pool[newIdx].revents = 0;
    }
}

void
mux_read(Multiplexer *mux, size_t idx)
{
    ssize_t rd;
    char buf[BUFSIZ];
    Connection conn;

    rd = read(mux->pool->pool[idx].fd, buf, sizeof(buf) - 1);
    buf[rd] = '\0';

    if (rd == 0)
    {
        /* socket closed */
        mux_hangup(mux, idx);
        return 0;
    }

    puts(buf);

    conn.fd = mux->pool->pool[idx].fd;

    /* We've read everything we wanted */
    shutdown(conn->fd, SHUT_RD);
    mux->pool->pool[conn->poolIdx].events = 0;
    mux->pool->pool[conn->poolIdx].revents = 0;
}

void
mux_hangup(Multiplexer *mux, size_t idx)
{
    assert(mux != NULL);

    printf("hangup: %u\n", (unsigned int)idx);
    shutdown(mux->pool->pool[idx].fd, SHUT_RDWR);
    close(mux->pool->pool[idx].fd);
    pool_slotFree(mux->pool, idx);
}


Если нужна ещё какая-то информация о коде (или сам код) -- приведу с радостью по первому требованию.

sockCtl и sockClient на данном этапе не имеют значения (кроме того, что они находятся в состоянии LISTEN), в дальнейшем их пути разойдутся.

Сценарий:

Запускаем сервер, телнетимся на нужный порт. Вводим данные -- всё хорошо; тыкаем ^], вводим q -- на стороне сервера видим:

Код

Events recvd: 1
POLLIN rcvd


после чего (поскольку read(sock)==0 обрабатывается в mux_read()) сокет захлопывается сервером. Если же порт клиента после первого mux_read() был подвергнут shutdown(SHUT_RD), а соответствующий poll.events установлен в 0, сервер вообще ничего не сообщает и продолжает радостно выдавать по точке в 5 секунд.

Да, по идее mux_read() должен возвращать значение, по которому i будет декрементироваться в случае, если сокет был удалён, но это не критично (в крайнем случае, poll() уйдёт на второй круг), и расправа над этой багой запланирована на сегодняшний вечер.

Большое спасибо за внимание!

Автор: GrayCardinal 10.9.2008, 16:45
NightGoblin
Если данные идут не по протоколу, имеешь полное право убить нафиг smile
Если у тебя "строковый" сервер, буфер сокета можно уменьшить байт до 256 smile Как не скажу, но знаю что можно. Вообще я не до конца понял проблему smile

Автор: NightGoblin 10.9.2008, 17:30
GrayCardinal,
Размер буфера можно установить до приёма соединения через SO_RCVBUF -- но, насколько мне удалось выяснить, после установки соединения этот буфер можно только увеличить (http://docs.hp.com/en/B2355-90136/ch03s01.html).

Проблема, собственно, в том, что нужно всё и сразу -- а существующие варианты работают только наполовину smile

Что конкретно непонятно? Если нужно -- объясню подробнее.

А вообще я сам всё больше склоняюсь к варианту "не умеешь вежливо разговаривать по протоколу -- ступай лесом" smile

Автор: MAKCim 10.9.2008, 21:14
NightGoblin
сформулируй одним предложением
что надо сделать?  smile 

Автор: MAKCim 10.9.2008, 21:39
Код

shutdown(conn->fd, SHUT_RD);
    mux->pool->pool[conn->poolIdx].events = 0;
    mux->pool->pool[conn->poolIdx].revents = 0;

после этого, я так понимаю, сервер не принимает данных от клиента, а может только возвратить результат?

Автор: NightGoblin 10.9.2008, 21:44
Цитата(MAKCim @  10.9.2008,  13:14 Найти цитируемый пост)
сформулируй одним предложением
что надо сделать?


Определить, когда отключился клиент, сокет которого закрыт на чтение. Это один из вариантов, но это будет достаточным решением проблемы.

Цитата(MAKCim @  10.9.2008,  13:39 Найти цитируемый пост)
после этого, я так понимаю, сервер не принимает данных от клиента, а может только возвратить результат? 


Именно так.

Автор: MAKCim 10.9.2008, 22:04
Цитата(NightGoblin @  10.9.2008,  21:44 Найти цитируемый пост)
Определить, когда отключился клиент, сокет которого закрыт на чтение.

проверить состояние TIME_WAIT на сокете

Автор: NightGoblin 11.9.2008, 18:14
MAKCim, есть ли переносимый способ проверить состояние сокета на TIME_WAIT? В линуксовом tcp(7) есть TCP_INFO, но там написано "This option should not be used in code intended to be portable." Сервер должен работать как минимум на FreeBSD (и, желательно, на Linux, на котором ведётся разработка smile ). То же касается и POLLRDHUP (пришло что-то на мыло, тут не вижу -- удалено?) -- насколько всё это портабельно?

Автор: MAKCim 11.9.2008, 21:35
NightGoblin
да, TCP_INFO и POLLRDHUP не переносимы

Цитата(NightGoblin @  10.9.2008,  21:44 Найти цитируемый пост)
Определить, когда отключился клиент, сокет которого закрыт на чтение.

так тут как бы POLLHUP'а достаточно

ведь что просходит
когда мы делаем shutdown(SHUT_RD), то клиенту посылается FIN сегмент, он отвечает на него ACK'ом
на стороне сервера сокет переходит в состояние FIN_WAIT_2
после close() на сокете на стороне клиента серверу посылается FIN сегмент, сокет на сервере переходит в состояние TIME_WAIT
через 2*MSL секунд сокет на сервере переходит в состояние TCP_CLOSE и poll() на нем возвращает POLLHUP
так что POLLHUP достаточен для определения момента закрытия соединения

Автор: NightGoblin 16.9.2008, 17:19
MAKCim, в общем, пока что я не пробовал разбираться. Проблема получилась в том, что я написал короткий тест, и там вроде бы всё работало. Потом переписал это под рабочую версию (вроде бы, ничего не поменял) -- и всё, POLLHUP перестал приходить. Пока что сделал poll на POLLIN и отрубаю клиентов при получении новых данных (которые мне не нужны). Тоже не оптимальный способ так делать, но до поры - до времени сойдёт. В ближайшее время покопаюсь, а пока другие проблемы свалились на голову (не относящиеся к теме).

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)