在 UNIX 环境下的 TCP 套接字编程中,连接的"关闭"并非简单的"断开"操作------不同场景下需要选择合适的关闭方式,以避免数据丢失、连接残留等问题。核心用于关闭套接字的函数有两个:close 和 shutdown。本文的内容,详细解析这两个函数的功能、参数与使用方法,对比其核心区别与适用场景,并通过实例演示如何正确使用它们处理 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 | 
需要关闭的套接字描述符(由 socket 或 accept 函数返回) | 
必须是当前进程持有的有效套接字描述符,否则会返回错误 | 
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 连接的"读方向"或"写方向"进行强制关闭,甚至可以同时关闭两个方向。这种"半关闭"能力是 shutdown 与 close 的核心区别。
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 释放描述符 | 
| 适用场景 | 普通场景下的套接字释放(如单进程连接,无需半关闭) | 需要半关闭、强制关闭或明确控制读写方向的场景(如等待对方确认数据) | 
三、函数使用实例演示
对套接字编程的实践思路,以下通过两个实例分别演示 close 和 shutdown 函数的使用,覆盖"多进程共享套接字"和"半关闭连接"两个典型场景。
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;
 fork子进程后,子进程继承套接字,引用计数变为 2;- 父进程立即调用 
close,引用计数减为 1(连接未关闭,子进程仍可使用); - 子进程睡眠 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 代码说明与运行结果
代码逻辑:
- 客户端连接服务器后,发送数据给服务器;
 - 调用 
shutdown(sockfd, SHUT_WR)关闭写方向------此时客户端无法再发送数据,但可接收服务器的响应; - 尝试再次发送数据,验证写方向已关闭(应返回错误);
 - 接收服务器的确认响应(读方向有效);
 - 调用 
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 可以明确告知对方"数据已结束",同时避免因直接关闭连接导致无法接收确认信息。
四、常见错误与解决方案
在使用 close 和 shutdown 函数时,容易因对其机制理解不透彻导致错误。下表整理了常见问题、原因及解决方法:
| 常见错误 | 错误原因 | 解决方案 | 
|---|---|---|
调用 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_RD、SHUT_WR、SHUT_RDWR 三个宏定义 | 
1. 严格使用系统定义的宏(避免直接传入数字); 2. 包含头文件 <sys/socket.h>,确保宏定义可见 | 
多线程场景下,多个线程同时调用 close 同一套接字,导致错误 | 
套接字描述符是进程级资源,多个线程同时 close 会导致引用计数异常(如重复减 1 至负数) | 
1. 对套接字操作加锁(如互斥锁),确保同一时间只有一个线程调用 close; 2. 设计线程职责,由单个线程负责套接字的关闭 | 
五、拓展:TCP 套接字关闭后的状态与优化
TCP 连接关闭并非"即时完成",而是需要经过"四次挥手"过程,期间会处于不同的状态(如 TIME_WAIT、CLOSE_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)。解决方案:
- 确保套接字关闭逻辑完整 :在任何代码分支(正常流程、错误处理、信号中断)中,都需调用 
close或shutdown关闭套接字; - 监控 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 释放资源"的组合,确保连接安全关闭。