close () не закрывает гнездо правильно
У меня многопоточный сервер (пул streamов), который обрабатывает большое количество запросов (до 500 / сек для одного узла), используя 20 streamов. Существует stream прослушивателя, который принимает входящие соединения и ставит их в очередь для обрабатываемых streamов обработчиков. После того, как ответ готов, streamи затем выписываются клиенту и закрывают сокет. Кажется, все было хорошо до недавнего времени, тестовая клиентская программа начала свисать случайно после прочтения ответа. После многократного копания кажется, что close () с сервера фактически не отключает сокет. Я добавил некоторые отладочные отпечатки в код с номером дескриптора файла, и я получаю этот тип вывода.
Processing request for 21 Writing to 21 Closing 21
Возвращаемое значение close () равно 0, или будет напечатан другой отладочный оператор. После этого вывода с зависающим клиентом lsof показывает установленное соединение.
SERVER 8160 root 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (ESTABLISHED)
- close vs shutdown socket?
- Могут ли сокеты TCP и UDP использовать один и тот же порт?
- Отправка сообщения всем клиентам (связь Client-Server)
- C # Установленное соединение было прервано программным обеспечением вашей хост-машины
- была предпринята попытка получить доступ к сокету таким образом, чтобы запретить его разрешения доступа. Зачем?
КЛИЕНТ 17747 root 12u IPv4 32754228 TCP localhost: 47530-> localhost: 9980 (ESTABLISHED)
Это похоже на то, что сервер никогда не отправляет последовательность выключения клиенту, и это состояние зависает, пока клиент не будет убит, оставив серверную сторону в состоянии ожидания
SERVER 8160 root 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (CLOSE_WAIT)
Также, если клиент имеет указанный тайм-аут, он будет тайм-аут вместо того, чтобы висит. Я также могу запустить вручную
call close(21)
на сервере из gdb, а затем клиент отключится. Это случается, возможно, когда-то в 50 000 запросов, но может не произойти в течение длительных периодов времени.
Версия для Linux: версия 2.6.21.7-2.fc8xen Centos: 5.4 (Final)
действия сокета следующие:
SERVER:
int client_socket; struct sockaddr_in client_addr; socklen_t client_len = sizeof (client_addr);
while(true) { client_socket = accept(incoming_socket, (struct sockaddr *)&client_addr, &client_len); if (client_socket == -1) continue; /* insert into queue here for threads to process */ }
Затем stream поднимает сокет и формирует ответ.
/* get client_socket from queue */ /* processing request here */ /* now set to blocking for write; was previously set to non-blocking for reading */ int flags = fcntl(client_socket, F_GETFL); if (flags < 0) abort(); if (fcntl(client_socket, F_SETFL, flags|O_NONBLOCK) < 0) abort(); server_write(client_socket, response_buf, response_length); server_close(client_socket);
server_write и server_close.
void server_write( int fd, char const *buf, ssize_t len ) { printf("Writing to %d\n", fd); while(len > 0) { ssize_t n = write(fd, buf, len); if(n <= 0) return;// I don't really care what error happened, we'll just drop the connection len -= n; buf += n; } } void server_close( int fd ) { for(uint32_t i=0; i<10; i++) { int n = close(fd); if(!n) {//closed successfully return; } usleep(100); } printf("Close failed for %d\n", fd); }
КЛИЕНТ:
Клиентская сторона использует libcurl v 7.27.0
CURL *curl = curl_easy_init(); CURLcode res; curl_easy_setopt( curl, CURLOPT_URL, url); curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, write_callback ); curl_easy_setopt( curl, CURLOPT_WRITEDATA, write_tag ); res = curl_easy_perform(curl);
Ничего необычного, просто базовое соединение. Клиент зависает в tranfer.c (в libcurl), потому что сокет не воспринимается как закрытый. Он ждет больше данных с сервера.
Вещи, которые я пробовал до сих пор:
Выключение перед закрытием
shutdown(fd, SHUT_WR); char buf[64]; while(read(fd, buf, 64) > 0); /* then close */
Установка SO_LINGER для принудительного закрытия через 1 секунду
struct linger l; l.l_onoff = 1; l.l_linger = 1; if (setsockopt(client_socket, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1) abort();
Это не имело никакого значения. Любые идеи очень приветствуются.
EDIT. Это оказалось проблемой безопасности streamов внутри библиотеки очередей, в результате чего сокет обрабатывался ненадлежащим образом несколькими streamами.
- Сколько накладных расходов накладывает SSL?
- Как найти полное имя хоста текущего компьютера в C (имя хоста и информация о домене)?
- Правильное закрытие SSLSocket
- Получить MAC-адрес на локальной машине с помощью Java
- NetworkStream.ReadAsync с маркером отмены никогда не отменяет
- Что значит связать сокет многоадресной рассылки (UDP)?
- API сокетов Java. Как узнать, было ли соединение закрыто?
- Как использовать сокет-клиент с WCF (net.tcp)?
Вот несколько кодов, которые я использовал во многих Unix-подобных системах (например, SunOS 4, SGI IRIX, HPUX 10.20, CentOS 5, Cygwin), чтобы закрыть сокет:
int getSO_ERROR(int fd) { int err = 1; socklen_t len = sizeof err; if (-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&err, &len)) FatalError("getSO_ERROR"); if (err) errno = err; // set errno to the socket SO_ERROR return err; } void closeSocket(int fd) { // *not* the Windows closesocket() if (fd >= 0) { getSO_ERROR(fd); // first clear any errors, which can cause close to fail if (shutdown(fd, SHUT_RDWR) < 0) // secondly, terminate the 'reliable' delivery if (errno != ENOTCONN && errno != EINVAL) // SGI causes EINVAL Perror("shutdown"); if (close(fd) < 0) // finally call close() Perror("close"); } }
Но вышеописанное не гарантирует отправку буферизованных записей.
Изящное закрытие: мне потребовалось около 10 лет, чтобы выяснить, как закрыть розетку. Но еще 10 лет я просто лениво называл usleep(20000)
за небольшую задержку, чтобы «обеспечить», чтобы буфер записи был сброшен до закрытия. Это, очевидно, не очень умно, потому что:
- Задержка была слишком длинной большую часть времени.
- Задержка была слишком коротка в течение некоторого времени - может быть!
- Сигнал, такой SIGCHLD может произойти, чтобы закончить
usleep()
(но я обычноusleep()
дважды, чтобы обработать этот случай - взломать). - Не было никаких признаков того, работает ли это. Но это, возможно, не важно, если: a) жесткие сбрасывания в порядке, и / или b) у вас есть контроль над обеими сторонами ссылки.
Но делать правильный флеш удивительно сложно. Использование SO_LINGER
по-видимому, не способ; см., например:
- http://msdn.microsoft.com/en-us/library/ms740481%28v=vs.85%29.aspx
- https://www.google.ca/#q=the-ultimate-so_linger-page
И SIOCOUTQ
похоже, SIOCOUTQ
для Linux.
Примечание shutdown(fd, SHUT_WR)
не прекращает писать, вопреки его имени, и, возможно, противоречит man 2 shutdown
.
Этот код flushSocketBeforeClose()
ждет до считывания нулевых байтов или до истечения таймера. Функция haveInput()
является простой оболочкой для select (2) и имеет значение для блокировки до 1 / 100th секунды.
bool haveInput(int fd, double timeout) { int status; fd_set fds; struct timeval tv; FD_ZERO(&fds); FD_SET(fd, &fds); tv.tv_sec = (long)timeout; // cast needed for C++ tv.tv_usec = (long)((timeout - tv.tv_sec) * 1000000); // 'suseconds_t' while (1) { if (!(status = select(fd + 1, &fds, 0, 0, &tv))) return FALSE; else if (status > 0 && FD_ISSET(fd, &fds)) return TRUE; else if (status > 0) FatalError("I am confused"); else if (errno != EINTR) FatalError("select"); // tbd EBADF: man page "an error has occurred" } } bool flushSocketBeforeClose(int fd, double timeout) { const double start = getWallTimeEpoch(); char discard[99]; ASSERT(SHUT_WR == 1); if (shutdown(fd, 1) != -1) while (getWallTimeEpoch() < start + timeout) while (haveInput(fd, 0.01)) // can block for 0.01 secs if (!read(fd, discard, sizeof discard)) return TRUE; // success! return FALSE; }
Пример использования:
if (!flushSocketBeforeClose(fd, 2.0)) // can block for 2s printf("Warning: Cannot gracefully close socket\n"); closeSocket(fd);
В приведенном выше getWallTimeEpoch()
my getWallTimeEpoch()
похож на time(),
а Perror()
является оболочкой для perror().
Редактировать: Некоторые комментарии:
-
Мое первое признание немного смущает. OP и Nemo оспаривали необходимость очистки внутреннего
so_error
до закрытия, но теперь я не могу найти никаких ссылок на это. Эта система была HPUX 10.20. После неудачногоconnect()
просто вызов функцииclose()
не освободил дескриптор файла, потому что система пожелала доставить мне выдающуюся ошибку. Но я, как и большинство людей, никогда не удосужился проверить возвращаемое значениеclose.
Поэтому у меня в конечном итоге закончились файловые дескрипторы(ulimit -n),
которые, наконец, привлекли мое внимание. -
(очень незначительная точка). Один комментатор возражал против жестко заданных числовых аргументов
shutdown()
, а не, например, SHUT_WR для 1. Самый простой ответ заключается в том, что Windows использует разные #SD_SEND
/SD_SEND
напримерSD_SEND
. И многие другие авторы (например, Beej) используют константы, как и многие устаревшие системы. -
Кроме того, я всегда, всегда, устанавливаю FD_CLOEXEC во всех своих сокетах, поскольку в моих приложениях я никогда не хочу, чтобы они передавались ребенку, и, что более важно, я не хочу, чтобы зависавший ребенок воздействовал на меня.
Пример кода для установки CLOEXEC:
static void setFD_CLOEXEC(int fd) { int status = fcntl(fd, F_GETFD, 0); if (status >= 0) status = fcntl(fd, F_SETFD, status | FD_CLOEXEC); if (status < 0) Perror("Error getting/setting socket FD_CLOEXEC flags"); }
Большой ответ от Джозефа Куинси. У меня есть комментарии к функции haveInput
. Удивительно, насколько вероятно, что select возвращает fd, который вы не включили в свой набор. Это будет серьезная ошибка ОС IMHO. Это то, что я хотел бы проверить, писал ли я модульные тесты для функции select
, а не в обычном приложении.
if (!(status = select(fd + 1, &fds, 0, 0, &tv))) return FALSE; else if (status > 0 && FD_ISSET(fd, &fds)) return TRUE; else if (status > 0) FatalError("I am confused"); // <--- fd unknown to function
Мой другой комментарий относится к обработке EINTR. Теоретически, вы могли бы застрять в бесконечном цикле, если бы select
продолжал возвращать EINTR, поскольку эта ошибка позволяет начать цикл. Учитывая очень короткий тайм-аут (0,01), представляется маловероятным. Тем не менее, я думаю, что подходящим способом борьбы с этим было бы вернуть ошибки вызывающему ( flushSocketBeforeClose
). Вызывающий может продолжать вызов haveInput
тех пор, пока его таймаут не истек, и объявить отказ для других ошибок.
ДОПОЛНЕНИЕ № 1
flushSocketBeforeClose
не выйдет быстро в случае read
возвращая ошибку. Он будет продолжать цикл до истечения таймаута. Вы не можете полагаться на select
внутри haveInput
чтобы предвидеть все ошибки. read
есть собственные ошибки (например: EIO
).
while (haveInput(fd, 0.01)) if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop return TRUE;
Это звучит для меня как ошибка в вашем дистрибутиве Linux.
Документация библиотеки GNU C гласит:
Когда вы закончите использовать сокет, вы можете просто закрыть его дескриптор файла с помощью
close
Ничего об очистке каких-либо флагов ошибки или ожидании сброса данных или какой-либо такой вещи.
Ваш код в порядке; ваш O / S имеет ошибку.