защо 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