tcp挥手时的异常情况(SIGPIPE、shutdown vs close操作)

当TCP进行挥手关闭连接时,只关闭一端仍允许另一端发送数据。然而,一旦client执行close,服务端再尝试发送数据会收到RST并触发SIGPIPE信号。close会完全关闭连接,而shutdown则允许更细粒度的控制,如仅停止读或写。若客户端关闭后服务端继续发送,某些Linux版本会产生SIGPIPE信号,提示无效操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

正常的TCP端的四次挥手

在这里插入图片描述

问题1:如果只是关闭了一端,另一端还能发数据么

我们知道,TCP是一个双全工协议,从协议层面,我们了解到,如果client发了FIN包给服务端,在收到ACK之后,状态切换成FIN_WAIT2。此时,从协议层面,只是关闭了client->server这个方向的数据传输。而server->client端,则可以继续往client端发数据。可以参考《tcp/ip详解》卷1,第18.5章节(TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力)。既然是这样,我们就自己亲测一把,看是否如理论所言。

//tcpserver.cpp
#include <iostream>
#include <cstring>
#include <strings.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

void sigroutine(int no) {
    cout << "sigroutine call no=" << no << endl;;
}

int main(int argc, char *argv[])
{
    signal(SIGPIPE, sigroutine);
    //创建套接字
    int sk = socket(AF_INET, SOCK_STREAM, 0); 

    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[1]));
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    //端口绑定
    bind(sk, (struct sockaddr*)&server, sizeof(server));

    //监听
    listen(sk, 5); 

    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    socklen_t len = sizeof(client);
    //接受连接请求
    int talk = accept(sk, (struct sockaddr*)&client, &len);

    //发送数据
    send(talk, "Hello", strlen("Hello") + 1, 0); 
    //接收数据
    char buff[1024] = {'\0'};
    recv(talk, buff, sizeof(buff), 0); 
    cout << buff << endl;
        cout << "sleep 3s" << endl;
    sleep(3);
    cout << "now client has close, try to send msg to client" << endl;
    send(talk, "Hello", strlen("Hello") + 1, 0);

    //关闭套接字
    close(talk);
    close(sk);
    return 0;
//tcpclient.cpp
#include <iostream>
#include <cstring>
#include <strings.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
using namespace std;

int main(int argc, char *argv[])
{
    //创建套接字
    int sk = socket(AF_INET, SOCK_STREAM, 0); 

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    //连接服务器
    connect(sk, (struct sockaddr*)&server, sizeof(server));

    char buff[1024] = {'\0'};
    //接收数据
    recv(sk, buff, sizeof(buff), 0); 
    cout << buff << endl;
    //发送数据
    send(sk, "I am hahaya", strlen("I am hahaya") + 1, 0); 

    //关闭套接字
    close(sk);
    cout << "sleep 10s, then close" << endl;
    sleep(10);
    /*  
    shutdown(sk, SHUT_WR);
    memset(buff, 0, sizeof(buff));
    int ret = recv(sk, buff, sizeof(buff), 0);
    cout << "ret = " << ret << ":" << buff << endl;

    cout << "I have close, but I don't exit process!" << endl;
    sleep(100);
    */
    return 0;
}
#makefile
TARGET:=server client
all:$(TARGET)

$(TARGET):%:%.o
>---g++ -g -o $@  $<

%.o:%.cpp
>---g++ -g -c $<

clean:
>---rm -f *.o $(TARGET)

将代码放到同一个目录下,make之后,会出现两个可执行文件:server client
执行步骤:
1、启动server,不妨监听12354端口,命令:
./server 12354
2、启动tcpdump抓包看交互细节。tcpdump -i any port 12354 -nn
3、启动client,假设与server同机,命令:
./client 127.0.0.1 12354
看到tcpdump运行结果:
在这里插入图片描述
在客户端执行close之后,服务端尝试给客户端再发数据之后,客户端认为是一个非法包,直接回了一个RST。而此时,连接从FIN_WAIT2状态在收到非法包之后,回复了RST包之后,client状态从FIN_WAIT2直接重置成CLOSE(CLOSE是假想的一个状态,在机器上是看到没有这个连接了),如下图所示:
在这里插入图片描述
RST包产生之后,通过netstat已经看不到这个连接了。

问题2:为什么使用close之后,无法再收数据?

这里可能与linux的实现细节相关,具体可以参考Beej’s networking guide里面提到的,我这里摘抄过来。

Whew! You’ve been send()ing and recv()ing data all day long, and you’ve had it. You’re ready to close the connection on your socket descriptor. This is easy. You can just use the regular Unix file descriptor close() function:
close(sockfd);
This will prevent any more reads and writes to the socket. Anyone attempting to read or write the socket on the remote end will receive an error.
Just in case you want a little more control over how the socket closes, you can use the shutdown() function. It allows you to cut off communication in a certain direction, or both ways (just like close() does.) Synopsis:

简要说明,调用了close,表示从OS层面,告诉OS,我这个socket即不发,也不收了。如果需要更加细节的控制,可以使用shutdown函数替换之。

socket close vs shutdown

close

如果对于socket,后续不需要收且不需要发数据的情况下,即直接调用close就可以。而对于对方已经调用close操作的服务而言,再往其发送数据,会收到RST包。前面已验证。

shutdown

如果关闭了连接之后,还需要细节的控制,则可以选择使用shutdown。另外,shutdown中有两个控制参数。SHUT_RD表示不再收数据,SHUT_WR表示不再写数据。前面的示例中,如果使用shutdown,无论使用什么参数(SHUT_RD或者SHUT_WR或者SHUT_RDWR),服务端再往client发数据的话,能正常发数据,不会收到RST包。
如果设置参数为SHUT_RD,则应用层无法收到服务端后续发过来的数据。如果设置为SHUT_RDWR,则客户端通过API是能够正常收到服务端发过来的数据。

RST之后,服务端再发数据会如何?SIGPIPE信号出来

接着上面的场景继续往后讨论,如果客户端调用了close之后,服务端没有close,而是继续发一次数据,则客户端会返回一个RST包。此时,连接已经不存在了。而服务端再发消息的话,进程会收到一个SIGPIPE信号。(可能不能版本的linux的实现不同,我的测试机器上的linux上,是默认忽略了这个信号,需要显示安装信号处理函数才能捕获)。
SIGPIPE信号,可以理解为”管道破裂“的意思。当你尝试往一个对端已经关闭读操作的管道中写数据中,因为永远不会有进程读到数据,所以从OS的角度来说,有必要提醒你,这是一个不会有结果的操作,所以产生了SIGPIPE信号来提醒你。

<think>嗯,我现在需要解决用户的问题:TCP客户端调用socket.close()后,服务端的响应行为是怎样的。首先,我应该回顾之前提供的引用内容,特别是引用[1]中提到服务器处理连接的部分。用户引用了三个参考内容,但其中引用[1]中的关键点提到当客户端关闭连接,服务器需要管理这些socket,比如关闭所有当前连接后再停止监听。而引用[3]则解释了socket作为进程间通信工具的作用。接下来,我可能需要深入TCP协议的四次挥手过程。当客户端发起FIN包,服务端会进入CLOSE_WAIT状态,发送ACK,然后处理剩余数据。之后服务端调用close()发送自己的FIN,进入LAST_ACK,等待客户端的ACK。不过用户的问题集中在客户端关闭后服务端的行为,所以需要区分服务端在被动关闭的处理流程。用户可能存在的疑问点包括服务端如何感知客户端关闭、read()/recv()的返回值变化、socket的状态变化是否会影响资源释放,以及TIME_WAIT的作用。需要结合网络编程的实际经验,比如recv返回0,服务端应关闭对应的socket。此外,引用[1]中提到服务端需要保存连接的socket,并在关闭逐个关闭它们,这一点要强调。另外,要考虑可能的异常情况,比如服务端未及关闭socket导致资源泄漏,或者大量连接处于TIME_WAIT的影响。需要将这些因素整理成清晰的步骤,让用户理解服务端在处理客户端关闭的正确操作流程和注意事项。最后,用户可能需要知道如何正确编写服务器代码来处理关闭事件,比如在发现recv返回0关闭连接,并释放资源。这需要引用[1]中的建议,确保服务端管理好每个socket的生命周期。</think> 当TCP客户端调用`socket.close()`,服务端的具体响应行为如下(基于TCP协议规范和编程实践): --- ### 📌 服务端的具体响应流程 1. **接收`FIN`包 (四次挥手开始)** 客户端发送`FIN`包后,服务端的TCP协议栈会将此连接状态标记为**`CLOSE_WAIT`**(被动关闭状态)。此服务端会: * 向客户端回复`ACK`确认收到`FIN` * **应用层行为**:服务端套接字的`recv()`或`read()`调用会**返回0字节**,表示对端已关闭连接。 2. **处理待发送数据** 若服务端输出缓冲仍有待发送数据: * 系统会尝试发送剩余数据(若客户端还未关闭接收窗口) * 若发送失败(客户端已不可达),触发`SIGPIPE`信号或`send()`返回错误。 3. **服务端关闭连接 (完成四次挥手)** 服务端调用`socket.close()`后: * 发送`FIN`包给客户端 * 连接状态变为**`LAST_ACK`**(等待客户端最后的ACK) * 收到客户端的`ACK`后,连接进入**`TIME_WAIT`状态**(持续2MSL间,约1-4分钟),最终完全关闭。 4. **资源释放** 服务端程序需**主动关闭对应的服务端Socket对象**: ```cpp // C++示例(QT) QTcpSocket *clientSocket = ...; // 保存的客户端socket clientSocket->close(); delete clientSocket; // 释放内存 ``` 否则会持续占用文件描述符资源(文件句柄泄漏)。 --- ### ⚠ 关键注意事项 1. **不主动关闭的后果** 若服务端未关闭对应Socket: * 文件描述符耗尽(无法接受新连接) * 内存泄漏 * 大量连接滞留在`CLOSE_WAIT`状态(可通过`netstat -ant`观察) 2. **`TIME_WAIT`的必要性** 服务端主动关闭后出现的`TIME_WAIT`状态: * 确保最后一个`ACK`丢失能重传`FIN` * 避免旧连接的数据包干扰新连接 * **解决方案**:通过套接字选项`SO_REUSEADDR`允许重用端口(需谨慎)。 3. **连接管理优化** 生产环境中建议: * 使用连接池管理Socket对象[^1] * 设置心跳机制检测僵尸连接 * 非阻塞IO结合`select()`/`epoll()`处理大并发连接 --- ### 🌰 服务端典型代码逻辑 ```cpp // QT伪代码示例 void Server::onNewConnection() { QTcpSocket *socket = tcpServer->nextPendingConnection(); connect(socket, &QTcpSocket::readyRead, [=](){ if(socket->bytesAvailable() == 0) { // 检测到关闭 socket->close(); socket->deleteLater(); // 释放资源 } else { // 正常处理数据... } }); } ``` --- ### 📚 延伸问题 1. 为什么`TIME_WAIT`状态需要等待2MSL间? 2. 如何避免服务端出现大量`CLOSE_WAIT`状态连接? 3. TCP协议中的半关闭(`shutdown()`)与`close()`有何区别? 4. 服务端如何区分客户端异常断网和正常关闭? > 数据表明,未正确关闭的Socket可导致服务器每小泄漏数千个文件描述符,最终导致服务瘫痪[^1]。实际开发中需通过连接超机制和资源监控规避此问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值