29

我有一个多线程服务器(线程池),它使用 20 个线程处理大量请求(一个节点最多 500/秒)。有一个侦听器线程接受传入连接并将它们排队以供处理程序线程处理。一旦响应准备好,线程就会写出到客户端并关闭套接字。一切似乎都很好,直到最近,一个测试客户端程序在读取响应后开始随机挂起。经过大量挖掘,似乎来自服务器的 close() 实际上并没有断开套接字。我已经使用文件描述符编号向代码中添加了一些调试打印,我得到了这种类型的输出。

Processing request for 21
Writing to 21
Closing 21

close() 的返回值为 0,否则将打印另一个调试语句。在客户端挂起的此输出之后,lsof 显示已建立的连接。

服务器 8160 根 21u IPv4 32754237 TCP 本地主机:9980->本地主机:47530(已建立)

客户端 17747 根 12u IPv4 32754228 TCP 本地主机:47530->本地主机:9980(已建立)

就好像服务器从不向客户端发送关闭序列,并且此状态一直挂起,直到客户端被杀死,使服务器端处于关闭等待状态

服务器 8160 根 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();

这些没有任何区别。任何想法将不胜感激。

编辑——这最终成为队列库中的线程安全问题,导致套接字被多个线程不恰当地处理。

4

3 回答 3

76

这是我在许多类 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()两次来处理这种情况——黑客攻击)。
  • 没有迹象表明这是否有效。但这可能并不重要,如果 a) 硬重置完全没问题,和/或 b) 您可以控制链接的两侧。

但是做一个适当的冲洗是非常困难的。使用SO_LINGER显然不是要走的路;参见例如:

并且SIOCOUTQ似乎是特定于 Linux 的。

Noteshutdown(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);

在上面, mygetWallTimeEpoch()类似于time(),并且Perror()是一个包装器perror().

编辑:一些评论:

  • 我的第一次录取有点尴尬。OP 和 Nemo 质疑关闭前清除内部的需要so_error,但我现在找不到任何参考。有问题的系统是 HPUX 10.20。在失败之后connect(),只是调用close()并没有释放文件描述符,因为系统希望向我传递一个未解决的错误。但我和大多数人一样,从不费心检查的返回值,close. 所以我最终用完了文件描述符(ulimit -n),,这最终引起了我的注意。

  • (非常次要的一点)一位评论员反对硬编码的数字参数shutdown(),而不是例如 SHUT_WR 为 1。最简单的答案是 Windows 使用不同的#defines/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");
   }
于 2012-10-04T15:35:29.853 回答
2

Joseph Quinsey 给出了很好的回答。haveInput我对该功能有意见。想知道 select 返回您未包含在集合中的 fd 的可能性有多大。恕我直言,这将是一个主要的操作系统错误。select如果我为函数编写单元测试,而不是在普通应用程序中,这就是我要检查的事情。

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 的处理有关。理论上,如果一直返回 EINTR,你可能会陷入无限循环select,因为这个错误会让循环重新开始。鉴于非常短的超时 (0.01),它似乎不太可能发生。但是,我认为处理此问题的适当方法是将错误返回给调用者(flushSocketBeforeClose)。haveInput只要超时没有过期,调用者就可以继续调用,并声明其他错误失败。

补充 #1

flushSocketBeforeCloseread在返回错误的情况下不会快速退出。它将一直循环,直到超时到期。您不能依靠select内部haveInput来预测所有错误。read有自己的错误(例如:)EIO

     while (haveInput(fd, 0.01)) 
        if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop
           return TRUE; 
于 2013-07-11T20:01:55.853 回答
1

在我看来,这听起来像是您的 Linux 发行版中的一个错误。

GNU C 库文档说:

当你使用完一个套接字后,你可以简单地关闭它的文件描述符close

没有关于清除任何错误标志或等待数据被刷新或任何类似的事情。

你的代码很好;您的操作系统有错误。

于 2012-10-04T16:16:19.240 回答