close()没有正确关闭套接字

时间:2012-10-04 15:17:32

标签: c sockets tcp client-server

我有一个多线程服务器(线程池),使用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)

CLIENT 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(最终版)

套接字操作如下

SERVER:

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

没什么特别的,只是一个基本的卷曲连接。客户端在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();

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

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

3 个答案:

答案 0 :(得分:56)

以下是我在许多类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的。

注意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().的包装

修改:有些意见:

  • 我的第一次入场有点尴尬。 OP和Nemo挑战了在关闭前清除内部so_error的必要性,但我现在无法找到任何参考。有问题的系统是HPUX 10.20。失败的connect()之后,只是调用close()没有释放文件描述符,因为系统希望向我发送一个未完成的错误。但是,像大多数人一样,我从不打扰检查close.的返回值。所以我最终用尽了文件描述符(ulimit -n),,最终引起了我的注意。

  • (非常小的一点)一位评论员反对shutdown()的硬编码数字论证,而不是SHUT_WR for 1.最简单的答案是Windows使用不同的#sninition / enums,例如: SD_SEND。许多其他作家(例如Beej)使用常量,许多遗留系统也是如此。

  • 此外,我总是在所有套接字上设置FD_CLOEXEC,因为在我的应用程序中,我从不希望它们传递给孩子,更重要的是,我不希望一个挂孩子影响我。 / p>

设置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");
   }

答案 1 :(得分:2)

Joseph Quinsey的精彩回答。我对haveInput函数有评论。想知道选择返回你没有包含在你的集合中的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的处理。从理论上讲,如果select保持返回EINTR,你可能陷入无限循环,因为这个错误让循环重新开始。鉴于超时很短(0.01),它似乎不太可能发生。但是,我认为处理此问题的适当方法是将错误返回给调用者(flushSocketBeforeClose)。只要其超时未到期,调用者就可以继续调用haveInput,并声明其他错误失败。

附加#1

如果flushSocketBeforeClose返回错误,

read将无法快速退出。它会一直循环,直到超时到期。您不能依赖select内的haveInput来预测所有错误。 read有自己的错误(例如:EIO)。

     while (haveInput(fd, 0.01)) 
        if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop
           return TRUE; 

答案 2 :(得分:0)

这听起来像是Linux发行版中的一个错误。

GNU C library documentation说:

  

使用套接字后,只需关闭其文件即可   close

的描述符

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

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