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 实现自定义超时

具体步骤:

  1. 创建套接字后,用 fcntl() 设置为非阻塞模式。
  2. 调用 connect(),此时它会立即返回 -1,errno 设置为 EINPROGRESS(表示连接正在进行)。
  3. 用 select() 或 poll() 监听套接字是否可写(连接完成)。
  4. 设置超时时间,比如 3 秒。
  5. 超时后如果还没连接成功,就关闭套接字,返回失败。

来看代码:

#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() 知识体系,方便大家记忆:

connect() 核心 基本用法 socket() → connect() → 收发数据 阻塞模式 默认模式,超时 75 秒 非阻塞模式 fcntl() + select() + getsockopt() 超时处理 select() 设置超时时间 错误检查 getsockopt() 检查 SO_ERROR 常见陷阱 EINPROGRESS / EINTR / 重复 connect

7.6 实战建议

最后,我给大家几个实战中的建议:

  • 永远不要用默认的阻塞 connect()——除非你写的是玩具程序。
  • 超时时间根据场景调整:内网服务 2-3 秒,公网服务 5-8 秒。
  • 连接失败后记得 close() 旧套接字,重新 socket() 再 connect()。
  • 考虑使用连接池:频繁创建和销毁连接开销很大,尤其是 HTTPS 场景。

一句话总结:connect() 看似简单,但阻塞超时、非阻塞处理、错误检查这三个环节,任何一个出问题都会让你的网络程序变得不可靠。我见过太多线上事故,根源就是 connect() 没处理好。

好了,这一讲就到这里。connect() 搞定了,下一讲我们聊聊数据收发——send() 和 recv() 的那些坑。到时候见。


公众号:蓝海资料掘金营,微信 deep3321