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的流程是这样的:
- 设置socket为非阻塞模式
- 调用
connect(),函数立即返回-1 - 检查
errno,如果是EINPROGRESS,说明连接正在进行 - 用
select()或poll()等待连接完成 - 检查连接是否成功
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设为EAGAIN或EWOULDBLOCK。
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的核心知识点,我画出来方便大家理解:
六、实战中的注意事项
最后,我总结几个实战中容易踩的坑:
| 问题 | 原因 | 解决方法 |
|---|---|---|
| connect返回后立即读写失败 | 连接尚未完全建立 | 用getsockopt检查SO_ERROR |
| accept返回-1误判为错误 | 忽略了EAGAIN | 检查errno,区分无连接和真错误 |
| fcntl设置后其他标志丢失 | 直接覆盖了原有标志 | 先F_GETFL再F_SETFL |
| select超时后未处理 | 超时返回0,但没做处理 | 超时后关闭socket或重试 |
我的建议:刚开始用非阻塞socket时,可以先写一个简单的echo服务器练手。把阻塞版本改成非阻塞版本,对比两者的行为差异。这样理解会更深刻。
好了,非阻塞socket的内容就讲到这里。记住核心思想:不让程序死等,让CPU去做更有价值的事情。下一节我们会讲IO多路复用,那是非阻塞socket的最佳搭档。
公众号:蓝海资料掘金营,微信deep3321