我有一个从服务器读取大文件并经常挂在特定计算机上的应用程序。它已经在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; }
我的测试中有一些注意事项:
可在以下位置找到测试应用程序的源:
套接字测试源
从Loopback接口捕获的tcpdump可以在以下位置找到:
tcpdump捕获
我通过发出以下命令来重现该问题:
> gcc socket_test.c -o socket_test > perl -e 'for (1..6000000){ print "a" }' | nc -l 60000 > ./socket_test 6000000
这将看到发送到测试应用程序的6000000字节,该应用程序尝试通过对recv()的一次调用来读取数据。
我很乐意听到关于我可能做错了什么的任何建议,或任何其他调试问题的方法。
MSG_WAITALL 应该 阻塞,直到收到所有数据。在recv的手册页中:
MSG_WAITALL
该标志请求操作块,直到满足完整请求为止。
但是,网络堆栈中的缓冲区可能不够大,无法容纳所有内容,这就是服务器上出现错误消息的原因。客户端网络堆栈根本无法容纳那么多数据。
解决方案是增加缓冲区大小(的SO_RCVBUF选项setsockopt),将消息拆分为较小的片段,或接收较小的块将其放入您自己的缓冲区中。最后是我的建议。
SO_RCVBUF
setsockopt
编辑: 我在您的代码中看到您已经按照我的建议进行操作(使用自己的缓冲读取较小的块),因此只需删除该MSG_WAITALL标志即可。
哦,当recv返回零时,意味着另一端已关闭连接,您也应该这样做。
recv