46、SO_LINGER选项:l_onoff/l_linger设置、强制关闭与优雅关闭
做网络编程这么多年,关闭连接这件事,看起来简单,坑却不少。
我记得刚入行那会儿,写一个高并发服务器。压测时发现,客户端断开后,服务端偶尔会丢数据。查了两天,最后发现是 close() 的行为没搞明白。嗯,今天我们就聊聊 SO_LINGER 这个选项。
SO_LINGER 到底是什么?
说白了,它控制的是 close() 或 shutdown() 之后,内核怎么处理还没发完的数据。
默认情况下,你调用 close(),内核会立即返回。但内核会在后台继续尝试把发送缓冲区里的数据发完。这叫优雅关闭。
但有些场景下,你不想等。你想立刻断掉。这时候就需要 SO_LINGER 了。
结构体长什么样?
struct linger {
int l_onoff; // 0: 关闭本选项 非0: 开启
int l_linger; // 等待时间,单位秒
};
就这么两个字段。但组合起来,有三种完全不同的行为。
三种模式,一张表说清楚
| l_onoff | l_linger | 行为描述 |
|---|---|---|
| 0 | 忽略 | 默认模式。close() 立即返回,内核后台发完数据再关闭 |
| 非0 | 0 | 强制关闭。close() 立即返回,丢弃缓冲区数据,发送 RST |
| 非0 | >0 | 带超时的优雅关闭。close() 阻塞,直到数据发完或超时 |
模式一:默认模式(l_onoff = 0)
这是最常用的。你调用 close(),函数立刻返回。内核在后台默默地把数据发完,然后完成四次挥手。
好处是不阻塞你的程序。坏处是——你没法知道数据到底发完没有。
我在项目中遇到过一个问题:客户端调了 close() 就退出进程了。服务端那边还在等最后一条数据。结果数据丢了,因为进程退出时,内核直接回收了资源。嗯,这里要注意:进程退出不等于数据发完。
模式二:强制关闭(l_onoff = 1, l_linger = 0)
这个模式很暴力。调用 close() 后,内核立刻丢弃发送缓冲区里所有数据。然后发送一个 RST 报文 给对端。
对端收到 RST 会怎样?read() 返回 -1,errno 是 ECONNRESET。说白了就是连接被强制重置了。
什么时候用这个?
- 你确定不需要再发数据了
- 你想快速释放资源
- 你不在乎对端能不能收到最后的数据
我曾经写一个心跳检测模块,发现某个连接已经死了,就直接强制关闭。省得等超时。
模式三:带超时的优雅关闭(l_onoff = 1, l_linger > 0)
这个模式比较折中。你设置一个超时时间,比如 5 秒。
调用 close() 后,函数会阻塞。内核在这段时间内尽力发完数据。如果数据发完了,正常关闭。如果超时了,强制关闭。
返回值也能告诉你结果:
- 返回 0:数据发完了,正常关闭
- 返回 -1,errno = EWOULDBLOCK:超时了,数据没发完
这个模式有个大坑:阻塞。如果你的程序是事件驱动的,一个 close() 卡住 5 秒,整个事件循环就废了。
我个人习惯:非阻塞 socket 下,不要用这个模式。你想想看,一个非阻塞 socket 调了阻塞的 close(),多别扭。
代码示例:设置 SO_LINGER
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
int set_linger(int fd, int onoff, int linger_sec) {
struct linger l;
l.l_onoff = onoff;
l.l_linger = linger_sec;
if (setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) < 0) {
perror("setsockopt SO_LINGER");
return -1;
}
return 0;
}
int main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
// 设置强制关闭
set_linger(sock, 1, 0);
// ... 连接、通信 ...
close(sock);
return 0;
}
什么时候选哪种?
- 默认模式:绝大多数场景。简单、高效、不阻塞。
- 强制关闭:你确定连接已经异常了,或者你不在乎数据完整性。
- 带超时的优雅关闭:你非常在意数据完整性,且能接受阻塞。比如文件传输的最后一步。
核心逻辑流程图
避坑指南
我曾经在一个项目里,用 SO_LINGER 设置超时 10 秒。结果压测时,大量连接卡在 close() 上。线程池被打满,服务雪崩。
后来我改成默认模式 + shutdown(SHUT_WR) 的方式。问题解决了。
所以我的建议是:
- 别在非阻塞 socket 上用带超时的
SO_LINGER - 别在生产环境随便设
l_linger超过 2 秒 - 如果你不确定,就用默认模式
好了,关于 SO_LINGER 就聊这么多。记住一句话:关闭连接,比打开连接更需要小心。
公众号:蓝海资料掘金营,微信deep3321