Почему опция SO_LINGER с таймаутом 0 или 10 секунд не удаляет сокеты сразу или через 10 секунд?

Я прочитал опцию TCP SO_LINGER (ноль) - когда это требуется и несколько других связанных вопросов и ответов, но я не могу воспроизвести любой из SO_LINGER поведение объясняется в этих постах. Я поделюсь одним из моих многочисленных экспериментов здесь.

Я выполняю этот эксперимент в следующей среде.

$ lsb_release -d
Description:    Debian GNU/Linux 9.0 (stretch)
$ gcc -dumpversion
6.3.0

Вот пример неправильного поведения клиента, который подключается к серверу, но не получает никаких данных в течение 90 секунд.

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>

int main()
{
    int sockfd;
    int ret;
    struct addrinfo hints, *ai;
    char buffer[256];
    ssize_t bytes;

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    if ((ret = getaddrinfo(NULL, "8000", &hints, &ai)) == -1) {
        fprintf(stderr, "client: getaddrinfo: %s\n", gai_strerror(ret));
        return 1;
    }

    sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
    if (sockfd == -1) {
        perror("client: socket");
        return 1;
    }

    if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) == -1) {
        perror("client: connect");
        close(sockfd);
        return -1;
    }

    printf("client: connected\n");

    /*
    bytes = recv(sockfd, buffer, sizeof buffer, 0);
    if (recv(sockfd, buffer, sizeof buffer, 0) == -1) {
        perror("client: recv");
        close(sockfd);
        return -1;
    }

    printf("client: received: %.*s\n", (int) bytes, buffer);
    */

    sleep(90);
    freeaddrinfo(ai);

    printf("client: closing socket ...\n");
    close(sockfd);
    printf("client: closed socket!\n");

    return 0;
}

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

Чтобы достичь этого, мы даем возможность SO_LINGER опция розетки с задержкой на 10 секунд.

/* server.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>

int main()
{
    int sockfd;
    int ret;
    int yes = 1;

    struct addrinfo hints, *ai;

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    if ((ret = getaddrinfo(NULL, "8000", &hints, &ai)) == -1) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
        return 1;
    }

    sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
    if (sockfd == -1) {
        perror("server: socket");
        return 1;
    }

    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) == -1) {
        perror("server: setsockopt");
        close(sockfd);
        return 1;
    }

    if (bind(sockfd, ai->ai_addr, ai->ai_addrlen) == -1) {
        perror("server: bind");
        close(sockfd);
        return 1;
    }

    freeaddrinfo(ai);

    if (listen(sockfd, 10) == -1) {
        perror("server: listen");
        close(sockfd);
        return 1;
    }

    printf("server: listening ...\n");

    while (1) {
        int client_sockfd;
        struct sockaddr_storage client_addr;
        socklen_t client_addrlen = sizeof client_addr;
        struct linger l_opt;

        printf("server: accepting ...\n");
        client_sockfd = accept(sockfd, (struct sockaddr *) &client_addr,
                               &client_addrlen);

        /* Set SO_LINGER opt for the new client socket. */
        l_opt.l_onoff = 1;
        l_opt.l_linger = 10;
        setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &l_opt, sizeof l_opt);

        if (client_sockfd == -1) {
            perror("server: accept");
            continue;
        }

        if (send(client_sockfd, "hello\n", 6, 0) == -1) {
            perror("server: send");
            continue;
        }

        printf("server: sent: hello\n");
        printf("server: closing client socket ...\n");
        close(client_sockfd);
        printf("server: closed client socket!\n");
    }

    return 0;
}

Вот мой экспериментатор.

# run.sh
gcc -std=c99 -Wall -Wextra -Wpedantic -D_DEFAULT_SOURCE server.c -o server
gcc -std=c99 -Wall -Wextra -Wpedantic -D_DEFAULT_SOURCE client.c -o client
./server &
sleep 1
./client
pkill ^server$

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

$ for i in {1..10}; do netstat -nopa 2> /dev/null | grep :8000; echo =====; sleep 10; done
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      16293/./server       off (0.00/0/0)
tcp        7      0 127.0.0.1:35536         127.0.0.1:8000          CLOSE_WAIT  16295/./client       off (0.00/0/0)
tcp        0      0 127.0.0.1:8000          127.0.0.1:35536         FIN_WAIT2   -                    timewait (59.84/0/0)
=====
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      16293/./server       off (0.00/0/0)
tcp        7      0 127.0.0.1:35536         127.0.0.1:8000          CLOSE_WAIT  16295/./client       off (0.00/0/0)
tcp        0      0 127.0.0.1:8000          127.0.0.1:35536         FIN_WAIT2   -                    timewait (49.83/0/0)
=====
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      16293/./server       off (0.00/0/0)
tcp        7      0 127.0.0.1:35536         127.0.0.1:8000          CLOSE_WAIT  16295/./client       off (0.00/0/0)
tcp        0      0 127.0.0.1:8000          127.0.0.1:35536         FIN_WAIT2   -                    timewait (39.82/0/0)
=====
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      16293/./server       off (0.00/0/0)
tcp        7      0 127.0.0.1:35536         127.0.0.1:8000          CLOSE_WAIT  16295/./client       off (0.00/0/0)
tcp        0      0 127.0.0.1:8000          127.0.0.1:35536         FIN_WAIT2   -                    timewait (29.81/0/0)
=====
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      16293/./server       off (0.00/0/0)
tcp        7      0 127.0.0.1:35536         127.0.0.1:8000          CLOSE_WAIT  16295/./client       off (0.00/0/0)
tcp        0      0 127.0.0.1:8000          127.0.0.1:35536         FIN_WAIT2   -                    timewait (19.80/0/0)
=====
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      16293/./server       off (0.00/0/0)
tcp        7      0 127.0.0.1:35536         127.0.0.1:8000          CLOSE_WAIT  16295/./client       off (0.00/0/0)
tcp        0      0 127.0.0.1:8000          127.0.0.1:35536         FIN_WAIT2   -                    timewait (9.78/0/0)
=====
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      16293/./server       off (0.00/0/0)
tcp        7      0 127.0.0.1:35536         127.0.0.1:8000          CLOSE_WAIT  16295/./client       off (0.00/0/0)
tcp        0      0 127.0.0.1:8000          127.0.0.1:35536         FIN_WAIT2   -                    timewait (0.00/0/0)
=====
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      16293/./server       off (0.00/0/0)
tcp        7      0 127.0.0.1:35536         127.0.0.1:8000          CLOSE_WAIT  16295/./client       off (0.00/0/0)
=====
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      16293/./server       off (0.00/0/0)
tcp        7      0 127.0.0.1:35536         127.0.0.1:8000          CLOSE_WAIT  16295/./client       off (0.00/0/0)
=====
=====

Приведенный выше вывод показывает, что сокет сервера (третья строка в каждой итерации вывода) остается в FIN_WAIT2 состояние в течение 60 секунд (то есть время ожидания по умолчанию).

Почему SO_LINGER вариант с таймаутом 10 секунды не гарантировали, что сервер закрыл свой клиентский сокет (т. е. локальный адрес = 127.0.0.1:8000; внешний адрес = 127.0.0.1:35536) успешно через 10 секунд?

Примечание: я получаю те же результаты даже с таймаутом 0, т. Е. Со следующим кодом сокет для Local Address = 127.0.0.1:8000 и Foreign Address = 127.0.0.1:35536 остается в FIN_WAIT2 состояние в течение 60 секунд.

        /* Set SO_LINGER opt for the new client socket. */
        l_opt.l_onoff = 1;
        l_opt.l_linger = 0;
        setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &l_opt, sizeof l_opt);

Если SO_LINGER не влияет на удаление розетки или FIN_WAIT2 тайм-аут, то, что на самом деле является целью SO_LINGER?

2 ответа

У вас есть основное недоразумение.

Установка SO_LINGER с положительным таймаутом делает ровно одну вещь. Это позволяет close() блокировать до этого времени, пока все ожидающие исходящие данные еще находятся в полете. Если вы не измените его, по умолчанию для close() быть асинхронным, что означает, что приложение не может определить, были ли отправлены какие-либо данные, все еще находящиеся в полете.

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

Это не имеет ничего общего с очисткой мертвых или бесполезных розеток. В частности, он не сокращает TIME_WAIT или следующие тайм-ауты TCP после закрытия.

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

Ваш фактический код ведет себя точно так, как ожидалось. Сервер закрыт, поэтому клиент находится в CLOSE_WAIT в течение 90 секунд, а сервер находится в режиме FIN_WAIT_2, ожидая закрытия клиента. Здесь нет ничего, кроме плохого поведения клиента. Сервер переживет его, как только истечет время ожидания.

@LoneLearner Вместо использования:

l_onoff = 1

l_linger = 0

попробуй это:

l_onoff = 0

l_linger = 0

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

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

Другие вопросы по тегам