close()没有正确关闭套接字
我有一个multithreading服务器(线程池),使用20个线程处理大量请求(一个节点最多500 /秒)。 有一个侦听器线程接受传入连接并将它们排队以供处理程序线程处理。 一旦响应准备就绪,线程就会写出到客户端并关闭套接字。 直到最近,一切似乎都很好,一个测试客户端程序在阅读响应后开始随机挂起。 经过大量挖掘后,似乎服务器的close()实际上并没有断开套接字。 我已经使用文件描述符编号为代码添加了一些调试打印,我得到了这种类型的输出。
Processing request for 21 Writing to 21 Closing 21
close()的返回值为0,否则将打印另一个调试语句。 使用挂起的客户端输出此输出后,lsof将显示已建立的连接。
SERVER 8160 root 21u IPv4 32754237 TCP localhost:9980-> localhost:47530(ESTABLISHED)
客户端17747 root 12u IPv4 32754228 TCP localhost:47530-> localhost:9980(ESTABLISHED)
就像服务器永远不会将关闭序列发送到客户端一样,这种状态会一直挂起,直到客户端被终止,让服务器端处于关闭等待状态
SERVER 8160 root 21u IPv4 32754237 TCP localhost:9980-> localhost:47530(CLOSE_WAIT)
此外,如果客户端指定了超时,它将超时而不是挂起。 我也可以手动运行
call close(21)
在gdb的服务器中,客户端将断开连接。 这可能在50,000个请求中发生一次,但可能不会在较长时间内发生。
Linux版本:2.6.21.7-2.fc8xen Centos版本:5.4(最终版)
套接字动作如下
服务器:
int client_socket; struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr);
while(true) { client_socket = accept(incoming_socket, (struct sockaddr *)&client_addr, &client_len); if (client_socket == -1) continue; /* insert into queue here for threads to process */ }
然后线程获取套接字并构建响应。
/* get client_socket from queue */ /* processing request here */ /* now set to blocking for write; was previously set to non-blocking for reading */ int flags = fcntl(client_socket, F_GETFL); if (flags < 0) abort(); if (fcntl(client_socket, F_SETFL, flags|O_NONBLOCK) < 0) abort(); server_write(client_socket, response_buf, response_length); server_close(client_socket);
server_write和server_close。
void server_write( int fd, char const *buf, ssize_t len ) { printf("Writing to %d\n", fd); while(len > 0) { ssize_t n = write(fd, buf, len); if(n <= 0) return;// I don't really care what error happened, we'll just drop the connection len -= n; buf += n; } } void server_close( int fd ) { for(uint32_t i=0; i<10; i++) { int n = close(fd); if(!n) {//closed successfully return; } usleep(100); } printf("Close failed for %d\n", fd); }
客户:
客户端使用的是libcurl v 7.27.0
CURL *curl = curl_easy_init(); CURLcode res; curl_easy_setopt( curl, CURLOPT_URL, url); curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, write_callback ); curl_easy_setopt( curl, CURLOPT_WRITEDATA, write_tag ); res = curl_easy_perform(curl);
没什么特别的,只是一个基本的curl连接。 客户端在tranfer.c中挂起(在libcurl中),因为套接字不会被视为已关闭。 它正在等待来自服务器的更多数据。
到目前为止我尝试过的事情:
关闭前关机
shutdown(fd, SHUT_WR); char buf[64]; while(read(fd, buf, 64) > 0); /* then close */
将SO_LINGER设置为在1秒内强制关闭
struct linger l; l.l_onoff = 1; l.l_linger = 1; if (setsockopt(client_socket, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1) abort();
这些没有区别。 任何想法将不胜感激。
编辑 – 这最终成为队列库中的线程安全问题,导致多个线程不适当地处理套接字。
以下是我在许多类Unix系统(例如SunOS 4,SGI IRIX,HPUX 10.20,CentOS 5,Cygwin)上使用的一些代码来关闭套接字:
int getSO_ERROR(int fd) { int err = 1; socklen_t len = sizeof err; if (-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&err, &len)) FatalError("getSO_ERROR"); if (err) errno = err; // set errno to the socket SO_ERROR return err; } void closeSocket(int fd) { // *not* the Windows closesocket() if (fd >= 0) { getSO_ERROR(fd); // first clear any errors, which can cause close to fail if (shutdown(fd, SHUT_RDWR) < 0) // secondly, terminate the 'reliable' delivery if (errno != ENOTCONN && errno != EINVAL) // SGI causes EINVAL Perror("shutdown"); if (close(fd) < 0) // finally call close() Perror("close"); } }
但上述内容并不能保证发送任何缓冲写入。
优雅的关闭:我花了大约10年时间才弄清楚如何关闭套接字。 但是在usleep(20000)
10年里,我懒得打电话给usleep(20000)
稍稍延迟,以确保写入缓冲区在关闭之前被刷新。 这显然不是很聪明,因为:
- 大部分时间延迟太长了。
- 有时候延迟太短 - 也许!
- 可能会发生SIGCHLD这样的信号来结束
usleep()
(但我通常称为usleep()
两次来处理这种情况 - 一个黑客)。 - 没有迹象表明这是否有效。 但是,如果a)硬复位完全正常,和/或b)您可以控制链路的两侧,这可能并不重要。
但是进行适当的冲洗是非常困难的。 使用SO_LINGER
显然不是 SO_LINGER
的方法; 看看例如:
- http://msdn.microsoft.com/en-us/library/ms740481%28v=vs.85%29.aspx
- https://www.google.ca/#q=the-ultimate-so_linger-page
SIOCOUTQ
似乎是特定于Linux的。
注意shutdown(fd, SHUT_WR)
不会停止写入,与其名称相反,可能与man 2 shutdown
相反。
此代码flushSocketBeforeClose()
等待读取零字节,或直到计时器到期。 函数haveInput()
是select(2)的简单包装器,并设置为阻塞最多1/100秒。
bool haveInput(int fd, double timeout) { int status; fd_set fds; struct timeval tv; FD_ZERO(&fds); FD_SET(fd, &fds); tv.tv_sec = (long)timeout; // cast needed for C++ tv.tv_usec = (long)((timeout - tv.tv_sec) * 1000000); // 'suseconds_t' while (1) { if (!(status = select(fd + 1, &fds, 0, 0, &tv))) return FALSE; else if (status > 0 && FD_ISSET(fd, &fds)) return TRUE; else if (status > 0) FatalError("I am confused"); else if (errno != EINTR) FatalError("select"); // tbd EBADF: man page "an error has occurred" } } bool flushSocketBeforeClose(int fd, double timeout) { const double start = getWallTimeEpoch(); char discard[99]; ASSERT(SHUT_WR == 1); if (shutdown(fd, 1) != -1) while (getWallTimeEpoch() < start + timeout) while (haveInput(fd, 0.01)) // can block for 0.01 secs if (!read(fd, discard, sizeof discard)) return TRUE; // success! return FALSE; }
使用示例:
if (!flushSocketBeforeClose(fd, 2.0)) // can block for 2s printf("Warning: Cannot gracefully close socket\n"); closeSocket(fd);
在上面,我的getWallTimeEpoch()
类似于time(),
而Perror()
是perror().
的包装器perror().
编辑:一些评论:
-
我的第一次入场有点尴尬。 OP和Nemo挑战了在关闭前清除内部
so_error
,但我现在无法找到任何参考。 有问题的系统是HPUX 10.20。 在connect()
失败后,只是调用close()
没有释放文件描述符,因为系统希望向我发送一个未完成的错误。 但是,我和大多数人一样,从不打算检查close.
的回报值close.
所以我最终用完了文件描述符(ulimit -n),
最终引起了我的注意。 -
(非常小的一点)一位评论员反对
shutdown()
的硬编码数字参数,而不是例如1的SHUT_WR。最简单的答案是Windows使用不同的SD_SEND
/ enums,例如SD_SEND
。 许多其他编写者(例如Beej)使用常量,许多遗留系统也是如此。 -
另外,我总是在所有套接字上设置FD_CLOEXEC,因为在我的应用程序中,我从不希望它们传递给孩子,更重要的是,我不希望一个挂孩子影响我。
设置CLOEXEC的示例代码:
static void setFD_CLOEXEC(int fd) { int status = fcntl(fd, F_GETFD, 0); if (status >= 0) status = fcntl(fd, F_SETFD, status | FD_CLOEXEC); if (status < 0) Perror("Error getting/setting socket FD_CLOEXEC flags"); }
Joseph Quinsey的精彩回答。 我对haveInput
函数有意见。 想知道选择返回你没有包含在你的集合中的fd的可能性。 这将是一个主要的操作系统错误恕我直言。 如果我为select
函数编写unit testing,那就是我要检查的那种东西,而不是普通的应用程序。
if (!(status = select(fd + 1, &fds, 0, 0, &tv))) return FALSE; else if (status > 0 && FD_ISSET(fd, &fds)) return TRUE; else if (status > 0) FatalError("I am confused"); // <--- fd unknown to function
我的其他评论涉及EINTR的处理。 理论上,如果select
继续返回EINTR,你可能陷入无限循环,因为这个错误让循环重新开始。 鉴于超时很短(0.01),它似乎不太可能发生。 但是,我认为处理此问题的适当方法是将错误返回给调用者( flushSocketBeforeClose
)。 调用者可以继续调用haveInput
,因为其超时尚未到期,并声明其他错误失败。
附加#1
如果read
返回错误, flushSocketBeforeClose
将不会快速退出。 它会一直循环,直到超时到期。 您不能依赖于haveInput
内部来预测所有错误。 read
有自己的错误(例如: EIO
)。
while (haveInput(fd, 0.01)) if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop return TRUE;
这听起来像是Linux发行版中的一个错误。
GNU C库文档说:
使用完套接字后,只需关闭即可关闭其文件描述符
没有关于清除任何错误标志或等待刷新数据或任何此类事情的事情。
你的代码很好; 你的操作系统有一个错误。