套接字recv()在MSG_WAITALL的大消息上挂起

Sha*_*arr 10 c sockets linux networking tcp

我有一个应用程序从服务器读取大文件并经常挂起在特定的机器上.它已在RHEL5.2下成功运行了很长时间.我们最近升级到RHEL6.1,它现在定期挂起.

我创建了一个可以重现问题的测试应用程序.它在100个中挂起约98次.

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/time.h>

int mFD = 0;

void open_socket()
{
  struct addrinfo hints, *res;
  memset(&hints, 0, sizeof(hints));
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_family = AF_INET;

  if (getaddrinfo("localhost", "60000", &hints, &res) != 0)
  {
    fprintf(stderr, "Exit %d\n", __LINE__);
    exit(1);
  }

  mFD = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

  if (mFD == -1)
  {
    fprintf(stderr, "Exit %d\n", __LINE__);
    exit(1);
  }

  if (connect(mFD, res->ai_addr, res->ai_addrlen) < 0)
  {
    fprintf(stderr, "Exit %d\n", __LINE__);
    exit(1);
  }

  freeaddrinfo(res);
}

void read_message(int size, void* data)
{
  int bytesLeft = size;
  int numRd = 0;

  while (bytesLeft != 0)
  {
    fprintf(stderr, "reading %d bytes\n", bytesLeft);

    /* Replacing MSG_WAITALL with 0 works fine */
    int num = recv(mFD, data, bytesLeft, MSG_WAITALL);

    if (num == 0)
    {
      break;
    }
    else if (num < 0 && errno != EINTR)
    {
      fprintf(stderr, "Exit %d\n", __LINE__);
      exit(1);
    }
    else if (num > 0)
    {
      numRd += num;
      data += num;
      bytesLeft -= num;
      fprintf(stderr, "read %d bytes - remaining = %d\n", num, bytesLeft);
    }
  }

  fprintf(stderr, "read total of %d bytes\n", numRd);
}

int main(int argc, char **argv)
{
  open_socket();

  uint32_t raw_len = atoi(argv[1]);
  char raw[raw_len];

  read_message(raw_len, raw);

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

我测试的一些注意事项:

  • 如果"localhost"映射到环回地址127.0.0.1,应用程序将挂起对recv()的调用并且NEVER返回.
  • 如果"localhost"映射到计算机的ip,从而通过以太网接口路由数据包,则应用程序成功完成.
  • 当我遇到挂起时,服务器发送"TCP窗口已满"消息,客户端以"TCP ZeroWindow"消息响应(请参阅图像并附上tcpdump capture).从这一点来看,它会永远挂起,服务器发送keep-alives,客户端发送ZeroWindow消息.客户端似乎永远不会扩展其窗口,允许传输完成.
  • 在挂起期间,如果我检查"netstat -a"的输出,则服务器发送队列中有数据,但客户端接收队列为空.
  • 如果我从recv()调用中删除MSG_WAITALL标志,则应用程序成功完成.
  • 悬挂问题仅在1台特定机器上使用环回接口时出现.我怀疑这可能都与时序依赖性有关.
  • 当我删除'文件'的大小时,挂起的可能性就会降低

测试应用程序的源代码可以在这里找到:

套接字测试源

可以在此处找到loopback接口的tcpdump捕获:

tcpdump捕获

我通过发出以下命令重现该问题:

>  gcc socket_test.c -o socket_test
>  perl -e 'for (1..6000000){ print "a" }' | nc -l 60000
>  ./socket_test 6000000
Run Code Online (Sandbox Code Playgroud)

这看到发送到测试应用程序的6000000个字节,它试图使用一次调用recv()来读取数据.

我很想听听有关我可能做错的建议或任何进一步调试问题的方法.

Som*_*ude 15

MSG_WAITALL 应该阻止,直到收到所有数据.从recv手册页:

该标志请求操作块直到满足完整请求.

但是,网络堆栈中的缓冲区可能不足以容纳所有内容,这就是服务器上出现错误消息的原因.客户端网络堆栈根本无法保存那么多数据.

解决方案是增加缓冲区大小(SO_RCVBUF选项setsockopt),将消息拆分成更小的块,或者接收更小的块放入自己的缓冲区.最后是我会推荐的.

编辑:我在你的代码中看到你已经做了我建议的事情(用自己的缓冲读取较小的块),所以只需删除MSG_WAITALL标志即可.

哦,当recv返回零时,这意味着另一端关闭了连接,你也应该这样做.