10、关闭连接:close()与shutdown()区别、优雅关闭、SO_LINGER选项
网络编程里,连接关闭这件事,看着简单,坑却不少。
我刚入行那会儿,觉得关闭连接不就是调个close()嘛,有啥好研究的?直到有一次线上服务出现大量 TIME_WAIT 连接,把端口池给耗尽了,我才意识到——嗯,这里面的门道比我想象的多得多。
10.1 close() 与 shutdown() 的根本区别
先说说最基础的。close() 和 shutdown() 都能用来关闭连接,但它们的语义完全不同。
close():引用计数减一
close() 的本质,是把 socket 的引用计数减 1。只有当引用计数降到 0 时,内核才会真正发起 TCP 的四次挥手。
什么意思呢?如果你 fork 了一个子进程,父子进程都持有同一个 socket 描述符,那么父进程调 close() 只是把引用计数从 2 变成 1,连接并不会关闭。只有子进程也调 close(),连接才会断开。
我在项目中遇到过一个问题:父进程关闭了 socket,但子进程还在用,结果数据莫名其妙就丢了。排查了半天,才发现是引用计数搞的鬼。
// close() 示例:引用计数机制
int sock = socket(AF_INET, SOCK_STREAM, 0);
// ... connect 或 accept ...
pid_t pid = fork();
if (pid == 0) {
// 子进程:读写数据
// ...
close(sock); // 引用计数从2减到1,连接未关闭
exit(0);
} else {
// 父进程
close(sock); // 引用计数从1减到0,真正关闭连接
wait(NULL);
}
shutdown():直接切断通道
shutdown() 就不一样了。它不管引用计数是多少,直接对连接进行操作。你可以选择关闭读方向、写方向,或者两个方向都关。
// shutdown() 的三种模式
int shutdown(int sockfd, int how);
// how 参数:
// SHUT_RD (0) — 关闭读方向
// SHUT_WR (1) — 关闭写方向
// SHUT_RDWR (2) — 关闭读写方向
我个人习惯用 shutdown(sock, SHUT_WR) 来实现半关闭。发送完数据后,告诉对端「我写完了,但你还可以继续发数据给我」。这在 HTTP 协议里很常见——客户端发完请求后关闭写方向,但还能接收服务端的响应。
核心区别总结:
close()操作的是描述符,受引用计数影响shutdown()操作的是连接本身,不受引用计数影响close()会释放描述符资源,shutdown()不会shutdown()支持半关闭,close()不支持
10.2 优雅关闭:别让数据死在路上
什么叫优雅关闭?说白了就是:确保发送缓冲区里的数据都发出去了,再关闭连接。
你想想看,如果你调了 close(),但内核发送缓冲区里还有 2KB 数据没发完,内核会尝试把这些数据发出去。但如果对端已经关闭了读方向,这些数据就会被丢弃,然后内核给你返回一个 RST 报文。
我曾经在做一个文件传输工具时,就踩过这个坑。大文件传到最后几 KB 总是丢失,查了三天才发现是关闭时机不对。
优雅关闭的标准流程
- 调用
shutdown(sock, SHUT_WR),告诉对端「我发完了」 - 继续调用
read()读取对端的剩余数据,直到返回 0 - 对端也关闭后,再调
close()释放资源
// 优雅关闭示例
// 假设我们已经发送完所有数据
// 步骤1:关闭写方向,发送 FIN
if (shutdown(sock, SHUT_WR) == -1) {
perror("shutdown");
// 处理错误
}
// 步骤2:等待对端关闭
char buf[1024];
int n;
while ((n = read(sock, buf, sizeof(buf))) > 0) {
// 处理对端可能发来的剩余数据
// 实际上,如果协议设计合理,这里应该读不到数据
}
// 步骤3:关闭 socket
close(sock);
小技巧: 如果你用的是非阻塞 I/O,优雅关闭的逻辑会更复杂一些。你需要用 poll() 或 epoll() 来监听 POLLIN 事件,直到 read() 返回 0。
10.3 SO_LINGER 选项:控制 close() 的行为
SO_LINGER 这个选项,说白了就是让你决定:调 close() 时,如果还有数据没发完,该怎么办?
它通过一个结构体来配置:
struct linger {
int l_onoff; // 0: 关闭 linger 功能;非0: 开启
int l_linger; // 等待时间,单位秒
};
三种配置模式
| l_onoff | l_linger | close() 的行为 |
|---|---|---|
| 0 | 忽略 | 默认行为:立即返回,内核后台尝试发送剩余数据 |
| 非0 | 0 | 强制关闭:立即发送 RST,丢弃所有未发送数据 |
| 非0 | >0 | 优雅关闭:阻塞等待指定秒数,让内核发送剩余数据 |
模式一:l_onoff = 0(默认)
这是最常用的模式。close() 立即返回,内核在后台继续发送缓冲区里的数据。如果数据发完了,正常四次挥手;如果发不完,对端可能会收到 RST。
我个人建议,大多数场景用这个模式就够了。它不会阻塞你的程序,内核会帮你处理好后续工作。
模式二:l_onoff = 1, l_linger = 0(强制关闭)
这个模式很暴力。调 close() 后,内核立即发送 RST 报文,丢弃所有未发送的数据。连接直接断开,不走四次挥手。
什么时候用?我记得有一次,我在做一个心跳检测模块。发现某个对端已经挂了,就没必要再优雅关闭了,直接发 RST 完事。
警告: 强制关闭会导致对端收到 ECONNRESET 错误。如果你的对端还在读数据,它会收到一个「连接被重置」的错误。这不是什么友好的行为,慎用。
模式三:l_onoff = 1, l_linger > 0(带超时的优雅关闭)
这个模式会让 close() 阻塞,直到数据发送完毕或超时。如果超时了,内核会强制关闭连接。
// 设置 SO_LINGER 示例
struct linger ling;
ling.l_onoff = 1;
ling.l_linger = 10; // 最多等10秒
if (setsockopt(sock, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling)) == -1) {
perror("setsockopt");
}
// 此时 close() 会阻塞最多10秒
close(sock);
这个模式有个坑:如果 close() 阻塞了,你的程序就卡住了。我见过有人把 l_linger 设成 30 秒,结果程序在关闭连接时卡了半分钟,用户体验极差。
我的建议:
- 默认用
l_onoff = 0,让内核异步处理 - 需要确保数据送达时,用
shutdown()+ 应用层确认,而不是依赖SO_LINGER - 强制关闭(
l_linger = 0)只在确定对端已死时使用 - 不要在生产环境用带超时的
SO_LINGER,除非你很清楚自己在做什么
10.4 知识体系图
下面这张图,把本章的核心知识点串起来了:
10.5 避坑指南
最后,分享几个我踩过的坑:
- 不要对同一个 socket 调两次 close(): 第二次调用会导致未定义行为。我见过有人在一个循环里不小心调了两次,结果程序崩溃了。
- 多线程环境下注意竞态: 一个线程在读,另一个线程在关,很容易出问题。建议用引用计数或状态标志来协调。
- SO_LINGER 的超时不要设太大: 我曾经设了 60 秒,结果程序在关闭连接时卡了一分钟,用户以为死机了。
- 不要依赖 SO_LINGER 来保证数据送达: 网络是不可靠的,应用层确认才是正道。
一句话总结: 用 shutdown() 来优雅地告诉对端「我说完了」,用 close() 来释放资源。至于 SO_LINGER,除非你很清楚自己在做什么,否则保持默认就好。
公众号:蓝海资料掘金营,微信deep3321