一、基于TCP的半关闭
TCP中的断开连接过程比建立连接过程更最重要,因为连接过程中一般不会出现大的变故,断开过程有可能发生预想不到的错误。
1、单方面断开连接带来的问题
Linux的close函数和Windows的closesocket函数意味着完全断开连接。完全断开不仅指无法传输数据,而且也不能接收数据。因此,在某些情况下,通信一方调用socket或者closesocket函数断开连接就显得不太优雅。
如果两台主机正在进行TCP双向通信,主机A发送完最后的数据后,调用close或者closesocket函数断开了连接,如果此时主机B还在向主机A发送数据,则断开之后主机A无法再继续接收主机B发送的数据。最终,主机B传输给主机A的重要数据就会被销毁。
因此,为了保证主机A断开连接之后还能继续接收到主机B发送的重要数据,只关闭一部分数据交换中使用的流(Half-close)的方法应运而生。断开一部分连接是指,可以传输数据但是无法接收,或者可以接收数据但是无法传输数据。
二、套接字和流(Stream)
两台主机通套接字建立连接后进入可交换数据的状态,又称“流形成的状态”。也就是把建立套接字后可交换数据的状态看作一种流。
一旦两台主机之间建立了套接字连接,每个主机就会拥有单独的输入流和输出流。假设主机A与主机B建立了连接,那么主机A的输出流对应主机B的输入流,主机A的输入流对应主机B的输出流。
优雅的断开连接即只断开输入输出流中的一个,即只断开输入流不断开输出流,或者只断开输出流不断开输入流。
三、优雅的断开连接函数shutdown()函数
函数原型如下:
#include <sys/socket.h>
int shutdown(int socket, int howto);
/*
成功返回 0, 失败返回 -1。
socket:需要断开部分流的通信套接字
howto:传递断开流的方式信息
howto可能值如下:
{
SHUT_RD : 断开输入流。
SHUT_WR : 断开输出流。
SHUT_RDWR : 同时断开输入输出流。
}
*/
名称 | 类型 | 特点 | |
1 | SHUT_RD | 输入流。 | 断开后套接字无法接收数据,但是仍可传输数据。即使输入缓冲区收到数据也会抹去,且无法调用输入相关函数。 |
2 | SHUT_WR | 输出流。 | 断开后套接字无法传输数据,但是仍可接收数据。如果输出缓冲区还有数据则会将其传递至目标主机。 |
3 | SHUT_RDWR | 输入流和输出流 | 同时中断I/O流。相当于分两次调用shutdown函数,一次以SHUT_RD为参数,另一次以SHUT_WR为参数。 |
关闭输入流代码示例:
// 1. 关闭写方向(发送FIN)
shutdown(sockfd, SHUT_WR);
// 2. 继续读取直到对方关闭连接(收到FIN,recv返回0)
char buffer[1024];
while (recv(sockfd, buffer, sizeof(buffer), 0) > 0) {
// 丢弃数据或处理剩余数据
}
// 3. 关闭套接字
close(sockfd);
四、使用SO_LINGER选项
SO_LINGER
选项可以控制close()
的行为,允许在关闭时等待未发送的数据和确认。
设置SO_LINGER:
#include <sys/socket.h>
struct linger lin;
lin.l_onoff = 1; // 启用linger选项
lin.l_linger = 10; // 等待时间(秒)
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
-
行为:
- 当调用
close()
时,如果发送缓冲区有数据,进程会阻塞直到数据被发送并确认,或者超过l_linger
指定的时间。 - 如果
l_linger
设为0,则直接丢弃数据并发送RST(非优雅关闭)。
- 当调用
注意事项:
- 设置
SO_LINGER
并指定超时时间,可以确保在关闭连接前发送所有数据。 - 但
SO_LINGER
可能导致close()
阻塞,因此通常建议使用非阻塞套接字结合超时。
五、TCP连接的优雅断开
1. 基本关闭流程(推荐)
// 1. 发送FIN通知对方数据发送完毕
shutdown(sockfd, SHUT_WR);
// 2. 读取剩余数据(可选)
char buffer[1024];
while (recv(sockfd, buffer, sizeof(buffer), 0) > 0) {
// 处理剩余数据
}
// 3. 完全关闭连接
close(sockfd);
2. 带超时的优雅关闭
// 设置超时结构
struct timeval timeout = {.tv_sec = 5, .tv_usec = 0};
// 1. 发送FIN
shutdown(sockfd, SHUT_WR);
// 2. 设置读超时
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
// 3. 读取剩余数据(带超时)
char buffer[1024];
ssize_t n;
while ((n = recv(sockfd, buffer, sizeof(buffer), 0)) > 0) {
// 处理数据
}
// 4. 处理可能的超时
if (n == -1 && errno == EWOULDBLOCK) {
// 超时处理
}
// 5. 完全关闭
close(sockfd);
3. 使用SO_LINGER选项
struct linger lin = {
.l_onoff = 1, // 启用linger
.l_linger = 10 // 等待10秒
};
// 设置linger选项
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
// 关闭连接(会阻塞直到数据发送完毕或超时)
close(sockfd);
六、UDP连接的优雅断开
1. 已连接UDP套接字
// 1. 发送断开通知(应用层协议)
const char* disconnect_msg = "DISCONNECT";
send(sockfd, disconnect_msg, strlen(disconnect_msg), 0);
// 2. 清空接收缓冲区
char buffer[1500];
while (recv(sockfd, buffer, sizeof(buffer), MSG_DONTWAIT) > 0) {
// 丢弃剩余数据
}
// 3. 正式断开连接
struct sockaddr unspec = { .sa_family = AF_UNSPEC };
connect(sockfd, (struct sockaddr*)&unspec, sizeof(unspec));
// 4. 关闭套接字
close(sockfd);
2. 未连接UDP套接字
// 1. 清空接收缓冲区
struct sockaddr_in src_addr;
socklen_t addr_len = sizeof(src_addr);
char buffer[1500];
while (recvfrom(sockfd, buffer, sizeof(buffer), MSG_DONTWAIT,
(struct sockaddr*)&src_addr, &addr_len) > 0) {
// 丢弃剩余数据
}
// 2. 关闭套接字
close(sockfd);
七、高级场景处理
1. 非阻塞套接字的优雅关闭
// 1. 发送FIN
shutdown(sockfd, SHUT_WR);
// 2. 使用poll/epoll等待可读事件
struct pollfd fds[1] = {{ .fd = sockfd, .events = POLLIN }};
int timeout_ms = 5000; // 5秒超时
while (1) {
int ret = poll(fds, 1, timeout_ms);
if (ret == 0) {
// 超时处理
break;
} else if (ret > 0) {
if (fds[0].revents & POLLIN) {
char buffer[1024];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n <= 0) break; // 连接关闭或出错
}
} else {
// 错误处理
perror("poll failed");
break;
}
}
// 3. 完全关闭
close(sockfd);
2. 多线程环境的安全关闭
// 共享标志
atomic_bool shutdown_requested = false;
// 发送线程
void* sender_thread(void* arg) {
while (!shutdown_requested) {
// 发送数据
}
// 通知接收方关闭
shutdown(sockfd, SHUT_WR);
return NULL;
}
// 接收线程
void* receiver_thread(void* arg) {
while (1) {
// 接收数据
if (/* 收到关闭通知 */ || shutdown_requested) {
break;
}
}
// 关闭套接字
close(sockfd);
return NULL;
}
3. HTTP类协议的优雅关闭
// 1. 发送Connection: close头
const char* response =
"HTTP/1.1 200 OK\r\n"
"Connection: close\r\n"
"\r\n"
"Goodbye!";
send(sockfd, response, strlen(response), 0);
// 2. 等待客户端关闭连接(根据协议)
struct pollfd fds = { .fd = sockfd, .events = POLLIN };
poll(&fds, 1, 3000); // 等待3秒
// 3. 关闭连接
close(sockfd);
八、跨平台最佳实践
1. Windows平台特殊处理
// 1. 发送FIN
shutdown(sockfd, SD_SEND);
// 2. 接收剩余数据
char buffer[1024];
int n;
while ((n = recv(sockfd, buffer, sizeof(buffer), 0)) > 0) {
// 处理数据
}
// 3. 关闭套接字
closesocket(sockfd);
// 4. 清理Winsock
WSACleanup();
2. 可移植的关闭函数
void graceful_close(int sockfd) {
#ifdef _WIN32
shutdown(sockfd, SD_SEND);
char buffer[1024];
while (recv(sockfd, buffer, sizeof(buffer), 0) > 0);
closesocket(sockfd);
#else
shutdown(sockfd, SHUT_WR);
char buffer[1024];
while (recv(sockfd, buffer, sizeof(buffer), 0) > 0);
close(sockfd);
#endif
}
九、错误处理与资源清理
1. 全面的错误处理
void safe_close(int sockfd) {
if (sockfd < 0) return;
// 尝试优雅关闭
if (shutdown(sockfd, SHUT_WR) == -1) {
// 处理shutdown错误
if (errno != ENOTCONN && errno != ENOTSOCK) {
perror("shutdown failed");
}
}
// 清空接收缓冲区
char buffer[1024];
while (1) {
ssize_t n = recv(sockfd, buffer, sizeof(buffer), MSG_DONTWAIT);
if (n <= 0) break;
}
// 关闭套接字
if (close(sockfd) == -1) {
perror("close failed");
}
}
2. 资源清理检查表
-
关闭所有相关描述符:
- 主套接字
- 派生套接字(accept产生的)
- 文件描述符
-
释放关联资源:
// 释放地址信息 freeaddrinfo(res); // 关闭epoll/kqueue描述符 close(epoll_fd); // 释放缓冲区 free(buffer);
-
重置状态变量:
sockfd = -1; is_connected = false;
十、性能优化技巧
1. 连接池管理
#define MAX_CONN 100
struct Connection {
int fd;
time_t last_used;
bool in_use;
} pool[MAX_CONN];
void return_connection(int sockfd) {
// 优雅关闭连接
shutdown(sockfd, SHUT_WR);
// 快速清空接收缓冲区(非阻塞模式)
char buffer[1024];
while (recv(sockfd, buffer, sizeof(buffer), MSG_DONTWAIT) > 0);
// 重置为未连接状态
struct sockaddr unspec = { .sa_family = AF_UNSPEC };
connect(sockfd, (struct sockaddr*)&unspec, sizeof(unspec));
// 放回连接池
mark_as_available(sockfd);
}
2. 批量关闭技术
void close_multiple(int fds[], int count) {
// 第一阶段:发送所有FIN
for (int i = 0; i < count; i++) {
if (fds[i] != -1) {
shutdown(fds[i], SHUT_WR);
}
}
// 第二阶段:使用poll处理接收
struct pollfd *pfds = calloc(count, sizeof(struct pollfd));
for (int i = 0; i < count; i++) {
pfds[i].fd = fds[i];
pfds[i].events = POLLIN;
}
poll(pfds, count, 100); // 100ms超时
// 第三阶段:关闭所有描述符
for (int i = 0; i < count; i++) {
if (fds[i] != -1) {
close(fds[i]);
fds[i] = -1;
}
}
free(pfds);
}