优雅的断开套接字连接

一、基于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输出流。断开后套接字无法传输数据,但是仍可接收数据。如果输出缓冲区还有数据则会将其传递至目标主机。
3SHUT_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. 资源清理检查表

  1. 关闭所有相关描述符

    • 主套接字
    • 派生套接字(accept产生的)
    • 文件描述符
  2. 释放关联资源

    // 释放地址信息
    freeaddrinfo(res);
    
    // 关闭epoll/kqueue描述符
    close(epoll_fd);
    
    // 释放缓冲区
    free(buffer);
  3. 重置状态变量

    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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值