C: 套接字连接超时

Posted

技术标签:

【中文标题】C: 套接字连接超时【英文标题】:C: socket connection timeout 【发布时间】:2011-02-05 13:23:29 【问题描述】:

我有一个简单的程序来检查端口是否打开,但我想缩短套接字连接的超时长度,因为默认值太长了。我不确定如何做到这一点。代码如下:

#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv) 
    u_short port;                /* user specified port number */
    char addr[1023];             /* will be a copy of the address entered by u */
    struct sockaddr_in address;  /* the libc network address data structure */
    short int sock = -1;         /* file descriptor for the network socket */

    if (argc != 3) 
        fprintf(stderr, "Usage %s <port_num> <address>", argv[0]);
        return EXIT_FAILURE;
    

    address.sin_addr.s_addr = inet_addr(argv[2]); /* assign the address */
    address.sin_port = htons(atoi(argv[2]));            /* translate int2port num */

    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (connect(sock,(struct sockaddr *)&address,sizeof(address)) == 0) 
        printf("%i is open\n", port);
      
    close(sock);
    return 0;

【问题讨论】:

您在答案中添加了 "fcntl(sock, F_SETFL, O_NONBLOCK)" 请注意,在此之后,下一个套接字读取也将变为非阻塞! 【参考方案1】:

设置套接字非阻塞,并使用select()(它带有一个超时参数)。如果一个非阻塞套接字正在尝试连接,那么select() 将在connect() 完成(成功或不成功)时指示该套接字是可写的。然后您使用getsockopt() 来确定@​​987654325@ 的结果:

int main(int argc, char **argv) 
    u_short port;                /* user specified port number */
    char *addr;                  /* will be a pointer to the address */
    struct sockaddr_in address;  /* the libc network address data structure */
    short int sock = -1;         /* file descriptor for the network socket */
    fd_set fdset;
    struct timeval tv;

    if (argc != 3) 
        fprintf(stderr, "Usage %s <port_num> <address>\n", argv[0]);
        return EXIT_FAILURE;
    

    port = atoi(argv[1]);
    addr = argv[2];

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr(addr); /* assign the address */
    address.sin_port = htons(port);            /* translate int2port num */

    sock = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(sock, F_SETFL, O_NONBLOCK);

    connect(sock, (struct sockaddr *)&address, sizeof(address));

    FD_ZERO(&fdset);
    FD_SET(sock, &fdset);
    tv.tv_sec = 10;             /* 10 second timeout */
    tv.tv_usec = 0;

    if (select(sock + 1, NULL, &fdset, NULL, &tv) == 1)
    
        int so_error;
        socklen_t len = sizeof so_error;

        getsockopt(sock, SOL_SOCKET, SO_ERROR, &so_error, &len);

        if (so_error == 0) 
            printf("%s:%d is open\n", addr, port);
        
    

    close(sock);
    return 0;

【讨论】:

这在 *nix 中有效,但在 Windows 中无效。在 Windows 中,您可以通过查看上面代码中“select”的返回值来确定套接字是否已连接。在 Windows 中,如果连接完成,则 select 返回 1,如果连接不成功,则返回 0。如果您查看 so_error,那么即使连接失败,Windows 也始终返回 0。正如他们所说,那是你的窗户。 如果我不想对其他套接字操作(如读、写)使用非阻塞模式怎么办?连接套接字后我可以清除O_NONBLOCK 标志吗?如果可以,安全吗? @anton_rh:是的,清除O_NONBLOCK 并将套接字重新置于阻塞模式是安全的。 使用 poll 比 select 要好得多。没有人应该再在新软件中使用“选择”,因为当您将每个进程的打开文件描述符数量增加到非常有限的数量时,它会被破坏。也推荐直接在socket调用中使用SOCK_NONBLOCK(也可以使用SOCK_CLOEXEC) 我意识到这个线程已经很老了,但我确实对你的解决方案有疑问:我注意到你没有检查你的 connect() 调用的返回值。我已尝试复制您的示例,但出现错误 connect() returned errno 119 "Connection already in progress"。仅当我使用 fcntl() 调用时才如此;否则我不会出错。那么......这是预期的行为吗?谢谢。【参考方案2】:

这篇文章可能会有所帮助:

Connect with timeout (or another use for select() )

看起来您在连接之前将套接字置于非阻塞模式,然后在连接建立后将其恢复为阻塞模式。

void connect_w_to(void)  
  int res; 
  struct sockaddr_in addr; 
  long arg; 
  fd_set myset; 
  struct timeval tv; 
  int valopt; 
  socklen_t lon; 

  // Create socket 
  soc = socket(AF_INET, SOCK_STREAM, 0); 
  if (soc < 0)  
     fprintf(stderr, "Error creating socket (%d %s)\n", errno, strerror(errno)); 
     exit(0); 
   

  addr.sin_family = AF_INET; 
  addr.sin_port = htons(2000); 
  addr.sin_addr.s_addr = inet_addr("192.168.0.1"); 

  // Set non-blocking 
  if( (arg = fcntl(soc, F_GETFL, NULL)) < 0)  
     fprintf(stderr, "Error fcntl(..., F_GETFL) (%s)\n", strerror(errno)); 
     exit(0); 
   
  arg |= O_NONBLOCK; 
  if( fcntl(soc, F_SETFL, arg) < 0)  
     fprintf(stderr, "Error fcntl(..., F_SETFL) (%s)\n", strerror(errno)); 
     exit(0); 
   
  // Trying to connect with timeout 
  res = connect(soc, (struct sockaddr *)&addr, sizeof(addr)); 
  if (res < 0)  
     if (errno == EINPROGRESS)  
        fprintf(stderr, "EINPROGRESS in connect() - selecting\n"); 
        do  
           tv.tv_sec = 15; 
           tv.tv_usec = 0; 
           FD_ZERO(&myset); 
           FD_SET(soc, &myset); 
           res = select(soc+1, NULL, &myset, NULL, &tv); 
           if (res < 0 && errno != EINTR)  
              fprintf(stderr, "Error connecting %d - %s\n", errno, strerror(errno)); 
              exit(0); 
            
           else if (res > 0)  
              // Socket selected for write 
              lon = sizeof(int); 
              if (getsockopt(soc, SOL_SOCKET, SO_ERROR, (void*)(&valopt), &lon) < 0)  
                 fprintf(stderr, "Error in getsockopt() %d - %s\n", errno, strerror(errno)); 
                 exit(0); 
               
              // Check the value returned... 
              if (valopt)  
                 fprintf(stderr, "Error in delayed connection() %d - %s\n", valopt, strerror(valopt) 
); 
                 exit(0); 
               
              break; 
            
           else  
              fprintf(stderr, "Timeout in select() - Cancelling!\n"); 
              exit(0); 
            
         while (1); 
      
     else  
        fprintf(stderr, "Error connecting %d - %s\n", errno, strerror(errno)); 
        exit(0); 
      
   
  // Set to blocking mode again... 
  if( (arg = fcntl(soc, F_GETFL, NULL)) < 0)  
     fprintf(stderr, "Error fcntl(..., F_GETFL) (%s)\n", strerror(errno)); 
     exit(0); 
   
  arg &= (~O_NONBLOCK); 
  if( fcntl(soc, F_SETFL, arg) < 0)  
     fprintf(stderr, "Error fcntl(..., F_SETFL) (%s)\n", strerror(errno)); 
     exit(0); 
   
  // I hope that is all 

【讨论】:

这是一个很好的答案,你为什么把它做成社区维基?您应该因推荐资源而赢得一些声誉。 链接到的论坛好像改了软件,所以链接现在失效了。 注意:虽然这个答案被认为是正确的,但下面caf的解决方案不仅解释了策略,还提供了工作代码。 这个解决方案(和大多数其他解决方案一样)有一个缺陷,即当signal/poll 被 EINTR 中断时,它会从 0 开始超时,即使已经有一段时间了过去。请参阅我的解决方案来解决这个问题。 这里的“do while”循环有什么意义?看来这个循环只能运行一次。【参考方案3】:

关于使用select()/poll()的答案是正确的,代码应该这样写才能移植。

但是,由于您使用的是 Linux,因此您可以这样做:

int synRetries = 2; // Send a total of 3 SYN packets => Timeout ~7s
setsockopt(fd, IPPROTO_TCP, TCP_SYNCNT, &synRetries, sizeof(synRetries));

请参阅 man 7 tcpman setsockopt

我用它来加速我需要快速修补的程序中的连接超时。通过select()/poll() 将其破解为超时不是一种选择。

【讨论】:

不错!简单实用!包括添加:#include synRetries = 1 会导致约 3 秒的超时(根据经验),不幸的是 synRetries = 0 会导致约 127 秒的超时(而不是预期的约 1 秒)。 synRetries = 2 ~7s 超时确认。【参考方案4】:

这是一个现代的connect_with_timeout 实现,使用poll,具有适当的错误和信号处理:

#include <sys/socket.h>
#include <fcntl.h>
#include <poll.h>
#include <time.h>

int connect_with_timeout(int sockfd, const struct sockaddr *addr, socklen_t addrlen, unsigned int timeout_ms) 
    int rc = 0;
    // Set O_NONBLOCK
    int sockfd_flags_before;
    if((sockfd_flags_before=fcntl(sockfd,F_GETFL,0)<0)) return -1;
    if(fcntl(sockfd,F_SETFL,sockfd_flags_before | O_NONBLOCK)<0) return -1;
    // Start connecting (asynchronously)
    do 
        if (connect(sockfd, addr, addrlen)<0) 
            // Did connect return an error? If so, we'll fail.
            if ((errno != EWOULDBLOCK) && (errno != EINPROGRESS)) 
                rc = -1;
            
            // Otherwise, we'll wait for it to complete.
            else 
                // Set a deadline timestamp 'timeout' ms from now (needed b/c poll can be interrupted)
                struct timespec now;
                if(clock_gettime(CLOCK_MONOTONIC, &now)<0)  rc=-1; break; 
                struct timespec deadline =  .tv_sec = now.tv_sec,
                                             .tv_nsec = now.tv_nsec + timeout_ms*1000000l;
                // Wait for the connection to complete.
                do 
                    // Calculate how long until the deadline
                    if(clock_gettime(CLOCK_MONOTONIC, &now)<0)  rc=-1; break; 
                    int ms_until_deadline = (int)(  (deadline.tv_sec  - now.tv_sec)*1000l
                                                  + (deadline.tv_nsec - now.tv_nsec)/1000000l);
                    if(ms_until_deadline<0)  rc=0; break; 
                    // Wait for connect to complete (or for the timeout deadline)
                    struct pollfd pfds[] =   .fd = sockfd, .events = POLLOUT  ;
                    rc = poll(pfds, 1, ms_until_deadline);
                    // If poll 'succeeded', make sure it *really* succeeded
                    if(rc>0) 
                        int error = 0; socklen_t len = sizeof(error);
                        int retval = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
                        if(retval==0) errno = error;
                        if(error!=0) rc=-1;
                    
                
                // If poll was interrupted, try again.
                while(rc==-1 && errno==EINTR);
                // Did poll timeout? If so, fail.
                if(rc==0) 
                    errno = ETIMEDOUT;
                    rc=-1;
                
            
        
     while(0);
    // Restore original O_NONBLOCK state
    if(fcntl(sockfd,F_SETFL,sockfd_flags_before)<0) return -1;
    // Success
    return rc;

【讨论】:

+1000。哇。我不得不说这是目前 *** 上最被低估的答案! +1 这绝对是美丽的。简单、极其有效、完全独立、记录在案,并且远离使用 select。 在其他答案中,这是我选择的答案。非阻塞+轮询。 不错!与往常一样,如果使用 select() 的解决方案少于 20 行,它通常会被破坏。 select() 有困难的语义。 这很好,谢谢。我仍然想知道为什么没有connect 函数允许我提供超时值作为参数。似乎是一个共同的需求。就目前而言,如果我想要默认超时以外的其他东西,我必须用大量代码替换我的一个 connect 调用。【参考方案5】:

在 Linux 上你也可以使用:

struct timeval timeout;
timeout.tv_sec  = 7;  // after 7 seconds connect() will timeout
timeout.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
connect(...)

如果不需要,别忘了在connect() 之后清除SO_SNDTIMEO

【讨论】:

我更喜欢上面的表格,在 setsockopt 中带有 SOL_SOCKET。【参考方案6】:

这个参数化了 ip、端口、超时(以秒为单位)、处理连接错误并为您提供以毫秒为单位的连接时间:

#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>

int main(int argc, char **argv) 
    struct sockaddr_in addr_s;
    char *addr;
    short int fd=-1;
    int port;
    fd_set fdset;
    struct timeval tv;
    int rc;
    int so_error;
    socklen_t len;
    struct timespec tstart=0,0, tend=0,0;
    int seconds;

    if (argc != 4) 
        fprintf(stderr, "Usage: %s <ip> <port> <timeout_seconds>\n", argv[0]);
        return 1;
    

    addr = argv[1];
    port = atoi(argv[2]);
    seconds = atoi(argv[3]);

    addr_s.sin_family = AF_INET; // utilizzo IPv4
    addr_s.sin_addr.s_addr = inet_addr(addr);
    addr_s.sin_port = htons(port);

    clock_gettime(CLOCK_MONOTONIC, &tstart);

    fd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(fd, F_SETFL, O_NONBLOCK); // setup non blocking socket

    // make the connection
    rc = connect(fd, (struct sockaddr *)&addr_s, sizeof(addr_s));
    if ((rc == -1) && (errno != EINPROGRESS)) 
        fprintf(stderr, "Error: %s\n", strerror(errno));
        close(fd);
        return 1;
    
    if (rc == 0) 
        // connection has succeeded immediately
        clock_gettime(CLOCK_MONOTONIC, &tend);
        printf("socket %s:%d connected. It took %.5f seconds\n",
            addr, port, (((double)tend.tv_sec + 1.0e-9*tend.tv_nsec) - ((double)tstart.tv_sec + 1.0e-9*tstart.tv_nsec)));

        close(fd);
        return 0;
     /*else 
        // connection attempt is in progress
     */

    FD_ZERO(&fdset);
    FD_SET(fd, &fdset);
    tv.tv_sec = seconds;
    tv.tv_usec = 0;

    rc = select(fd + 1, NULL, &fdset, NULL, &tv);
    switch(rc) 
    case 1: // data to read
        len = sizeof(so_error);

        getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &len);

        if (so_error == 0) 
            clock_gettime(CLOCK_MONOTONIC, &tend);
            printf("socket %s:%d connected. It took %.5f seconds\n",
                addr, port, (((double)tend.tv_sec + 1.0e-9*tend.tv_nsec) - ((double)tstart.tv_sec + 1.0e-9*tstart.tv_nsec)));
            close(fd);
            return 0;
         else  // error
            printf("socket %s:%d NOT connected: %s\n", addr, port, strerror(so_error));
        
        break;
    case 0: //timeout
        fprintf(stderr, "connection timeout trying to connect to %s:%d\n", addr, port);
        break;
    

    close(fd);
    return 0;

【讨论】:

【参考方案7】:

Nahuel Greco的解决方案除了编译错误还有什么问题吗?

如果我改变一行

// Compilation error
setsockopt(fd, SO_SNDTIMEO, &timeout, sizeof(timeout));

// Fixed?
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));

然后它似乎像宣传的那样工作 - socket() 返回超时错误。

结果代码:

struct timeval timeout;
timeout.tv_sec  = 7;  // after 7 seconds connect() will timeout
timeout.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
connect(...)

我不知道在发送超时和非阻塞套接字之间进行权衡,但我很想学习。

【讨论】:

如果您打算在阻塞模式下使用套接字,您将通过实施 SO_SNDTIMEO 解决方案来减少系统调用,而不是将套接字置于非阻塞模式、连接然后再回到阻塞模式。似乎快了一点。【参考方案8】:

SO_RCVTIMEOSO_SNDTIMEO 这两个套接字选项对connect 没有影响。以下是包含此说明的屏幕截图的链接,这里我只是简要介绍一下。使用connect 实现超时的恰当方法是使用signalselect or poll

信号

connect 可以通过使用系统调用(包装器)alarm 被自生成信号SIGALRM 中断。但是,应该为相同的信号安装信号处理,否则程序将被终止。代码是这样的......

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<signal.h>
#include<errno.h>

static void signal_handler(int signo)

    return; // Do nothing just interrupt.


int main()

    /* Register signal handler */

    struct sigaction act, oact;

    act.sa_handler = signal_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

#ifdef SA_INTERRUPT
    act.sa_flags |= SA_INTERRUPT;
#endif

    if(sigaction(SIGALRM, &act, &oact) < 0)  // Error registering signal handler.
    
        fprintf(stderr, "Error registering signal disposition\n");
        exit(1);
    

    /* Prepare your socket and sockaddr structures */

    int sockfd;
    struct sockaddr* servaddr;

    /* Implementing timeout connect */

    int sec = 30;

    if(alarm(sec) != 0)
        fprintf(stderr, "Already timer was set\n");

    if(connect(sockfd, servaddr, sizeof(struct sockaddr)) < 0)
    
        if(errno == EINTR)
            fprintf(stderr, "Connect timeout\n");
        else
            fprintf(stderr, "Connect failed\n");

        close(sockfd);

        exit(1);
    

    alarm(0);  /* turn off the alarm */

    sigaction(SIGALRM, &oact, NULL);  /* Restore the default actions of SIGALRM */

    /* Use socket */


    /* End program */

    close(sockfd);
    return 0;

选择或投票

由于已经有一些用户很好地解释了如何使用select 来实现connect 超时,所以我没有必要再重复一遍。 poll 可以以相同的方式使用。但是,所有答案中都存在一些共同的错误,我想解决这些错误。

即使套接字是非阻塞的,如果我们连接的服务器在同一台本地机器上,connect 可能会成功返回。所以建议在调用select之前检查connect的返回值。

Berkeley 派生的实现(和 POSIX)对非阻塞套接字和connect 具有以下规则。

1) 连接成功完成后,描述符变为可写(TCPv2 的第 531 页)。

2)当连接建立遇到错误时,描述符变为可读可写(TCPv2第530页)。

所以代码应该处理这些情况,这里我只是编写了必要的修改代码。

/* All the code stays */

/* Modifications at connect */

int conn_ret = connect(sockfd, servaddr, sizeof(struct sockdaddr));

if(conn_ret == 0)
    goto done;

/* Modifications at select */

int sec = 30;
for( ; ; )

    struct timeval timeo;
    timeo.tv_sec = sec;
    timeo.tv_usec = 0;

    fd_set wr_set, rd_set;
    FDZERO(&wr_set);
    FD_SET(sockfd, &wr_set);
    rd_set = wr_set;
    int sl_ret = select(sockfd + 1, &rd_set, &wr_set, NULL, &timeo);

    /* All the code stays */



done:

    /* Use your socket */

Text from Unix Network Programming Volume 1

【讨论】:

以上是关于C: 套接字连接超时的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法区分连接超时和套接字超时?

如何配置套接字连接超时

如何配置套接字连接超时

如何设定ClientSocket的超时时间。

ZMQ 套接字连接超时

android 套接字连接超时