为什么 write() 在应该的时候不 return 0?

why write() doesn't return 0 when it should?

我遇到过这样的情况,在远程关闭的客户端上使用 write() 服务器端不会 return 0.

根据 man 2 write :

On success, the number of bytes written is returned (zero indicates nothing was written). On error, -1 is returned, and errno is set appropriately.

根据我的理解:在远程关闭的套接字上使用 read/write 时,第一次尝试应该会失败(因此 return 0),并且下一次尝试应该会触发破损的管道。但事实并非如此。 write() 就好像它在第一次尝试时成功发送了数据,然后在下一次尝试时我发现管道坏了。

我的问题是为什么?

我知道如何正确处理破损的管道,这不是问题所在。我只是想了解为什么 write 在这种情况下不 return 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);
}

使套接字上的 write return 为零的唯一方法是要求它写入零字节。如果套接字出现错误,您将始终得到 -1.

如果你想得到一个"connection closed"指标,你需要使用read return 0用于一个远程关闭的连接。

套接字接口就是这样写的。当你有一个连接的套接字或管道时,你应该先关闭发送端,然后接收端就会得到 EOF 并可以关闭。首先关闭接收端是 "unexpected" 所以它 returns 一个错误而不是返回 0.

这对管道很重要,因为它允许复杂的命令比其他方式更快地完成。例如,

bunzip2 < big_file.bz2 | head -n 10

假设 big_file.bz2 很大。只会读取第一部分,因为一旦 bunzip2 尝试向 head 发送更多数据,它就会被杀死。这使得整个命令完成得更快,并且使用更少 CPU。

套接字继承了相同的行为,但增加了复杂性,您必须分别关闭套接字的发送和接收部分。

如果您阅读了整个手册页,那么您会读到错误 return 值:

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

因此,对 write() 的调用不会 return 0 而是 -1 并且 errno 将设置为 'EPIPE'

需要注意的一点是,在TCP中,当连接的一侧关闭其 套接字,它实际上停止在该套接字上传输;它发送一个数据包到 通知它的远程对等点它不会再通过那个传输 联系。但是,这并不意味着它也停止接收。 (到 继续接收是关闭方的本地决定;如果它停止接收,它可以 丢失远程对等方传输的数据包。)

所以,当你 write() 到一个远程关闭的套接字时,但是 没有本地关闭,你无法知道另一端是否还在等待读取 更多数据包,因此 TCP 堆栈将缓冲您的数据并尝试发送它。作为 send() 手册页中所述,

No indication of failure to deliver is implicit in a send(). Locally detected errors are indicated by a return value of -1.

(当你 write() 到一个套接字时,你实际上是 send()ing 到它。)

当你 write() 第二次时,虽然,并且远程对等方肯定有 关闭套接字(不仅shutdown()写入),本地TCP堆栈可能已经 已经收到来自对等方的重置数据包,通知它有关错误的信息 最后发送的数据包。只有这样才能write() return报错,告诉 它的用户认为此管道已损坏(EPIPE 错误代码)。

如果远程端只有 shutdown() 写入,但套接字仍然打开, 它的 TCP 堆栈将成功接收数据包并将确认 收到数据返回给发送者。