Почему recv() в клиентской программе получает сообщения, отправленные клиенту после того, как клиент вызвал выключение (sockfd, SHUT_RD)?

Из POSIX.1-2008/2013 документация shutdown():

int shutdown(int socket, int how);

...

Функция shutdown() должна вызывать все или часть полнодуплексного соединение сокета, связанное с сокетом дескриптора файла, к быть закрытым.

Функция shutdown() принимает следующие аргументы:

  • socketУказывает файловый дескриптор сокета.

  • howУказывает тип отключения. Значения следующие:

    • SHUT_RDОтключает дальнейшие операции приема.
    • SHUT_WRОтключает дальнейшие операции отправки.
    • SHUT_RDWRОтключает дальнейшие операции отправки и получения.

...

Страница для shutdown(2) говорит почти то же самое.

Вызов shutdown() вызывает все или часть полнодуплексного соединения на сокете, связанной с отключением sockfd. Если how SHUT_RD, дальнейшие приемы будут запрещены. Если how - SHUT_WR, дальнейшие передачи будут запрещены. Если how - SHUT_RDWR, дальнейшие приемы и передачи будут запрещены.

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

------------------------------------------------------
Time  netcat (nc)  C (a.out)   Result Observed
------------------------------------------------------
 0 s  listen       -           -
 2 s               connect()   -
 4 s  send "aa"    -           -
 6 s  -            recv() #1   recv() #1 receives "aa"
 8 s  -            shutdown()  -
10 s  send "bb"    -           -
12 s  -            recv() #2   recv() #2 receives "bb"
14 s  -            recv() #3   recv() #3 returns 0
16 s  -            recv() #4   recv() #4 returns 0
18 s  send "cc"    -           -
20 s  -            recv() #5   recv() #5 receives "cc"
22 s  -            recv() #6   recv() #6 returns 0
------------------------------------------------------

Ниже приведено краткое описание приведенной выше таблицы.

  • Время: Истекшее время (в секундах) с начала теста.
  • netcat (nc): Шаги, выполняемые через netcat (nc). Netcat был сделан, чтобы слушать на порт 8888 и принять TCP-соединения из моей программы на C, скомпилированной в . /a.out. Здесь Netcat играет роль сервера. Он отправляет три сообщения "aa", "bb" и "cc" в программу C после 4s, 10s и 18s, соответственно, истекли.
  • C (a.out): Шаги, выполненные моей программой на языке C, скомпилированной в. /a.out. Это выполняет 6 вызовов recv() после 6s, 12s, 14s, 16s, 20s и 22s прошло.
  • Результат: Результат, наблюдаемый на выходе программы C. Он показывает, что он способен recv() сообщение "bb", которое было отправлено после shutdown() успешно завершена. См. Строки для "12 с" и "20 с".

Вот программа C (клиентская программа).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

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

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

    /* Select TCP/IPv4 address only. */
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    if ((ret = getaddrinfo("localhost", "8888", &hints, &ai)) == -1) {
        printf("getaddrinfo() error: %s\n", gai_strerror(ret));
        return EXIT_FAILURE;
    }

    if ((sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol)) == -1) {
        printf("socket() error: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }

    /* Connect to localhost:8888. */
    sleep(2);
    if ((connect(sockfd, ai->ai_addr, ai->ai_addrlen)) == -1) {
        printf("connect() error: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }

    freeaddrinfo(ai);

    /* Test 1: Receive before shutdown. */
    sleep(4);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #1 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    sleep(2);
    if (shutdown(sockfd, SHUT_RD) == -1) {
        printf("shutdown() error: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }
    printf("shutdown() complete\n");

    /* Test 2: Receive after shutdown. */
    sleep (4);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #2 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    /* Test 3. */
    sleep (2);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #3 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    /* Test 4. */
    sleep (2);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #4 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    /* Test 5. */
    sleep (4);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #5 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    /* Test 6. */
    sleep (2);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #6 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);
}

Вышеприведенный код был сохранен в файле с именем foo.c.

Вот небольшая оболочка script, которая компилирует и запускает вышеуказанную программу и вызывает netcat (nc) для прослушивания на порту 8888 и отвечает клиенту с сообщениями aa, bb и cc с определенными интервалами в соответствии с таблицей показано выше. Следующая оболочка script сохраняется в файле с именем run.sh.

set -ex
gcc -std=c99 -pedantic -Wall -Wextra -D_POSIX_C_SOURCE=200112L foo.c
./a.out &
(sleep 4; printf aa; sleep 6; printf bb; sleep 8; printf cc) | nc -vvlp 8888

При запуске вышеуказанной оболочки script наблюдается следующий вывод.

$ sh run.sh 
+ gcc -std=c99 -pedantic -Wall -Wextra -D_POSIX_C_SOURCE=200112L foo.c
+ nc -vvlp 8888
+ sleep 4
listening on [any] 8888 ...
+ ./a.out
connect to [127.0.0.1] from localhost [127.0.0.1] 54208
+ printf aa
+ sleep 6
recv() #1 returned 2 bytes: aa
shutdown() complete
+ printf bb
+ sleep 8
recv() #2 returned 2 bytes: bb
recv() #3 returned 0 bytes: 
recv() #4 returned 0 bytes: 
+ printf cc
recv() #5 returned 2 bytes: cc
recv() #6 returned 0 bytes: 
 sent 6, rcvd 0

Выход показывает, что программа C может принимать сообщения с recv() даже после того, как он вызвал shutdown(). Единственное поведение, которое вызов shutdown(), похоже, повлиял на то, является ли recv() вызов немедленно возвращается или блокируется в ожидании следующего сообщения. Обычно, перед shutdown(), вызов recv() будет ждать сообщение для прибытия. Но после вызова shutdown() recv() возвращает 0 немедленно, когда нет нового сообщения.

Я ожидал, что все вызовы recv() после shutdown() не сработают (например, return -1) из-за документации, приведенной выше.

Два вопроса:

  • Является ли поведение, наблюдаемое в моем эксперименте, т.е. recv() вызовом возможность получать новые сообщения, отправленные после shutdown(), в соответствии с стандарте POSIX и странице руководства для shutdown(2), что у меня есть цитируется выше?
  • Почему это происходит после вызова shutdown(), recv() возвращает 0 немедленно, а не ждет появления нового сообщения?

Ответы

Ответ 1

Вы задали два вопроса: совместим ли он с стандартом posix и почему recv возвращает 0 вместо блокировки.

Стандарт для выключения

В документации для shutdown говорится:

Функция shutdown() отключает последующие операции отправки и/или приема в сокете, в зависимости от значения аргумента how.

Это означает, что никакие дальнейшие вызовы read не возвратят никаких данных.

Однако документация для recv гласит:

Если сообщения не доступны, и сверстник выполнил упорядоченное завершение, recv() должен вернуть 0.

Считая это вместе, это может означать, что после того, как удаленные одноранговые вызовы shutdown

  • вызовы recv должны возвращать ошибку, если данные доступны, или
  • вызовы recv могут продолжать возвращать данные после shutdown, если "сообщения доступны для приема".

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

(Обратите внимание, что любой протокол, который буферизует в любой точке стека, может иметь данные в пути, которые еще не могут быть прочитаны. Семантика shutdown позволяет вам получать эти данные после вызова shutdown.)

Однако это относится к одноранговому вызову shutdown, а не к вызывающему процессу. Если это также применимо, если вызывающий процесс называется shutdown?

Соответствует ли это тому, что

Стандарт неоднозначен.

Если процесс, вызывающий shutdown(fd, SHUT_RD), считается эквивалентным одноранговому вызову shutdown(fd, SHUT_WR), тогда он совместим.

С другой стороны, чтение текста строго, кажется, не соответствует требованиям. Но тогда нет кода ошибки для случая, когда процесс вызывает recv после shutdown(SHUT_RD). Коды ошибок являются исчерпывающими, что означает, что этот сценарий не является ошибкой, поэтому должен возвращать 0, как в соответствующей ситуации, когда одноранговый вызов вызывает shutdown(SHUT_WR).

Тем не менее, это то, что вы хотите - сообщение в пути может быть получено, если вы хотите. Если вы не хотите, чтобы они не вызывали recv.

В той степени, в которой это неоднозначно, это должно считаться ошибкой в ​​стандарте.

Почему данные post- shutdown recv не ограничены данными, которые были в пути

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

  • В случае сокетов unix данные могут быть буферизованы на принимающей стороне, операционной системой или на стороне отправки.
  • В случае TCP данные могут быть буферизованы процессом приема, операционной системой, аппаратным буфером сетевой карты, пакеты могут проходить транзитом на промежуточных маршрутизаторах, буферизованных аппаратурой передающей сетевой карты, путем отправки операционных системы или отправки.

Фон

  • posix предоставляет api для равномерного взаимодействия с различными типами потоков, включая анонимные каналы, именованные каналы, а также сокеты TCP и UDP IPv4 и IPv6... и raw Ethernet, Token Ring и IPX/SPX и X.25 и ATM...

  • Поэтому posix предоставляет набор функций, который в целом охватывает основные возможности большинства потоковых и пакетных протоколов.

  • Однако не все возможности поддерживаются протоколом

С точки зрения дизайна, если вызывающий абонент запрашивает операцию, которая не поддерживается базовым протоколом, существует ряд опций:

  • Введите состояние ошибки и запретите дальнейшие операции с файловым дескриптором.

  • Возврат ошибки из вызова, но в противном случае пренебрегайте им.

  • Верните успех и сделайте самое подходящее, что имеет смысл.

  • Внедрите какую-либо оболочку или наполнитель, чтобы предоставить отсутствующую функциональность.

Первые два параметра исключаются стандартом posix. Очевидно, что третий вариант был выбран разработчиками Linux.