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 总是丢失,查了三天才发现是关闭时机不对。

优雅关闭的标准流程

  1. 调用 shutdown(sock, SHUT_WR),告诉对端「我发完了」
  2. 继续调用 read() 读取对端的剩余数据,直到返回 0
  3. 对端也关闭后,再调 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 知识体系图

下面这张图,把本章的核心知识点串起来了:

连接关闭核心知识体系 关闭连接 close() shutdown() SO_LINGER 引用计数减一 释放描述符 半关闭支持 不受引用计数影响 控制 close() 的阻塞行为 l_onoff=0(默认) l_onoff=1, l_linger=0 l_onoff=1, l_linger>0 核心原则:优先用 shutdown() 实现半关闭,用 close() 释放资源,慎用 SO_LINGER

10.5 避坑指南

最后,分享几个我踩过的坑:

  • 不要对同一个 socket 调两次 close(): 第二次调用会导致未定义行为。我见过有人在一个循环里不小心调了两次,结果程序崩溃了。
  • 多线程环境下注意竞态: 一个线程在读,另一个线程在关,很容易出问题。建议用引用计数或状态标志来协调。
  • SO_LINGER 的超时不要设太大: 我曾经设了 60 秒,结果程序在关闭连接时卡了一分钟,用户以为死机了。
  • 不要依赖 SO_LINGER 来保证数据送达: 网络是不可靠的,应用层确认才是正道。

一句话总结:shutdown() 来优雅地告诉对端「我说完了」,用 close() 来释放资源。至于 SO_LINGER,除非你很清楚自己在做什么,否则保持默认就好。


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