SO_KEEPALIVE не работает во время вызова write ()? - PullRequest
4 голосов
/ 14 октября 2011

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

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

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

Но если я попытаюсь выполнить 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);
}

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

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

Спасибо!


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

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

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

Ответы [ 6 ]

2 голосов
/ 15 октября 2011

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

у меня нормально работает =)

//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. Может быть, это где-то ошибка?

1 голос
/ 10 апреля 2012

Получили ли вы успешно байт или 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, я думаю, вы обнаружите, что сообщения keepalive работают правильно.


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

1 голос
/ 30 декабря 2011

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

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

1 голос
/ 15 октября 2011

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

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

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

0 голосов
/ 25 января 2016

Это происходит из-за повторной передачи 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 / кр / событие / загрузить / 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 вы можете встретить различные события возврата в зависимости от используемых вами платформ, поэтому вы не должны определять состояние разорванного соединения только событиями возврата. Например, HP возвращает событие POLLERR, а AIX возвращает только событие POLLIN, когда происходит тайм-аут keepalive. В это время вы встретите ошибку ETIMEDOUT в вызове recv ().

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

0 голосов
/ 15 октября 2011

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

...