Linux服务器编程实践52-SO_LINGER选项:控制close关闭TCP连接的行为
1. 引言:close关闭TCP连接的默认行为
在Linux网络编程中,close()
系统调用是关闭文件描述符的常用方式,对于TCP连接对应的socket也不例外。默认情况下,当我们调用close(sockfd)
关闭一个TCP socket时,close
会立即返回,但内核并不会马上终止连接——它会负责将该socket对应的TCP发送缓冲区中残留的数据(未被对方确认的数据包)发送给通信对端,直到所有数据发送完成或超时。
这种默认行为虽然安全(避免数据丢失),但在某些场景下并不适用。例如:
- 服务器需要快速回收资源,不希望等待残留数据发送;
- 检测到通信异常(如客户端恶意连接),需要立即终止连接以避免资源浪费;
- 需要精确控制连接关闭的时机,避免长时间阻塞。
为了解决这些问题,Linux提供了SO_LINGER
socket选项,允许开发者自定义close
关闭TCP连接的行为。
2. SO_LINGER选项的核心原理
SO_LINGER
选项通过setsockopt()
系统调用设置,其核心是通过一个linger
结构体控制close
的行为。该结构体定义如下:
#include <sys/socket.h>
struct linger {int l_onoff; // 开启(非0)或关闭(0)SO_LINGER选项int l_linger; // 滞留时间(秒),仅当l_onoff非0时有效
};
其中,l_onoff
是开关变量,l_linger
是滞留时间(单位:秒)。根据这两个变量的不同组合,close
会产生三种不同的行为,我们通过下表详细说明:
l_onoff | l_linger | close行为 | 适用场景 |
---|---|---|---|
0 | 忽略 | 默认行为:close立即返回,内核异步发送残留数据,连接正常关闭(四次挥手)。 | 大多数常规场景,确保数据不丢失。 |
非0 | 0 | close立即返回,内核丢弃发送缓冲区残留数据,向对端发送RST报文(复位连接),强制终止连接。 | 异常场景(如客户端超时),快速回收资源。 |
非0 | >0 | close阻塞等待l_linger秒: 1. 若期间残留数据发送完成且收到确认,连接正常关闭; 2. 若超时仍未完成,内核丢弃残留数据,强制关闭连接。 | 需要等待数据发送,但又要避免无限阻塞的场景。 |
注意:当l_onoff=非0
且l_linger>0
时,close
的阻塞行为还受socket是否为“阻塞模式”影响:
- 阻塞socket:close会阻塞直到数据发送完成或超时;
- 非阻塞socket:close立即返回,需通过返回值和errno判断数据是否发送成功。
3. SO_LINGER选项的实践示例
下面通过三个完整的代码示例,演示SO_LINGER
三种配置的实际效果。所有示例基于TCP通信,包含客户端和服务器端代码。
3.1 示例1:默认行为(l_onoff=0)
该示例展示SO_LINGER
关闭时,close
的默认行为——内核异步发送残留数据。
服务器端代码(默认关闭SO_LINGER)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>int main() {// 1. 创建TCP socketint listen_fd = socket(PF_INET, SOCK_STREAM, 0);assert(listen_fd >= 0);// 2. 绑定地址struct sockaddr_in addr;bzero(&addr, sizeof(addr));addr.sin_family = AF_INET;addr.sin_addr.s_addr = htonl(INADDR_ANY);addr.sin_port = htons(8888);assert(bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0);// 3. 监听assert(listen(listen_fd, 5) == 0);// 4. 接受客户端连接struct sockaddr_in client_addr;socklen_t client_len = sizeof(client_addr);int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);assert(conn_fd >= 0);printf("客户端已连接:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));// 5. 发送数据后关闭(默认SO_LINGER关闭)const char* data = "Hello, Client! This is server data.";send(conn_fd, data, strlen(data), 0);printf("发送数据完成,调用close()...\n");// 默认行为:close立即返回,内核异步发送残留数据close(conn_fd);printf("close()已返回,连接将由内核完成关闭\n");close(listen_fd);return 0;
}
客户端代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>int main() {// 1. 创建TCP socketint sock_fd = socket(PF_INET, SOCK_STREAM, 0);assert(sock_fd >= 0);// 2. 连接服务器struct sockaddr_in server_addr;bzero(&server_addr, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");server_addr.sin_port = htons(8888);assert(connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == 0);// 3. 接收服务器数据char buf[1024] = {0};ssize_t recv_len = recv(sock_fd, buf, sizeof(buf)-1, 0);if (recv_len > 0) {printf("收到服务器数据:%s\n", buf);} else if (recv_len == 0) {printf("服务器正常关闭连接(四次挥手)\n");} else {printf("接收数据失败\n");}close(sock_fd);return 0;
}
运行结果
// 服务器输出
客户端已连接:127.0.0.1:54321
发送数据完成,调用close()...
close()已返回,连接将由内核完成关闭// 客户端输出
收到服务器数据:Hello, Client! This is server data.
服务器正常关闭连接(四次挥手)
结果说明:close
立即返回,内核后续完成TCP四次挥手,客户端正常收到数据并检测到连接正常关闭。
3.2 示例2:强制关闭(l_onoff=1,l_linger=0)
该示例展示SO_LINGER
开启且滞留时间为0时,close
强制关闭连接的行为——丢弃残留数据并发送RST报文。
// 服务器端关键代码(仅修改SO_LINGER配置,其他代码同示例1)
#include <sys/socket.h>// 设置SO_LINGER:l_onoff=1,l_linger=0
struct linger ling = {1, 0};
setsockopt(conn_fd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));// 发送数据(故意预留残留数据)
const char* data = "Hello, Client! This is server data with more content...";
// 只发送部分数据,制造残留
send(conn_fd, data, 10, 0);
printf("发送部分数据,调用close()强制关闭...\n");// 强制关闭:丢弃残留数据,发送RST
close(conn_fd);
printf("close()已返回,连接已强制关闭\n");
运行结果
// 服务器输出
客户端已连接:127.0.0.1:54322
发送部分数据,调用close()强制关闭...
close()已返回,连接已强制关闭// 客户端输出
收到服务器数据:Hello, Cli
接收数据失败(errno=104: Connection reset by peer)
结果说明:客户端仅收到部分数据,随后收到RST报文,连接被强制重置(Connection reset by peer
),服务器端残留数据被丢弃。
3.3 示例3:延迟关闭(l_onoff=1,l_linger=5)
该示例展示SO_LINGER
开启且滞留时间为5秒时,close
阻塞等待数据发送的行为。
// 服务器端关键代码(修改SO_LINGER配置)
struct linger ling = {1, 5}; // 滞留5秒
setsockopt(conn_fd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));// 发送大量数据,确保内核需要时间发送
char large_data[4096];
memset(large_data, 'A', sizeof(large_data));
send(conn_fd, large_data, sizeof(large_data), 0);
printf("发送大量数据,调用close()阻塞等待...\n");// 阻塞等待5秒:若数据发送完成则正常关闭,否则强制关闭
close(conn_fd);
printf("close()已返回,连接关闭完成\n");
运行结果(正常情况:数据5秒内发送完成)
// 服务器输出
客户端已连接:127.0.0.1:54323
发送大量数据,调用close()阻塞等待...
(等待约1秒后)
close()已返回,连接关闭完成// 客户端输出
收到服务器数据:AAAAA...(4096个A)
服务器正常关闭连接(四次挥手)
运行结果(超时情况:数据5秒内未发送完成)
// 服务器输出(模拟网络延迟,客户端未及时接收)
客户端已连接:127.0.0.1:54324
发送大量数据,调用close()阻塞等待...
(等待5秒后)
close()已返回,连接关闭完成// 客户端输出(因网络延迟,仅收到部分数据)
收到服务器数据:AAAAA...(2048个A)
接收数据失败(errno=104: Connection reset by peer)
结果说明:当数据在滞留时间内发送完成,连接正常关闭;若超时未完成,内核丢弃残留数据并强制关闭连接。
4. SO_LINGER选项的行为可视化
为了更直观地理解SO_LINGER
三种配置的差异,我们通过JavaScript绘制时序图,展示close
关闭连接的过程。
4.1 SO_LINGER三种配置的时序对比图
5. SO_LINGER的使用注意事项
5.1 避免滥用强制关闭(l_linger=0)
强制关闭(发送RST)会导致:
- 对端无法区分“正常关闭”和“异常重置”,可能误判网络故障;
- 残留数据丢失,破坏TCP的可靠传输特性;
- 可能引发“半打开连接”(对端仍认为连接有效,但服务器已关闭)。
建议仅在检测到恶意连接、超时等异常场景使用强制关闭。
5.2 合理设置滞留时间(l_linger>0)
滞留时间并非越长越好:
- 过短(如1秒):可能导致正常数据发送不完整;
- 过长(如30秒):
close
阻塞时间过长,影响服务器并发性能。
建议根据业务场景估算数据发送时间(如基于带宽和数据量),设置2~10秒的滞留时间。
5.3 结合非阻塞socket使用
当socket为非阻塞模式时,l_onoff=1
且l_linger>0
的配置不会导致close
阻塞,而是立即返回。此时需通过errno
判断数据是否发送成功:
// 非阻塞socket + SO_LINGER示例
int ret = close(conn_fd);
if (ret == 0) {printf("数据已发送完成,连接正常关闭\n");
} else if (errno == EWOULDBLOCK) {printf("数据未发送完成,已超时丢弃\n");
}
6. 总结
SO_LINGER
选项为开发者提供了对TCP连接关闭行为的精确控制,核心是通过l_onoff
和l_linger
的组合实现三种关闭策略:
- 默认策略(l_onoff=0):安全优先,适用于大多数需要可靠传输的场景;
- 强制策略(l_onoff=1,l_linger=0):效率优先,适用于异常场景下的快速资源回收;
- 延迟策略(l_onoff=1,l_linger>0):平衡安全与效率,适用于需要等待数据发送但避免无限阻塞的场景。
在实际开发中,需根据业务需求选择合适的策略,避免因滥用SO_LINGER
导致数据丢失或性能问题。