首页 文章

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

提问于
浏览
22

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

Processing request for 21
Writing to 21
Closing 21

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

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

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

  • 2

    以下是我在许多类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) ,稍微延迟了'ensure',写入缓冲区在关闭之前被刷新了 . 这显然不是很聪明,因为:

    • 大多数时候延误太长了 .

    • 有些时候延迟太短 - 也许!

    • 这样的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(). 的包装器

    Edit: 一些评论:

    • 我的第一次入场有点尴尬 . 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,因为在我的应用程序中,我从不希望它们传递给孩子,更多重要的是,我不希望一个垂头丧气的孩子影响我 .

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

    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

    flushSocketBeforeCloseread 返回错误的情况下不会快速退出 . 它会一直循环,直到超时到期 . 你不能依赖 select 里面的 haveInput 来预测所有的错误 . read 有自己的错误(例如: EIO ) .

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

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

    GNU C library documentation说:

    完成套接字使用后,只需关闭即可关闭文件描述符

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

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

相关问题