почему write() не возвращает 0, когда должен?

Я столкнулся со случаем, когда использование write() на стороне сервера на удаленно закрытом клиенте не возвращает 0.

По словам человека 2 write :

В случае успеха возвращается количество записанных байтов (ноль означает, что ничего не было записано). При ошибке возвращается -1, и errno устанавливается соответствующим образом.

Насколько я понимаю: при использовании read/write на удаленно закрытом сокете первая попытка должна быть неудачной (таким образом, возвращается 0), а следующая попытка должна вызвать сломанный канал. Но это не так. write() действует так, как если бы ему удалось отправить данные с первой попытки, а затем я получаю сломанный канал при следующей попытке.

Мой вопрос: почему?

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

Ниже приведен код сервера, который я написал. На стороне клиента я попробовал базовый клиент C (с close() и shutdown() для закрытия сокета) и netcat. Все три дали мне тот же результат.


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

#define MY_STR "hello world!"

int start_server(int port)
{
  int fd;
  struct sockaddr_in sin;

  fd = socket(AF_INET, SOCK_STREAM, 0);
  if (fd == -1)
    {
      perror(NULL);
      return (-1);
    }
  memset(&sin, 0, sizeof(struct sockaddr_in));
  sin.sin_addr.s_addr = htonl(INADDR_ANY);
  sin.sin_family = AF_INET;
  sin.sin_port = htons(port);
  if (bind(fd, (struct sockaddr *)&sin, sizeof(struct sockaddr)) == -1
      || listen(fd, 0) == -1)
    {
      perror(NULL);
      close(fd);
      return (-1);
    }
  return (fd);
}

int accept_client(int fd)
{
  int client_fd;
  struct sockaddr_in client_sin;
  socklen_t client_addrlen;

  client_addrlen = sizeof(struct sockaddr_in);
  client_fd = accept(fd, (struct sockaddr *)&client_sin, &client_addrlen);
  if (client_fd == -1)
    return (-1);
  return (client_fd);
}

int main(int argc, char **argv)
{
  int fd, fd_client;
  int port;
  int ret;

  port = 1234;
  if (argc == 2)
    port = atoi(argv[1]);
  fd = start_server(port);
  if (fd == -1)
    return (EXIT_FAILURE);
  printf("Server listening on port %d\n", port);
  fd_client = accept_client(fd);
  if (fd_client == -1)
    {
      close(fd);
      printf("Failed to accept a client\n");
      return (EXIT_FAILURE);
    }
  printf("Client connected!\n");
  while (1)
    {
      getchar();
      ret = write(fd_client, MY_STR, strlen(MY_STR));
      printf("%d\n", ret);
      if (ret < 1)
    break ;
    }
  printf("the end.\n");
  return (0);
}

person yoones    schedule 03.10.2015    source источник
comment
Обратите внимание, что вы также должны учитывать тот факт, что write() может возвращать число больше 0, но меньше strlen(MY_STR), а также может возвращать -1 и сигнал EINTR.   -  person Dietrich Epp    schedule 03.10.2015
comment
В коде, предназначенном для производства, я бы так и сделал. Приведенный выше код был написан только для того, чтобы немного повозиться с write().   -  person yoones    schedule 03.10.2015
comment
Запись в сокет эквивалентна send(), а send() не гарантирует доставку данных.   -  person Vaughn Cato    schedule 03.10.2015
comment
Об этом следует сообщать как об ошибке на странице руководства. Сказать, что ноль указывает на то, что ничего не было записано, является вопиюще неправильным и противоречит спецификации функции write, которая запрещает когда-либо возвращать ноль, за исключением, возможно, (неуказанного) случая, когда аргумент nbyte равен нулю: pubs.opengroup.org/onlinepubs/9699919799/functions/write.html   -  person R.. GitHub STOP HELPING ICE    schedule 03.10.2015


Ответы (4)


Единственный способ заставить write возвращать ноль в сокете — попросить его записать нулевые байты. Если в сокете есть ошибка, вы всегда получите -1.

Если вы хотите получить индикатор «соединение закрыто», вам нужно использовать read, который будет возвращать 0 для удаленно закрытого соединения.

person Some programmer dude    schedule 03.10.2015

Именно так был написан интерфейс сокетов. Когда у вас есть подключенный сокет или канал, вы должны сначала закрыть передающий конец, а затем принимающий конец получит EOF и может отключиться. Закрытие принимающей стороны первым является «неожиданным», поэтому вместо возврата 0 возвращается ошибка.

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

bunzip2 < big_file.bz2 | head -n 10

Предположим, что big_file.bz2 огромен. Только первая часть будет прочитана, потому что bunzip2 будет убит, как только попытается отправить больше данных в head. Это позволяет выполнить всю команду намного быстрее и с меньшей загрузкой ЦП.

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

person Dietrich Epp    schedule 03.10.2015

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

Таким образом, когда вы write() подключаетесь к сокету, который закрыт удаленно, но не закрыт локально, вы не можете знать, ожидает ли другой конец еще чтения пакетов, и поэтому стек TCP буферизует ваши данные и попытается отправить их. Как указано на send() странице руководства,

В send() не подразумевается никаких указаний на невозможность доставки. Локально обнаруженные ошибки обозначаются возвращаемым значением -1.

(Когда вы write() подключаетесь к сокету, вы на самом деле send() подключаетесь к нему.)

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

Если удаленный партнер имеет только shutdown() записи, но все еще имеет открытый сокет, его стек TCP успешно получит пакет и подтвердит получение данных обратно отправителю.

person Douglas Santos    schedule 04.05.2016

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

"EPIPE  fd is connected to a pipe or *socket whose reading end is closed*."

Таким образом, вызов write() вернет не 0, а -1, а для errno будет установлено значение «EPIPE».

person user3629249    schedule 04.10.2015
comment
Тогда почему write() возвращает значение › 0 ? ACK (или его отсутствие) должен сообщить записи, что ему не удалось передать данные. - person yoones; 04.10.2015