UNIX下C语言编程与实践60-UNIX TCP 套接字关闭:close 与 shutdown 函数的区别与使用场景

在 UNIX 环境下的 TCP 套接字编程中,连接的"关闭"并非简单的"断开"操作------不同场景下需要选择合适的关闭方式,以避免数据丢失、连接残留等问题。核心用于关闭套接字的函数有两个:closeshutdown。本文的内容,详细解析这两个函数的功能、参数与使用方法,对比其核心区别与适用场景,并通过实例演示如何正确使用它们处理 TCP 连接关闭。

一、TCP 套接字关闭核心函数解析

TCP 是面向连接的协议,套接字的关闭需要遵循"优雅关闭"或"强制关闭"的逻辑。close 函数基于"引用计数"实现,而 shutdown 函数基于"方向控制"实现,二者的设计理念和使用场景截然不同。

1.1 close 函数:基于引用计数的套接字关闭

close 函数是 UNIX 中通用的文件描述符关闭函数,同样适用于套接字------它的核心逻辑是"减少套接字的引用计数",仅当引用计数降至 0 时,才真正关闭 TCP 连接。

1.1.1 函数原型
复制代码
#include <unistd.h>

int close(int sockfd);
1.1.2 参数说明
参数 功能 使用注意
sockfd 需要关闭的套接字描述符(由 socketaccept 函数返回) 必须是当前进程持有的有效套接字描述符,否则会返回错误
1.1.3 核心机制:引用计数

UNIX 中的套接字描述符采用"引用计数"管理------当一个套接字被创建后(如 socket 创建客户端套接字,accept 创建服务器端连接套接字),其引用计数初始化为 1。当多个进程或线程共享该套接字(如 fork 子进程继承父进程的套接字)时,引用计数会相应增加。

close 函数的作用是将该套接字的引用计数减 1

  • 若引用计数减 1 后大于 0:仅释放当前进程对套接字的引用,TCP 连接仍保持(其他持有该套接字的进程可继续使用);
  • 若引用计数减 1 后等于 0:彻底关闭 TCP 连接(发送 FIN 数据包,触发 TCP 四次挥手),释放套接字占用的内核资源。

关键结论close 函数关闭的是"进程与套接字的关联",而非直接关闭"TCP 连接"。只有当所有持有该套接字的进程都调用 close 后,连接才会真正关闭。

1.1.4 返回值
  • 成功:返回 0(表示套接字描述符已释放,引用计数已更新);

  • 失败:返回 -1,并设置 errno(如 EBADF 表示套接字描述符无效,EINTR 表示调用被信号中断)。

1.2 shutdown 函数:基于方向控制的强制关闭

shutdown 函数是专为套接字设计的关闭函数------它不依赖引用计数,而是直接对 TCP 连接的"读方向"或"写方向"进行强制关闭,甚至可以同时关闭两个方向。这种"半关闭"能力是 shutdownclose 的核心区别。

1.2.1 函数原型
复制代码
#include <sys/socket.h>

int shutdown(int s, int how);
1.2.2 参数说明
参数 功能 常用取值与含义
s 需要操作的套接字描述符 close 函数的 sockfd 一致,需为有效套接字描述符
how 指定关闭的方向,决定对 TCP 连接的操作范围 * SHUT_RD:关闭"读方向"------当前进程无法再从套接字接收数据(即使对方发送数据,内核也会丢弃),后续调用 read/recv 会返回 0(表示EOF); * SHUT_WR:关闭"写方向"------当前进程无法再向套接字发送数据(后续调用 write/send 会返回错误),内核会向对方发送 FIN 数据包(表示"数据已发送完毕"); * SHUT_RDWR:同时关闭"读方向"和"写方向"------等效于先调用 SHUT_RD 再调用 SHUT_WR,彻底切断当前进程与对方的双向通信。
1.2.3 核心特性:不依赖引用计数

shutdown 函数的作用对象是"TCP 连接"本身,而非"套接字描述符的引用"------无论套接字的引用计数是否为 0,只要调用 shutdown,就会立即对指定方向的连接进行关闭:

  • 即使其他进程仍持有该套接字的引用,shutdown 关闭的方向对所有进程都生效(如进程 A 调用 shutdown(s, SHUT_WR) 后,进程 B 也无法再通过该套接字发送数据);
  • shutdown 不会改变套接字的引用计数,调用后仍需通过 close 释放套接字描述符(避免资源泄漏)。

核心优势shutdown 支持"半关闭"(如 SHUT_WR 关闭写方向但保留读方向),适用于"发送完数据后,需等待对方确认"的场景(如 HTTP 响应发送后,等待客户端确认接收)。

1.2.4 返回值
  • 成功:返回 0(表示指定方向的连接已关闭);

  • 失败:返回 -1,并设置 errno(如 EBADF 表示套接字无效,EINVAL 表示 how 参数取值错误)。

二、close 与 shutdown 函数的核心区别对比

为了更清晰地理解两个函数的差异,下表从"核心机制""连接影响""方向控制"等维度进行对比:

对比维度 close 函数 shutdown 函数
核心机制 基于"引用计数"------减少套接字描述符的引用计数 基于"方向控制"------直接操作 TCP 连接的读/写方向
对 TCP 连接的影响 仅当引用计数降至 0 时,才关闭连接(发送 FIN) 无论引用计数如何,立即关闭指定方向的连接(发送 FIN 或切断接收)
方向控制能力 无------只能"全关闭"(若连接关闭,读写方向均切断) 有------支持 SHUT_RD(读)、SHUT_WR(写)、SHUT_RDWR(读写)
多进程共享场景 单个进程调用后,其他进程仍可使用套接字(引用计数未到 0) 单个进程调用后,所有进程均受影响(如关闭写方向后,其他进程也无法发送数据)
资源释放 引用计数到 0 时,释放套接字内核资源 不释放套接字资源,仅关闭连接方向,需后续调用 close 释放描述符
适用场景 普通场景下的套接字释放(如单进程连接,无需半关闭) 需要半关闭、强制关闭或明确控制读写方向的场景(如等待对方确认数据)

三、函数使用实例演示

对套接字编程的实践思路,以下通过两个实例分别演示 closeshutdown 函数的使用,覆盖"多进程共享套接字"和"半关闭连接"两个典型场景。

3.1 实例1:close 函数与多进程共享套接字

场景:父进程通过 fork 创建子进程,父子进程共享同一个 TCP 连接套接字。演示"仅父进程调用 close 时,连接不关闭;子进程也调用 close 后,连接才关闭"的逻辑。

3.1.1 完整代码
复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 9001

// 错误处理宏
#define ERROR_CHECK(ret, msg) \
    if (ret == -1) { \
        perror(msg); \
        exit(EXIT_FAILURE); \
    }

int main() {
    int sockfd;
    struct sockaddr_in serv_addr;
    pid_t pid;

    // 步骤1:创建TCP套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    ERROR_CHECK(sockfd, "socket() failed");
    printf("创建套接字成功,sockfd = %d\n", sockfd);

    // 步骤2:初始化服务器地址并连接
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    serv_addr.sin_port = htons(SERVER_PORT);

    int conn_ret = connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    ERROR_CHECK(conn_ret, "connect() failed");
    printf("与服务器 %s:%d 连接成功\n", SERVER_IP, SERVER_PORT);

    // 步骤3:fork子进程,共享sockfd
    pid = fork();
    ERROR_CHECK(pid, "fork() failed");

    if (pid == 0) {
        // 子进程:睡眠5秒后关闭套接字
        printf("子进程(PID: %d):将在5秒后调用close()\n", getpid());
        sleep(5);
        
        int close_ret = close(sockfd);
        ERROR_CHECK(close_ret, "child close() failed");
        printf("子进程(PID: %d):调用close()成功,释放套接字引用\n", getpid());
        
        // 子进程继续运行3秒,模拟其他操作
        sleep(3);
        printf("子进程(PID: %d):退出\n", getpid());
        exit(EXIT_SUCCESS);
    } else {
        // 父进程:立即调用close()
        printf("父进程(PID: %d):立即调用close()\n", getpid());
        int close_ret = close(sockfd);
        ERROR_CHECK(close_ret, "parent close() failed");
        printf("父进程(PID: %d):调用close()成功,释放套接字引用\n", getpid());
        
        // 父进程等待子进程退出,避免僵死进程
        wait(NULL);
        printf("父进程(PID: %d):子进程已退出,连接此时才真正关闭\n", getpid());
    }

    return 0;
}
3.1.2 代码说明与运行结果

代码逻辑:

  1. 父进程创建套接字并连接服务器,此时套接字引用计数为 1;
  2. fork 子进程后,子进程继承套接字,引用计数变为 2;
  3. 父进程立即调用 close,引用计数减为 1(连接未关闭,子进程仍可使用);
  4. 子进程睡眠 5 秒后调用 close,引用计数减为 0(连接真正关闭,触发四次挥手)。

运行结果(需先启动监听 9001 端口的 TCP 服务器):

复制代码
创建套接字成功,sockfd = 3
与服务器 127.0.0.1:9001 连接成功
父进程(PID: 1234):立即调用close()
父进程(PID: 1234):调用close()成功,释放套接字引用
子进程(PID: 1235):将在5秒后调用close()
子进程(PID: 1235):调用close()成功,释放套接字引用
子进程(PID: 1235):退出
父进程(PID: 1234):子进程已退出,连接此时才真正关闭

现象验证 :可通过 netstat -an | grep 9001 命令观察连接状态------父进程调用 close 后,连接仍处于 ESTABLISHED(已建立)状态;子进程调用 close 后,连接才变为 TIME_WAIT(关闭过程中)。

3.2 实例2:shutdown 函数与半关闭连接

场景:TCP 客户端向服务器发送完数据后,需要"关闭写方向"(告知服务器"数据已发送完毕"),但仍需"保留读方向"(等待服务器返回确认信息)。此时 shutdown(SHUT_WR) 是最佳选择。

3.2.1 完整代码
复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 9001
#define BUF_SIZE 1024

#define ERROR_CHECK(ret, msg) \
    if (ret == -1) { \
        perror(msg); \
        exit(EXIT_FAILURE); \
    }

int main() {
    int sockfd;
    struct sockaddr_in serv_addr;
    char send_buf[BUF_SIZE] = "Hello Server! This is client data.";
    char recv_buf[BUF_SIZE] = {0};
    int ret;

    // 步骤1:创建并连接套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    ERROR_CHECK(sockfd, "socket() failed");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    serv_addr.sin_port = htons(SERVER_PORT);

    ret = connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    ERROR_CHECK(ret, "connect() failed");
    printf("与服务器 %s:%d 连接成功\n", SERVER_IP, SERVER_PORT);

    // 步骤2:向服务器发送数据
    ret = send(sockfd, send_buf, strlen(send_buf), 0);
    ERROR_CHECK(ret, "send() failed");
    printf("发送数据:%s(长度:%d 字节)\n", send_buf, ret);

    // 步骤3:调用shutdown关闭写方向(半关闭),保留读方向
    ret = shutdown(sockfd, SHUT_WR);
    ERROR_CHECK(ret, "shutdown(SHUT_WR) failed");
    printf("已关闭写方向:无法再发送数据,但可接收服务器响应\n");

    // 步骤4:尝试发送数据(应失败)
    ret = send(sockfd, "This is a test after shutdown", 29, 0);
    if (ret == -1) {
        printf("尝试发送数据失败(符合预期):%s\n", strerror(errno));
    }

    // 步骤5:接收服务器的确认响应(读方向仍有效)
    ret = recv(sockfd, recv_buf, BUF_SIZE, 0);
    if (ret > 0) {
        printf("收到服务器响应:%s(长度:%d 字节)\n", recv_buf, ret);
    } else if (ret == 0) {
        printf("服务器已关闭连接(收到EOF)\n");
    } else {
        perror("recv() failed");
    }

    // 步骤6:关闭套接字(释放描述符)
    ret = close(sockfd);
    ERROR_CHECK(ret, "close() failed");
    printf("已关闭套接字,释放资源\n");

    return 0;
}
3.2.2 代码说明与运行结果

代码逻辑:

  1. 客户端连接服务器后,发送数据给服务器;
  2. 调用 shutdown(sockfd, SHUT_WR) 关闭写方向------此时客户端无法再发送数据,但可接收服务器的响应;
  3. 尝试再次发送数据,验证写方向已关闭(应返回错误);
  4. 接收服务器的确认响应(读方向有效);
  5. 调用 close 释放套接字描述符。

运行结果(服务器需在收到数据后返回确认信息,如"Data received successfully"):

复制代码
与服务器 127.0.0.1:9001 连接成功
发送数据:Hello Server! This is client data.(长度:35 字节)
已关闭写方向:无法再发送数据,但可接收服务器响应
尝试发送数据失败(符合预期):Bad file descriptor
收到服务器响应:Data received successfully(长度:26 字节)
已关闭套接字,释放资源

半关闭价值 :在需要"先发送完数据,再等待对方确认"的场景(如 HTTP 请求、文件传输),SHUT_WR 可以明确告知对方"数据已结束",同时避免因直接关闭连接导致无法接收确认信息。

四、常见错误与解决方案

在使用 closeshutdown 函数时,容易因对其机制理解不透彻导致错误。下表整理了常见问题、原因及解决方法:

常见错误 错误原因 解决方案
调用 close 后,其他进程仍能使用套接字,误以为"连接已关闭" 未理解 close 的引用计数机制------单个进程调用 close 仅释放自身引用,未改变其他进程的引用 1. 若需立即关闭连接,改用 shutdown(SHUT_RDWR); 2. 多进程场景下,确保所有共享套接字的进程都调用 close
调用 close 后,仍有未接收的数据丢失 close 关闭连接时,内核会丢弃套接字接收缓冲区中未读取的数据 1. 关闭前先调用 read/recv 读取剩余数据(直到返回 0 或错误); 2. 若需保留读数据,先调用 shutdown(SHUT_WR),读取完数据后再 close
shutdown 后未调用 close,导致套接字描述符泄漏 shutdown 仅关闭连接方向,不释放套接字描述符------进程退出前未 close 会导致描述符泄漏 养成"shutdown 后必 close"的习惯:无论 shutdown 是否成功,最终都需调用 close 释放描述符
shutdown 参数 how 取值错误(如传入 4),导致功能异常 how 参数的取值范围不了解------仅支持 SHUT_RDSHUT_WRSHUT_RDWR 三个宏定义 1. 严格使用系统定义的宏(避免直接传入数字); 2. 包含头文件 <sys/socket.h>,确保宏定义可见
多线程场景下,多个线程同时调用 close 同一套接字,导致错误 套接字描述符是进程级资源,多个线程同时 close 会导致引用计数异常(如重复减 1 至负数) 1. 对套接字操作加锁(如互斥锁),确保同一时间只有一个线程调用 close; 2. 设计线程职责,由单个线程负责套接字的关闭

五、拓展:TCP 套接字关闭后的状态与优化

TCP 连接关闭并非"即时完成",而是需要经过"四次挥手"过程,期间会处于不同的状态(如 TIME_WAITCLOSE_WAIT)。这些状态若处理不当,可能导致端口耗尽、连接残留等问题。

5.1 常见关闭状态解析

状态名称 产生场景 持续时间 作用
TIME_WAIT(时间等待) 主动关闭连接的一方(发送 FIN 后,收到对方 ACK 和 FIN,发送最后一个 ACK 后进入) 通常为 2*MSL(MSL 是报文最大生存时间,默认 30 秒,即 TIME_WAIT 持续 60 秒) 1. 确保最后一个 ACK 被对方接收(若对方未收到,会重发 FIN,TIME_WAIT 期间可再次发送 ACK); 2. 避免旧连接的数据包干扰新连接(等待旧数据包超时消失)
CLOSE_WAIT(关闭等待) 被动关闭连接的一方(收到对方 FIN 后,发送 ACK 后进入,此时应用层未调用 close/shutdown 无固定时间,取决于应用层何时关闭套接字 表示"已收到对方关闭请求,等待应用层关闭套接字"

5.2 状态优化方案

5.2.1 TIME_WAIT 状态优化

TIME_WAIT 状态的连接会占用端口(默认 60 秒),若短时间内大量创建并关闭连接(如高并发服务器),可能导致"端口耗尽"。优化方案:

  • 启用端口复用 :通过 setsockopt 函数设置 SO_REUSEADDR 选项,允许新连接复用处于 TIME_WAIT 状态的端口。
    示例代码:

    复制代码
    int reuse = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
  • 缩短 TIME_WAIT 时间 :在 Linux 系统中,可通过修改内核参数 net.ipv4.tcp_fin_timeout 缩短 MSL 时间(如设为 30 秒,TIME_WAIT 变为 60 秒 → 30 秒),但需谨慎操作(可能增加旧数据包干扰风险)。

5.2.2 CLOSE_WAIT 状态优化

CLOSE_WAIT 状态的产生本质是"应用层未及时关闭套接字"(如程序bug导致未调用 close/shutdown)。解决方案:

  • 确保套接字关闭逻辑完整 :在任何代码分支(正常流程、错误处理、信号中断)中,都需调用 closeshutdown 关闭套接字;
  • 监控 CLOSE_WAIT 连接数 :通过 netstat -an | grep CLOSE_WAIT | wc -l 定期检查,若数量异常增长,排查代码中是否存在套接字泄漏。

六、总结:如何选择 close 与 shutdown 函数?

在实际 TCP 套接字编程中,选择哪个函数关闭连接,需根据具体场景判断:

6.1 优先选择 close 函数的场景

  • 单进程使用套接字,无需半关闭或强制关闭------如简单的客户端连接,发送完数据后直接关闭;
  • 仅需释放当前进程对套接字的引用,不影响其他进程------如多进程共享套接字时,单个进程退出前释放描述符;
  • 普通场景下的"正常关闭"------无需控制读写方向,仅需确保连接最终关闭。

6.2 优先选择 shutdown 函数的场景

  • 需要半关闭连接------如发送完数据后,需等待对方确认(关闭写方向,保留读方向);
  • 需要强制关闭连接------如多进程共享套接字时,需立即切断连接(无论其他进程是否释放引用);
  • 明确控制读写方向------如仅关闭读方向(避免接收无效数据),或仅关闭写方向(告知对方数据已结束);
  • 异常场景下的紧急关闭------如检测到对方异常,需立即切断双向通信(调用 shutdown(s, SHUT_RDWR))。

最终建议shutdown 函数的功能更灵活,但使用后仍需调用 close 释放套接字描述符;close 函数更简单,但需注意引用计数机制的影响。在不确定的场景下,可优先使用"shutdown 控制方向 + close 释放资源"的组合,确保连接安全关闭。

相关推荐
梁辰兴3 小时前
计算机操作系统:进程同步
网络·缓存·操作系统·进程·进程同步·计算机操作系统
hazy1k4 小时前
K230基础-录放视频
网络·人工智能·stm32·单片机·嵌入式硬件·音视频·k230
AORO20254 小时前
适合户外探险、物流、应急、工业,五款三防智能手机深度解析
网络·人工智能·5g·智能手机·制造·信息与通信
white-persist5 小时前
XXE 注入漏洞全解析:从原理到实战
开发语言·前端·网络·安全·web安全·网络安全·信息可视化
风清再凯5 小时前
01-iptables防火墙安全
服务器·网络·安全
十重幻想5 小时前
PTA6-1 使用函数求最大公约数(C)
c语言·数据结构·算法
脑子慢且灵6 小时前
C语言与Java语言编译过程及文件类型
java·c语言·开发语言·汇编·编辑器
蒙奇D索大6 小时前
【C语言加油站】C语言文件操作详解:从“流”的概念到文件的打开与关闭
c语言·开发语言·笔记·学习·改行学it
云飞云共享云桌面7 小时前
东莞精密机械制造工厂如何10个SolidWorks共用一台服务器资源
java·运维·服务器·网络·数据库·电脑·制造