2011-10-14 3 views
4

Я разрабатываю приложение сокета, которое должно быть надежным для сетевых сбоев.SO_KEEPALIVE не работает во время вызова write()?

Приложение имеет 2 работающих потока, одно ожидающее сообщение из сокета (цикл чтения()), а другое отправляет сообщения в сокет (цикл write()).

В настоящее время я пытаюсь использовать SO_KEEPALIVE для обработки сетевых сбоев. Он работает нормально, если я заблокирован только для чтения(). Через несколько секунд после того, как соединение будет потеряно (сетевой кабель удален), сбой чтения() завершится с сообщением «Тайм-аут соединения».

Но, если я попытаюсь выполнить wrte() после отключения сети (и до истечения таймаута), как write(), так и read() будут блокироваться навсегда, без ошибок.

Это код с разделенным образцом, который направляет stdin/stdout в сокет. Он прослушивает порт 5656:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
#include <pthread.h> 
#include <sys/types.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <netinet/tcp.h> 

int socket_fd; 

void error(const char *msg) { 
    perror(msg); 
    exit(1); 
} 

//Read from stdin and write to socket 
void* write_daemon (void* _arg) { 
    while (1) { 
     char c; 
     int ret = scanf("%c", &c); 
     if (ret <= 0) error("read from stdin"); 
     int ret2 = write(socket_fd, &c, sizeof(c)); 
     if (ret2 <= 0) error("write to socket"); 
    } 
    return NULL; 
} 

//Read from socket and write to stdout 
void* read_daemon (void* _arg) { 
    while (1) { 
     char c; 
     int ret = read(socket_fd, &c, sizeof(c)); 
     if (ret <= 0) error("read from socket"); 
     int ret2 = printf("%c", c); 
     if (ret2 <= 0) error("write to stdout"); 
    } 
    return NULL; 
} 


//Enable and configure KEEPALIVE - To detect network problems quickly 
void config_socket() { 
    int enable_no_delay = 1; 
    int enable_keep_alive = 1; 
    int keepalive_idle  =1; //Very short interval. Just for testing 
    int keepalive_count =1; 
    int keepalive_interval =1; 
    int result; 

    //=> http://tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/#setsockopt 
    result = setsockopt(socket_fd, SOL_SOCKET, SO_KEEPALIVE, &enable_keep_alive, sizeof(int)); 
    if (result < 0) 
     error("SO_KEEPALIVE"); 

    result = setsockopt(socket_fd, SOL_TCP, TCP_KEEPIDLE, &keepalive_idle, sizeof(int)); 
    if (result < 0) 
     error("TCP_KEEPIDLE"); 

    result = setsockopt(socket_fd, SOL_TCP, TCP_KEEPINTVL, &keepalive_interval, sizeof(int)); 
    if (result < 0) 
     error("TCP_KEEPINTVL"); 

    result = setsockopt(socket_fd, SOL_TCP, TCP_KEEPCNT, &keepalive_count, sizeof(int)); 
    if (result < 0) 
     error("TCP_KEEPCNT"); 
} 

int main(int argc, char *argv[]) { 
    //Create Server socket, bound to port 5656 
    int listen_socket_fd; 
    int tr=1; 
    struct sockaddr_in serv_addr, cli_addr; 
    socklen_t clilen = sizeof(cli_addr); 
    pthread_t write_thread, read_thread; 

    listen_socket_fd = socket(AF_INET, SOCK_STREAM, 0); 
    if (listen_socket_fd < 0) 
     error("socket()"); 

    if (setsockopt(listen_socket_fd,SOL_SOCKET,SO_REUSEADDR,&tr,sizeof(int)) < 0) 
     error("SO_REUSEADDR"); 

    bzero((char *) &serv_addr, sizeof(serv_addr)); 
    serv_addr.sin_family = AF_INET; 
    serv_addr.sin_addr.s_addr = INADDR_ANY; 
    serv_addr.sin_port = htons(5656); 
    if (bind(listen_socket_fd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) 
     error("bind()"); 

    //Wait for client socket 
    listen(listen_socket_fd,5); 
    socket_fd = accept(listen_socket_fd, (struct sockaddr *) &cli_addr, &clilen); 
    config_socket(); 
    pthread_create(&write_thread, NULL, write_daemon, NULL); 
    pthread_create(&read_thread , NULL, read_daemon , NULL); 
    close(listen_socket_fd); 
    pthread_exit(NULL); 
} 

Чтобы воспроизвести ошибку, используйте телнет 5656. Если выйдет через пару секунд после того, как бя связь теряется, если я пытаюсь написать что-то в терминале. В этом случае он будет блокироваться навсегда.

Итак, вопросы: что случилось? как это исправить? Существуют ли другие альтернативы?

Спасибо!


Я попытался использовать Wireshark для проверки сетевого подключения. Если я не вызываю write(), я могу видеть, что пакеты TCP keep-alive отправляются, а соединение закрывается через несколько секунд.

Если вместо этого я пытаюсь написать(), он перестает отправлять пакеты Keep-Alive и вместо этого начинает отправлять повторные передачи TCP (мне кажется, что все в порядке). Проблема в том, что время между повторными передачами становится все больше и больше после каждого сбоя, и, похоже, он никогда не отказывается и не закрывает сокет.

Есть ли способ установить максимальное количество повторных передач или что-нибудь подобное? Спасибо

ответ

1

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

Для нас решение состояло в том, чтобы просто взять управление в свои руки и не полагаться на базовые ОС/драйверы, чтобы сказать вам, когда соединение замирает. Если вы контролируете как клиентские, так и серверные стороны, вы можете представить свои собственные сообщения ping, которые отказывают между клиентом и сервером. Таким образом, вы можете: a) контролировать свои собственные таймауты подключения и b) легко вести запись, указывающую на работоспособность соединения.

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

+0

Мне это нравится, но я реализую только одну сторону существующего протокола, который не имеет никакого способа заставить «пинги». –

0

В write_daemon(), вы сохраняете значение, возвращаемое write() в переменную ret2, но проверка на ошибки сокета, используя переменную ret вместо этого, так что вы никогда не будете на самом деле поймать любую write() ошибки.

+0

Спасибо! Я исправил это. К сожалению, это не устранило проблему. –

2

Я нашел опцию сокета TCP_USER_TIMEOUT (rfc5482), которая закрывает соединение, если отправленные данные не ACK'ed после указанного интервала.

Он отлично работает для меня =)

//defined in include/uapi/linux/tcp.h (since Linux 2.6.37) 
#define TCP_USER_TIMEOUT 18 

int tcp_timeout  =10000; //10 seconds before aborting a write() 

result = setsockopt(socket_fd, SOL_TCP, TCP_USER_TIMEOUT, &tcp_timeout, sizeof(int)); 
if (result < 0) 
    error("TCP_USER_TIMEOUT"); 

Тем не менее, я чувствую, что я не должен использовать как SO_KEEP_ALIVE и TCP_USER_TIMEOUT. Может быть, это ошибка?

+0

Это не ошибка, «TCP_USER_TIMEOUT» была реализована для решения той же проблемы, о которой вы говорили выше, т.е. в случае, если сторона, которая отправляет пакеты keepalive, если отправляет в этот сокет, тогда таймер повторной передачи запускается и отсоединение узнается через 15-20 минут! Использование 'TCP_USER_TIMEOUT', устанавливает жесткий предел того, как долго пакет может оставаться без ACK'd. – brokenfoot

1

TCP Keep Alive указан в RFC1122. Функция Keep Alive для TCP не предназначена для обнаружения кратковременных сбоев сети, а вместо этого для очистки блоков управления TCP/буферами, которые могут использовать драгоценные ресурсы. Этот RFC был также написан в 1989 году. RFC явно заявляет, что TCP Keep Alives не следует отправлять более одного раза в два часа, а затем это необходимо, только если другого трафика не было. Если протокол более высокого уровня должен обнаруживать потерю соединения, то задача самого высокого уровня заключается в том, чтобы сделать это сам. Протокол BGP-маршрутизации, который работает выше TCP, отправляет его собственную форму сообщения Keep Alive каждые 60 секунд по умолчанию. BGP Spec говорит, что соединение считается мертвым, если в последние 3 * keep_alive_interval секунды не было нового трафика. OpenSSH реализует свою собственную жизнь в форме пинга и понга. Он будет повторять отправку до X pings, которая ожидает отклика (понг) в течение Y раз, или он убивает соединение. TCP сам по себе очень тяжело доставляет данные перед лицом временных сбоев в сети и сам по себе не является полезным для обнаружения сбоев в сети.

Как правило, если вы хотите реализовать сохранение и хотите избежать блокировки, можно переключиться на неблокирующий ввод-вывод и поддерживать таймер, для которого можно использовать вызовы select()/poll() с помощью тайм-аут. Другим вариантом может быть использование отдельного потока таймера или даже более грубый подход использования SIGALARM. Я рекомендую использовать O_NONBLOCK с помощью fcntl(), чтобы установить сокет на неблокирующий ввод-вывод. Затем вы можете использовать gettimeofday() для записи при приеме входящего ввода-вывода и спящего режима с помощью select() до тех пор, пока не произойдет либо следующий Keep Alive, либо произойдет ввод-вывод.

+0

Не совсем то, что вы сказали. В RFC 1122 указано, что интервал DEFAULT для первого зонда keepalive должен составлять не менее двух часов. Это означает, что пользователь может установить его на все, что захочет. – lvella

1

Вы успешно достали байт или ACK с другой стороны перед отсоединением кабеля? Может быть, это связано с поведением, описанным в http://lkml.indiana.edu/hypermail/linux/kernel/0508.2/0757.html:


Ваш тест сомнительна, потому что вы не получите даже один ACK в установленном состоянии, поэтому tp-> rcv_tstamp переменная не имеет возможности получить инициализирован. Единственный ACK, который вы получаете, тот, который отвечает на установку соединения SYN, и мы не инициализируем tp-> rcv_stamp для этого ACK.

Проверка времени проверки всегда требует, чтобы tp-> rcv_tstamp имеет действительное значение, и пока вы не обработаете ACK в состоянии ESTABLISHED, это не так.

Если вы успешно отправили или получили хотя бы один байт по соединению и, таким образом, обработали хотя бы один ACK в состоянии ESTABLISHED, я думаю, вы обнаружите, что keepalives ведут себя правильно.


Это неявное поведение SO_KEEPALIVE.

0

Это из-за повторной передачи tcp, выполняемой стеком tcp без вашего сознания. Вот решения.

Даже если вы уже установили опцию keepalive в свой сокет приложения, вы не сможете своевременно определить состояние мертвого соединения сокета, если ваше приложение продолжает записывать в сокет. Это из-за повторной передачи tcp в стеке tcp ядра. tcp_retries1 и tcp_retries2 являются параметрами ядра для настройки тайм-аута повторной передачи tcp. Трудно предсказать точное время тайм-аута повторной передачи, поскольку оно рассчитывается механизмом RTT. Вы можете увидеть это вычисление в rfc793. (3.7. Обмен данными)

https://www.rfc-editor.org/rfc/rfc793.txt

Каждый платформы имеют конфигурации ядра для TCP повторной передачи.

Linux : tcp_retries1, tcp_retries2 : (exist in /proc/sys/net/ipv4) 

http://linux.die.net/man/7/tcp

HPUX : tcp_ip_notify_interval, tcp_ip_abort_interval 

http://www.hpuxtips.es/?q=node/53

AIX : rto_low, rto_high, rto_length, rto_limit 

http://www-903.ibm.com/kr/event/download/200804_324_swma/socket.pdf

Вы должны установить более низкое значение для tcp_retries2 (по умолчанию 15), если вы хотите, чтобы рано обнаружить мертвую соединение, но это не точное время, как я уже сказал. Кроме того, в настоящее время вы не можете установить эти значения только для одного сокета. Это глобальные параметры ядра. Была проведена пробная версия для применения опции сокета повторной передачи tcp для одиночного сокета (http://patchwork.ozlabs.org/patch/55236/), но я не думаю, что он был применен в основной строке ядра. Я не могу найти такое определение параметров в файлах системных заголовков.

Для справки вы можете отслеживать свой вариант гнезда keepalive через 'netstat -timers', как показано ниже. https://stackoverflow.com/questions/34914278

netstat -c --timer | grep "192.0.0.1:43245    192.0.68.1:49742" 

tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (1.92/0/0) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (0.71/0/0) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (9.46/0/1) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (8.30/0/1) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (7.14/0/1) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (5.98/0/1) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (4.82/0/1) 

Кроме того, когда KEEPALIVE ocurrs тайм-аут, вы можете встретить различные возвращаемые события в зависимости от платформ, которые вы используете, так что вы не должны решить статус мертвое подключения только обратными событий. Например, HP возвращает событие POLLERR, и AIX возвращает только событие POLLIN, когда происходит таймаут keepalive. В это время вы встретите ошибку ETIMEDOUT в вызове recv().

В последней версии ядра (с версии 2.6.37) вы можете использовать параметр TCP_USER_TIMEOUT, который будет работать хорошо. Эта опция может использоваться для одиночного сокета.

Смежные вопросы