15、非阻塞socket:fcntl()设置非阻塞模式,非阻塞connect()与accept()

各位同学,今天我们来聊聊非阻塞socket。说实话,这个话题在实战中特别重要。我记得刚入行那会儿,写了一个阻塞式的网络服务,结果一个客户端卡住了,整个服务都跟着瘫痪。从那以后,我对非阻塞模式就格外上心。

一、什么是非阻塞模式?

先说说阻塞模式。你想想看,默认情况下,socket是阻塞的。比如你调用connect(),程序就会停在那里,直到连接建立成功或失败。这期间,你的程序啥也干不了。

非阻塞模式就不一样了。调用connect()后,函数会立刻返回,告诉你「正在连接中」。你可以去做别的事情,过一会儿再来检查连接状态。说白了,就是不让程序死等。

核心区别:

  • 阻塞模式:函数不返回,直到操作完成
  • 非阻塞模式:函数立即返回,操作可能还没完成

二、用fcntl()设置非阻塞模式

设置非阻塞模式,最常用的方法就是fcntl()。我个人习惯用这个函数,因为它简单直接。

// 设置非阻塞模式
int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        return -1;
    }
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

这段代码做了两件事:先获取当前的文件状态标志,然后加上O_NONBLOCK标志。嗯,这里要注意,一定要先获取再设置,不要直接覆盖。

我的经验:有些新手直接写fcntl(fd, F_SETFL, O_NONBLOCK),这样会把其他标志清掉。我曾经因为这个bug排查了半天,后来才发现是标志被覆盖了。

三、非阻塞connect()

非阻塞connect()是实战中的重头戏。为什么要用非阻塞connect?我举个例子:你的程序需要同时连接多个服务器,如果用阻塞方式,就得一个一个连,太慢了。

非阻塞connect的流程是这样的:

  1. 设置socket为非阻塞模式
  2. 调用connect(),函数立即返回-1
  3. 检查errno,如果是EINPROGRESS,说明连接正在进行
  4. select()poll()等待连接完成
  5. 检查连接是否成功
int nonblocking_connect(const char *ip, int port) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    set_nonblocking(sock);

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET, ip, &addr.sin_addr);

    int ret = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
    if (ret == -1 && errno != EINPROGRESS) {
        close(sock);
        return -1;
    }

    // 用select等待连接完成
    fd_set wset;
    FD_ZERO(&wset);
    FD_SET(sock, &wset);

    struct timeval tv = {5, 0};  // 5秒超时
    ret = select(sock + 1, NULL, &wset, NULL, &tv);
    if (ret <= 0) {
        close(sock);
        return -1;
    }

    // 检查连接是否成功
    int error = 0;
    socklen_t len = sizeof(error);
    getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len);
    if (error != 0) {
        close(sock);
        return -1;
    }

    return sock;
}

避坑指南:我曾经遇到过一个坑——非阻塞connect返回后,直接用write()写数据,结果失败了。原因是连接还没真正建立。一定要用getsockopt()检查SO_ERROR,确认连接状态。

四、非阻塞accept()

非阻塞accept()相对简单一些。服务器端设置监听socket为非阻塞,然后每次调用accept(),如果没有新连接,函数会立即返回-1,errno设为EAGAINEWOULDBLOCK

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
set_nonblocking(server_fd);

// 绑定和监听...
bind(server_fd, ...);
listen(server_fd, 5);

// 主循环
while (1) {
    struct sockaddr_in client_addr;
    socklen_t addr_len = sizeof(client_addr);

    int client_fd = accept(server_fd, 
                           (struct sockaddr*)&client_addr, 
                           &addr_len);

    if (client_fd == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            // 没有新连接,继续做其他事情
            do_other_work();
            continue;
        }
        // 真正的错误
        perror("accept");
        break;
    }

    // 处理新连接
    handle_client(client_fd);
}

你看,非阻塞accept的好处是:服务器可以在没有新连接时,去做其他事情。比如处理已连接客户端的请求,或者执行定时任务。

五、知识体系图

下面这张图总结了非阻塞socket的核心知识点,我画出来方便大家理解:

非阻塞Socket核心知识体系 非阻塞Socket 设置方法:fcntl() + O_NONBLOCK 非阻塞connect() 返回EINPROGRESS select()等待 + getsockopt()检查 非阻塞accept() 返回EAGAIN / EWOULDBLOCK 无连接时做其他工作

六、实战中的注意事项

最后,我总结几个实战中容易踩的坑:

问题 原因 解决方法
connect返回后立即读写失败 连接尚未完全建立 用getsockopt检查SO_ERROR
accept返回-1误判为错误 忽略了EAGAIN 检查errno,区分无连接和真错误
fcntl设置后其他标志丢失 直接覆盖了原有标志 先F_GETFL再F_SETFL
select超时后未处理 超时返回0,但没做处理 超时后关闭socket或重试

我的建议:刚开始用非阻塞socket时,可以先写一个简单的echo服务器练手。把阻塞版本改成非阻塞版本,对比两者的行为差异。这样理解会更深刻。

好了,非阻塞socket的内容就讲到这里。记住核心思想:不让程序死等,让CPU去做更有价值的事情。下一节我们会讲IO多路复用,那是非阻塞socket的最佳搭档。


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