socket编程中,当客户端用connect()去连接一个服务端是,如果服务器或网络繁忙,或连接了一个未开机的主机时,客户端发送的SYN包得不到响应,connect函数会很久不返回(阻塞模式下)。具体的超时时间与系统相关,有的可能设置为75秒,有的可能设置为120秒等等。在有的应用中,阻塞太久不是程序想要的行为?
connect超时时间与/proc/sys/net/ipv4/tcp_syn_retries的值有关,这个值决定了重发SYN包的次数,另一方面重发SYN包的间隔时间系列是:1, 2, 4, 8, 16, 32, 64, 120, 120... ...这个时间系列是固定的,所以tcp_syn_retries的值越大,connect超时时间就越长。这是一个系统设置,我们通过改这个值来缩短connect的超时时间显然不太合理。(可以搜索sysctl查看如何修改)
下面我们看看如何用其它方式来修改connect超时时间
方法一:
步骤
- 建立socket
- 把socket设置为非阻塞模式
- 调用connect
- 使用select检查该socket描述符是否可写(注意,是可写)
- 根据select返回结果判断connect结果
- 将socket恢复为阻塞模式
代码示例
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int connect_timeout(int sockfd, struct sockaddr *serv_addr, int addrlen, int timeout);
int open_clientfd(const char *server, int port)
{
struct sockaddr_in serverAddr;
struct hostent *hostp;
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd < 0)
{
perror("socket: ");
return -1;
}
if ( (hostp = gethostbyname(server)) == NULL)
{
perror("gethostbyname: ");
close(clientfd);
return -1;
}
bzero((char *)&serverAddr, sizeof(serverAddr));
memcpy((void *)&(serverAddr.sin_addr), (void *)hostp->h_addr_list[0],hostp->h_length);
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = ntohs(port);
int n = connect_timeout(clientfd, (sockaddr *)&serverAddr, sizeof(serverAddr), 10000);
if (n<0)
{
printf("error\n");
close(clientfd);
return -1;
}
else if (n==0)
{
printf("time out\n");
close(clientfd);
return -1;
}
else
printf("connect sucessed\n");
return clientfd;
}
/*
超时版的connect,timeout单位是毫秒
*/
int connect_timeout(int sockfd, struct sockaddr *serv_addr, int addrlen, int timeout)
{
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
int n = connect(sockfd, serv_addr, sizeof(*serv_addr));
if(n < 0)
{
/*
EINPROGRESS表示connect正在尝试连接
#define EWOULDBLOCK EAGAIN Operation would block
*/
if(errno != EINPROGRESS && errno != EWOULDBLOCK)
return -1;
struct timeval tv;
tv.tv_sec = timeout/1000;
tv.tv_usec = (timeout - tv.tv_sec*1000)*1000;
fd_set wset;
FD_ZERO(&wset);
FD_SET(sockfd, &wset);
n = select(sockfd+1, NULL, &wset, NULL, &tv);
if(n < 0)
{
// select出错
return -1;
}
else if (0 == n)
{
// 超时
return 0;
}
}
fcntl(fd,F_SETFL,flags & ~O_NONBLOCK); // 恢复为阻塞模式
return 1;
}
int main(int argc, char **argv)
{
int sk = open_clientfd(argv[1], atoi(argv[2]));
printf("socket: %d\n", sk);
if (sk>0)
close(sk);
return 0;
}
方法二(转):
linux内核中,connect的实现用到的超时参数是sndtimeo
net/ipv4/af_inet.c
timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
tcp_sk(sk)->fastopen_req &&
tcp_sk(sk)->fastopen_req->data ? 1 : 0;
/* Error code is set above */
if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
goto out;
err = sock_intr_errno(timeo);
if (signal_pending(current))
goto out;
}
这意味着:在Linux平台下,可以通过在connect之前设置SO_SNDTIMO来达到控制连接超时的目的。
示例代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <errno.h>
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_in addr;
struct timeval timeo = {3, 0};
socklen_t len = sizeof(timeo);
fd = socket(AF_INET, SOCK_STREAM, 0);
if (argc == 4)
timeo.tv_sec = atoi(argv[3]);
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeo, len);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
if (errno == EINPROGRESS) {
fprintf(stderr, "timeout\n");
return -1;
}
perror("connect");
return 0;
}
printf("connected\n");
return 0;
}