在使用 connect() 函数建立 TCP 连接时,默认情况下,如果服务器未响应,connect() 会一直阻塞,直到连接建立成功或发生错误。这可能导致客户端长时间等待,不利于程序的健壮性。为了解决这个问题,您可以通过以下方法对 connect() 进行超时处理。
方法一:使用非阻塞模式结合 select() 实现超时处理(推荐)
步骤:
- 将套接字设置为非阻塞模式:
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
- 调用 connect():
在非阻塞模式下,connect() 会立即返回。如果连接正在进行,connect() 会返回 -1,并设置 errno 为 EINPROGRESS。
int ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (ret < 0 && errno != EINPROGRESS) {
perror("connect");
close(sockfd);
return -1;
}
- 使用 select() 监控套接字的可写性:
当套接字变为可写时,表示连接已建立或失败。使用 select() 可以设定超时时间。
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(sockfd, &writefds);
struct timeval timeout;
timeout.tv_sec = 5; // 设置超时时间为 5 秒
timeout.tv_usec = 0;
int ret = select(sockfd + 1, NULL, &writefds, NULL, &timeout);
if (ret == 0) {
// 超时处理
printf("Connect timed out\n");
close(sockfd);
return -1;
} else if (ret < 0) {
// 错误处理
perror("select");
close(sockfd);
return -1;
}
- 检查连接是否成功:
使用 getsockopt() 获取套接字错误状态,判断连接是否成功。
int err = 0;
socklen_t len = sizeof(err);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {
perror("getsockopt");
close(sockfd);
return -1;
}
if (err != 0) {
// 连接失败,处理错误
printf("Connection failed: %s\n", strerror(err));
close(sockfd);
return -1;
}
- 恢复套接字为阻塞模式(可选):
如果后续操作需要阻塞模式,可以将套接字设置回阻塞模式。
flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags & ~O_NONBLOCK);
完整示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int connect_with_timeout(int sockfd, const struct sockaddr *addr, socklen_t addrlen, int timeout_sec) {
int flags, ret, err;
socklen_t len;
fd_set writefds;
struct timeval timeout;
// 获取当前套接字状态标志
if ((flags = fcntl(sockfd, F_GETFL, 0)) < 0) {
perror("fcntl F_GETFL");
return -1;
}
// 设置非阻塞模式
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0) {
perror("fcntl F_SETFL");
return -1;
}
// 发起非阻塞连接
ret = connect(sockfd, addr, addrlen);
if (ret < 0 && errno != EINPROGRESS) {
perror("connect");
return -1;
}
// 使用 select 监控套接字可写事件
if (ret != 0) {
FD_ZERO(&writefds);
FD_SET(sockfd, &writefds);
timeout.tv_sec = timeout_sec;
timeout.tv_usec = 0;
ret = select(sockfd + 1, NULL, &writefds, NULL, &timeout);
if (ret == 0) {
// 超时处理
printf("Connect timed out\n");
return -1;
} else if (ret < 0) {
perror("select");
return -1;
} else if (FD_ISSET(sockfd, &writefds)) {
// 检查连接是否成功
len = sizeof(err);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {
perror("getsockopt");
return -1;
}
if (err != 0) {
printf("Connection failed: %s\n", strerror(err));
return -1;
}
} else {
printf("Unexpected result in select\n");
return -1;
}
}
// 恢复套接字为原始模式
if (fcntl(sockfd, F_SETFL, flags) < 0) {
perror("fcntl F_SETFL restore");
return -1;
}
return 0; // 连接成功
}
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 创建套接字
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return -1;
}
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
// 替换为服务器的 IP 地址
if (inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr) <= 0) {
perror("inet_pton");
close(sockfd);
return -1;
}
// 尝试连接,超时时间为 5 秒
if (connect_with_timeout(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr), 5) < 0) {
printf("Failed to connect to server\n");
close(sockfd);
return -1;
}
printf("Connected to server successfully\n");
// 后续操作...
close(sockfd);
return 0;
}
方法二:使用 SO_SNDTIMEO 套接字选项(不推荐)
可以通过 setsockopt() 设置 SO_SNDTIMEO 选项,为 connect() 设置超时时间。但是需要注意,这种方法在不同操作系统上的行为可能不一致,特别是在 connect() 阻塞时,SO_SNDTIMEO 不一定会起作用。
struct timeval timeout;
timeout.tv_sec = 5; // 设置超时时间为 5 秒
timeout.tv_usec = 0;
if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) {
perror("setsockopt SO_SNDTIMEO");
}
由于兼容性和可靠性的问题,不推荐使用这种方法。
方法三:使用 alarm() 或多线程(不推荐)
使用 alarm():
通过 alarm() 设置一个定时器,在指定时间后发送 SIGALRM 信号。需要在 connect() 前设置信号处理函数。
#include <signal.h>
void sigalrm_handler(int signum) {
// 处理超时
}
signal(SIGALRM, sigalrm_handler);
alarm(5); // 设置 5 秒超时
connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
alarm(0); // 取消定时器
注意:
- 使用 alarm() 可能会影响程序中其他使用定时器和信号的部分。
- 需要处理信号的可重入性,增加了编程复杂度。
使用多线程:
在单独的线程中执行 connect(),主线程等待一定时间后检查连接是否成功。
注意:
- 增加了程序的复杂度。
- 需要注意线程同步和资源管理。
总结
- 推荐方法: 使用非阻塞模式结合 select() 进行超时处理,这是最可靠、最通用的方法。
- 不推荐方法: 使用 SO_SNDTIMEO 或 alarm(),因为它们在不同平台上的行为可能不一致,或者会引入其他复杂性。
通过正确地处理 connect() 的超时,您可以提高网络程序的健壮性,避免因连接问题导致的长时间阻塞,提升用户体验。