Почему приложение может принимать и читать / записывать TCP-соединение в состоянии CLOSE_WAIT (Linux)? - PullRequest
0 голосов
/ 02 марта 2020

Я предполагал, что сервер может читать / записывать из / в сокет, когда TCP-соединение находится в состоянии ESTABLISHED. Но я вижу, что сервер действительно может читать и записывать в сокет, когда TCP-соединение находится в состоянии CLOSE_WAIT. Это происходит, когда клиент закрыл соединение на своей стороне, но сервер еще не обнаружил / не обработал случай окончания потока.

Например. Синхронный, блокирующий однопоточный сервер:

#include <unistd.h>
#include <netdb.h>
#include <signal.h>

int main (void)
{
    signal(SIGPIPE, SIG_IGN); // ignore SIGPIPE from last send()
    int listen_sock = socket(PF_INET, SOCK_STREAM, 0);
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
    struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(8000), .sin_addr.s_addr = htonl(INADDR_ANY)};
    bind(listen_sock, (struct sockaddr *) &addr, sizeof(addr));
    listen(listen_sock, 10);
    while (1) {
        int sock = accept(listen_sock, 0, 0);
        char buffer[1024];
        recv(sock, buffer, sizeof(buffer), 0);
        sleep(1);
        send(sock, buffer, 10, 0);
        send(sock, buffer, 10, 0);
        sleep(1);
        send(sock, buffer, 10, 0);
        close(sock);
    }
}

Компиляция и запуск:

gcc server_short.c -o server_short && strace ./server_short

Просмотр подключений на одном хосте:

watch -n0.1 'sudo ss -lnt | grep 8000; sudo netstat -tpvn | grep 8000'

Запуск недолговечных клиентов на другом хосте:

seq 20 | xargs -P100 -n1 bash -c 'echo -n "0123456789ABCDEF" | telnet $SERVER_IP 8000'

Затем я вижу соединения в состояниях CLOSE_WAIT и SYN_RECV, SYN_RECV превратится в CLOSE_WAIT, так как соединения в очереди будут обрабатываться сервером:

LISTEN    11        10                 0.0.0.0:8000             0.0.0.0:*       
tcp       17      0 server_IP:8000       client_IP:33016        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33030        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33020        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33028        CLOSE_WAIT  -                   
tcp        0      0 server_IP:8000       client_IP:33012        CLOSE_WAIT  21282/./server 
tcp        0      0 server_IP:8000       client_IP:33046        SYN_RECV    -                   
tcp        0      0 server_IP:8000       client_IP:33040        SYN_RECV    -                   
tcp        0      0 server_IP:8000       client_IP:33044        SYN_RECV    -                   
tcp        0      0 server_IP:8000       client_IP:33036        SYN_RECV    -                   
tcp       17      0 server_IP:8000       client_IP:33018        CLOSE_WAIT  -                   
tcp        0      0 server_IP:8000       client_IP:33048        SYN_RECV    -                   
tcp       17      0 server_IP:8000       client_IP:33034        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33022        CLOSE_WAIT  -                   
tcp        0      0 server_IP:8000       client_IP:33042        SYN_RECV    -                   
tcp        0      0 server_IP:8000       client_IP:33038        SYN_RECV    -                   
tcp       17      0 server_IP:8000       client_IP:33032        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33026        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33014        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33024        CLOSE_WAIT  -

И затем сервер обрабатывает эти соединения CLOSE_WAIT по одному:

socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(8000), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 10)                           = 0
accept(3, NULL, NULL)                   = 4
recvfrom(4, "0123456789ABCDEF", 1024, 0, NULL, NULL) = 16
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=22658, si_uid=1001} ---
close(4)                                = 0
accept(3, NULL, NULL)                   = 4
recvfrom(4, "0123456789ABCDEF", 1024, 0, NULL, NULL) = 16
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=22658, si_uid=1001} ---
close(4)                                = 0
... AND SO ON

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

Я понимаю, что этот код сервера не идеален, но даже если вы сделаете его асинхронным (с помощью select, epoll, boost :: asio и т. Д.), Вы также примете соединения CLOSE_WAIT, прочитаете входящий запрос и начальную обработку запроса перед обнаружением, что соединение не живо. Хотя серверная сторона знала, что соединение было закрыто клиентом до того, как оно было принято сервером.

Итак, вопросы:

  1. Соответствует ли это поведение спецификации TCP? Законно ли принимать и читать из соединения CLOSE_WAIT?
  2. Почему ядро ​​разрешает принимать соединения CLOSE_WAIT? Или почему recv не возвращает ошибку в этом случае? Кто может быть заинтересован в чтении данных из закрытого соединения? Какова цель этого поведения? Кажется, я не вижу, как можно использовать этот случай.
  3. Как обнаружить этот случай и не начинать обработку запроса, когда на 100% уверены, что никто не получит ответ?

1 Ответ

1 голос
/ 02 марта 2020

Стек TCP не зависит от вашего пользовательского приложения. Когда вы устанавливаете сокет в LISTEN, стек TCP уже принимает входящие соединения (входящий SYN имеет значение SYN|ACK), а также сохраняет входящие пакеты в буфере приема.

Когда вы программируете вызовы accept(), он либо блокирует и ждет, когда кто-то подключится к сокету, либо возвращает установленный сокет, который уже существует с точки зрения стека TCP (или конечного автомата TCP).

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

CLOSE_WAIT по своей сути является состоянием "стек TCP ожидает, что локальное приложение также закроет сокет ", поэтому ваше предположение, что вы можете только read(), когда состояние ESTABLISHED неверно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...