7. 发起连接:connect() 函数详解、连接超时处理、非阻塞 connect
大家好,我是你们的老朋友。今天我们来聊聊网络编程里一个绕不开的话题——connect()。
说实话,我刚开始写网络程序那会儿,觉得 connect() 不就是把套接字连到服务器上嘛,几行代码搞定。直到有一次,线上服务突然卡死,排查了半天才发现是 connect() 阻塞住了,整整等了 75 秒才超时返回。嗯,从那以后,我再也不敢小看这个函数了。
今天这一讲,我会把 connect() 的方方面面掰开揉碎了讲清楚。包括它的基本用法、阻塞与非阻塞模式、超时处理,以及一些实战中的坑。你想想看,如果连连接都建立不好,后面的数据收发还怎么玩?
7.1 connect() 函数的基本用法
先看原型:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数很简单:
- sockfd:你之前创建好的套接字描述符。
- addr:服务器地址结构体指针,里面填 IP 和端口。
- addrlen:地址结构体的长度。
返回值:成功返回 0,失败返回 -1 并设置 errno。
一个典型的 TCP 客户端调用 connect() 的流程是这样的:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sockfd);
exit(1);
}
// 连接成功,开始收发数据...
看着挺简单对吧?但这里有个关键点——默认情况下,connect() 是阻塞的。什么意思?就是如果服务器没响应,你的程序就会卡在 connect() 这一行,直到内核超时或者收到响应。
核心要点:默认的 connect() 超时时间由内核决定,通常是 75 秒左右。这在生产环境中是不可接受的。
7.2 阻塞 connect 的超时问题
我在项目中遇到过这样的情况:客户端连接一个内网服务,网络偶尔抖动一下,connect() 就卡住了。用户那边直接反馈“软件卡死了”。
为什么会这样?因为默认的 connect() 超时时间太长了。Linux 内核的 TCP 连接超时重传策略是这样的:
| 重传次数 | 超时时间 |
|---|---|
| 第 1 次 | 3 秒 |
| 第 2 次 | 6 秒 |
| 第 3 次 | 12 秒 |
| 第 4 次 | 24 秒 |
| 第 5 次 | 48 秒 |
| 总时间 | 约 93 秒(实际 75 秒左右) |
说白了,如果你不做任何处理,一个 connect() 最多能等 75 秒。这在大多数应用场景下都是不可接受的。
7.3 非阻塞 connect 与超时控制
那怎么办?我的做法是:使用非阻塞 connect() + select/poll 实现自定义超时。
具体步骤:
- 创建套接字后,用
fcntl()设置为非阻塞模式。 - 调用 connect(),此时它会立即返回 -1,errno 设置为
EINPROGRESS(表示连接正在进行)。 - 用 select() 或 poll() 监听套接字是否可写(连接完成)。
- 设置超时时间,比如 3 秒。
- 超时后如果还没连接成功,就关闭套接字,返回失败。
来看代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int connect_with_timeout(int sockfd, const struct sockaddr *addr,
socklen_t addrlen, int timeout_sec) {
// 1. 设置为非阻塞
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
// 2. 发起连接
int ret = connect(sockfd, addr, addrlen);
if (ret == 0) {
// 运气好,立即连接成功(比如本机回环)
fcntl(sockfd, F_SETFL, flags); // 恢复原模式
return 0;
}
if (errno != EINPROGRESS) {
// 真正的错误,比如网络不可达
return -1;
}
// 3. 用 select 等待连接完成
fd_set wset;
struct timeval tv;
FD_ZERO(&wset);
FD_SET(sockfd, &wset);
tv.tv_sec = timeout_sec;
tv.tv_usec = 0;
ret = select(sockfd + 1, NULL, &wset, NULL, &tv);
if (ret <= 0) {
// 超时或出错
return -1;
}
// 4. 检查连接是否真的成功
int error = 0;
socklen_t len = sizeof(error);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) {
return -1;
}
if (error != 0) {
errno = error;
return -1;
}
// 5. 恢复阻塞模式(可选)
fcntl(sockfd, F_SETFL, flags);
return 0;
}
个人经验:我习惯把超时时间设为 3-5 秒。太短了容易误判(网络偶尔抖动),太长了用户体验不好。另外,记得在连接成功后恢复套接字的阻塞模式,否则后续的 read()/write() 也会变成非阻塞,逻辑就乱了。
7.4 非阻塞 connect 的常见陷阱
这里有几个坑,我曾经都踩过,分享给大家:
- 陷阱一:select 返回可写不代表连接成功
select 说套接字可写,只是说明连接过程完成了,但可能是失败的。必须用 getsockopt() 检查 SO_ERROR。 - 陷阱二:忽略 EINTR 信号
connect() 被信号中断时会返回 EINTR。非阻塞模式下虽然少见,但严谨的代码还是要处理。 - 陷阱三:忘记恢复文件描述符标志
如果你后续的读写逻辑依赖阻塞模式,一定要在连接成功后恢复。 - 陷阱四:对同一个套接字重复 connect()
一个 TCP 套接字只能 connect() 一次。如果想重连,必须新建套接字。
警告:千万不要在非阻塞 connect() 之后直接调用 write() 发送数据。即使 select 返回可写,也要先检查连接状态。我曾经因为没检查 SO_ERROR,结果 write() 返回成功,但数据根本没发出去——因为连接其实失败了。
7.5 知识体系总览
下面这张图是我自己整理的 connect() 知识体系,方便大家记忆:
7.6 实战建议
最后,我给大家几个实战中的建议:
- 永远不要用默认的阻塞 connect()——除非你写的是玩具程序。
- 超时时间根据场景调整:内网服务 2-3 秒,公网服务 5-8 秒。
- 连接失败后记得 close() 旧套接字,重新 socket() 再 connect()。
- 考虑使用连接池:频繁创建和销毁连接开销很大,尤其是 HTTPS 场景。
一句话总结:connect() 看似简单,但阻塞超时、非阻塞处理、错误检查这三个环节,任何一个出问题都会让你的网络程序变得不可靠。我见过太多线上事故,根源就是 connect() 没处理好。
好了,这一讲就到这里。connect() 搞定了,下一讲我们聊聊数据收发——send() 和 recv() 的那些坑。到时候见。