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

Dav*_*rey 24 c sockets tcp client-server

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

Processing request for 21
Writing to 21
Closing 21
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

在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  */
}
Run Code Online (Sandbox Code Playgroud)

然后线程获取套接字并构建响应.

/*  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);
Run Code Online (Sandbox Code Playgroud)

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);
  }
Run Code Online (Sandbox Code Playgroud)

客户:

客户端使用的是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);
Run Code Online (Sandbox Code Playgroud)

没什么特别的,只是一个基本的卷曲连接.客户端在tranfer.c中挂起(在libcurl中),因为套接字不会被视为已关闭.它正在等待来自服务器的更多数据.

到目前为止我尝试过的事情:

关闭前关机

shutdown(fd, SHUT_WR);                                                                                                                                            
char buf[64];                                                                                                                                                     
while(read(fd, buf, 64) > 0);                                                                                                                                         
/*  then close  */ 
Run Code Online (Sandbox Code Playgroud)

将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();
Run Code Online (Sandbox Code Playgroud)

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

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

Jos*_*sey 60

以下是我在许多类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");
   }
}
Run Code Online (Sandbox Code Playgroud)

但上述内容并不能保证发送任何缓冲写入.

优雅的关闭:我花了大约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;
}
Run Code Online (Sandbox Code Playgroud)

使用示例:

   if (!flushSocketBeforeClose(fd, 2.0)) // can block for 2s
       printf("Warning: Cannot gracefully close socket\n");
   closeSocket(fd);
Run Code Online (Sandbox Code Playgroud)

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

编辑:一些评论:

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

  • (非常小的一点)一位评论员反对硬编码的数字论证shutdown(),而不是像SHUT_WR那样1.最简单的答案是Windows使用不同的#degine/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");
   }
Run Code Online (Sandbox Code Playgroud)

  • 我希望我能两次投票.这只是我在野外看到的正确关闭套接字的第二个样本. (5认同)
  • 我认为`shutdown`应该使用相应的宏`SHUT_RD`等进行操作 (2认同)
  • 以防万一其他人试图弄清楚“getSO_ERROR()”如何有助于解决问题:事实证明,使用“SO_ERROR”调用“getsockopt”将首先获取错误状态,然后重置它。这些信息对我来说并不容易找到,我也不确定它是否可移植。以下手册页记录了此行为:https://linux.die.net/man/3/getsockopt 但我的发行版上的相同手册页(`man 3 getsockopt`)却没有(RHEL8)。 (2认同)