我有一个多线程服务器(线程池),它使用 20 个线程处理大量请求(一个节点高达 500/秒)。有一个监听器线程接受传入的连接并将它们排队等待处理程序线程进行处理。一旦响应准备就绪,线程就会写出到客户端并关闭套接字。直到最近一切似乎都很好,一个测试客户端程序在读取响应后开始随机挂起。经过大量挖掘,似乎来自服务器的 close() 实际上并没有断开套接字。我在代码中添加了一些带有文件描述符编号的调试输出,我得到了这种类型的输出。
Processing request for 21
Writing to 21
Closing 21
close() 的返回值为 0,否则将打印另一条调试语句。客户端挂起后,lsof 显示已建立连接。
服务器 8160 root 21u IPv4 32754237 TCP localhost:9980->localhost:47530 (ESTABLISHED)
客户端 17747 root 12u IPv4 32754228 TCP localhost:47530->localhost:9980 (ESTABLISHED)
就好像服务器从来没有向客户端发送过关闭序列,这个状态一直挂起,直到客户端被杀掉,让服务器端处于关闭等待状态
服务器 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(最终版)
socket Action 如下
服务器:
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 年的时间才弄清楚如何关闭套接字。但又过了 10 年,我只是懒洋洋地调用 usleep(20000)
来稍微延迟一下,以“确保”写入缓冲区在关闭前被刷新。这显然不是很聪明,因为:
- 大部分时间延迟太长。
- 有些时候延迟太短了——也许吧!
- 可能会出现 SIGCHLD 这样的信号来结束
usleep()
(但我通常会调用usleep()
两次来处理这种情况——一个 hack)。 - 没有迹象表明这是否有效。但如果 a) 硬重置完全没问题,和/或 b) 您可以控制链接的两侧,这可能并不重要。
但是进行适当的冲洗非常困难。使用 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()
是 的包装器错误()。
编辑:一些评论:
我的第一次承认有点尴尬。 OP 和 Nemo 质疑在关闭之前清除内部
so_error
的需要,但我现在找不到任何引用资料。有问题的系统是 HPUX 10.20。在connect()
失败后,仅仅调用close()
并没有释放文件描述符,因为系统希望向我传递一个 Unresolved 错误。但是我和大多数人一样,从来没有费心去检查close.
的返回值,所以我最终用完了文件描述符(ulimit -n),
这终于引起了我的注意.(非常小的一点)一位评论员反对
shutdown()
的硬编码数字参数,而不是例如SHUT_WR 表示 1。最简单的答案是 Windows 使用不同的#defines/enums,例如SD_SEND
。许多其他作者(例如 Beej)和许多遗留系统一样使用常量。另外,我总是,总是,在我的所有套接字上设置 FD_CLOEXEC,因为在我的应用程序中,我从不希望它们传递给 child ,更重要的是,我不希望挂起的 child 影响我。
设置 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");
}
关于close() 没有正确关闭套接字,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12730477/