C: 套接字连接超时

66

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

#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)"的代码。注意,这之后下一个套接字读取也变成了非阻塞! - user2791114
有趣的是,在这个 man 页面中提到了这一点,但在我安装在 Ubuntu 上的页面中,你必须具备阅读关于“EINPROGRESS”错误的信息的智慧才能理解套接字可以是非阻塞的,以进行异步连接。 - Alexis Wilke
8个回答

76

将socket设置为非阻塞,并使用select()(它需要一个超时参数)。如果一个非阻塞的socket正在尝试连接,那么当connect()完成时(无论成功与否),select()将指示该socket是可写的。然后您可以使用getsockopt()来确定connect()的结果:

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

9
这在*nix系统下有效,但在Windows中无效。在Windows中,你可以通过查看上述代码中"select"的返回值来确定套接字是否已连接。在Windows中,如果连接成功,select返回1;如果连接不成功,它将返回0。如果你查看so_error,那么Windows始终返回0,即使连接失败。正如人们所说,这就是Windows。 - deltamind106
1
如果我不想在其他套接字操作(如读取、写入)中使用非阻塞模式怎么办?连接套接字后可以清除 O_NONBLOCK 标志吗?如果可能的话,这样做是否安全? - anton_rh
1
@anton_rh:是的,清除O_NONBLOCK并将套接字重新设置为阻塞模式是安全的。 - caf
1
最好使用poll而不是select。现在新软件中不应再使用“select”,因为当您增加每个进程的打开文件描述符数量时,它会出现问题。此外,建议直接在套接字调用中使用SOCK_NONBLOCK(也许还要使用SOCK_CLOEXEC)。 - Lothar
1
@deltamind106 可能有些变化,或者你在不同的情况下尝试过,但是在基本情况下,如果由于超时而导致select返回0,因为远程端点不存在,则上面的代码中的so_error会被设置为10061,也就是WSAECONNREFUSED,即“无法建立连接,因为目标计算机积极拒绝了它。”换句话说:在Windows(10)上对我来说很好用。 - stijn
显示剩余3条评论

56

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

连接超时(或 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 
}

1
这是一个非常好的答案,为什么你把它设为社区维基?你应该因为提供了这个资源而获得一些声望。 - Tim Post
3
链接的论坛似乎更换了他们的软件,所以现在该链接失效了。 - Jorenko
2
注意:尽管此答案被认为是正确的,但caf下面的解决方案不仅解释了策略,还提供了可行的代码。 - ecume des jours
3
这个解决方案(以及大多数其他解决方案)存在一个缺陷,即当 signal/poll 受到 EINTR 中断时,它会重新将超时设置为0,即使已经经过了一些时间。请参考我的解决方案,它对此进行了处理。 - Jay Sullivan
2
这里使用“do while”循环的意义是什么?看起来这个循环只可能运行一次。 - josh798
显示剩余3条评论

31

这里是一个现代的 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;
}

3
+1000。我必须说这是目前StackOverflow上最被低估的答案! - kanso
1
+1 这真是太棒了。简单、极其有效、完全自包含、有文档说明,而且避免使用 select 语句。 - Alex Baum
在其他答案中,这是我选择的一个。非阻塞+轮询。 - philippe lhardy
3
这很棒,谢谢。我还在想为什么没有一个“connect”函数可以让我提供超时值作为参数。这似乎是一个常见的需求。目前看来,如果我想要除默认超时时间以外的其他超时时间,我必须用大量的代码替换我的一个“connect”调用。 - lurker
1
不可移植,无法在Windows上运行 - p0358
显示剩余3条评论

30
使用select()/poll()的答案是正确的,代码应该按照这种方式编写以实现可移植性。
然而,由于你在使用Linux,你可以这样做:
#include <netinet/tcp.h>
//...

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()来设置超时时间是不可行的。
在win10 22H2 Ubuntu 20.04.6 LTS上通过tcpdump观察到的经验值 Linux 23DKMN3 5.15.90.1-microsoft-standard-WSL2 #1 SMP Fri Jan 27 02:56:13 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
重试次数 wsl2超时时间(秒) 本机Ubuntu20.04超时时间
0 1.049923 1.011340
1 3.129942 3.027308
2 7.209805 7.123300
3 15.859880 15.315300
4 32.489877 31.443295
5 65.139792 63.699309
6 130.43962 129.235296

3
不错!简单实用!包含添加:#include <netinet/tcp.h> - Johannes Overmann
synRetries = 1 导致约3秒的超时(经验性),而 synRetries = 0 不幸地导致约127秒的超时(不像预期中的约1秒)。synRetries = 2 约7秒的超时已得到确认。 - Johannes Overmann
@JohannesOvermann,请再确认一下你的测试结果好吗?我更新了答案,并附上了我的测试结果,但似乎与你的有些不同(你的结果差了一个)。当 retries = 1 时,结果约为1秒,而 retries = 2 时,结果约为3秒。这是通过 tcpdump 计时的。 - undefined
1
我从来没有使用过win10的Linux子系统。所有的TCP堆栈和操作系统的行为都不同。这完全不可移植。所以这些数字永远不会一致。 - undefined

10
在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形式。 - Adrian May

5
这个函数可以参数化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;
}

5
两个套接字选项SO_RCVTIMEOSO_SNDTIMEOconnect没有影响。以下是包含此说明的屏幕截图链接,这里我只做简要介绍。使用signalselect或poll是实现connect超时的合适方法。

信号

通过使用系统调用(wrapper)alarm,可以通过自动生成的信号SIGALRM中断connect。但是,必须为相同的信号安装信号处理程序,否则程序将被终止。代码如下:...
#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 还是 poll

已经有一些用户提供了如何使用 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


1
答案错误地指出SNDTIMEO不适用于“connect”。这是不正确的 - “man 7 socket”页面清楚地说明使用SO_SNDTIMEO将导致连接的EINPROGRESS。 - dash-o

4
除了编译错误外,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(...)

我对于发送超时和非阻塞套接字之间的权衡不太熟悉,但我很想了解。


2
如果您计划在阻塞模式下使用套接字,那么通过实现SO_SNDTIMEO解决方案而不是将套接字置于非阻塞模式、连接然后再放回阻塞模式,您将进行更少的系统调用。看起来会快一点。 - Nahuel Greco

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接