You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

为何关闭TCP连接后首次send()不会触发SIGPIPE信号?

为什么TCP客户端在服务端关闭后首次send成功,第二次才触发SIGPIPE?

这是个非常典型的TCP连接状态机行为问题,咱们一步步拆解背后的逻辑:

1. 首次send成功的原因

当服务端主动调用close()关闭socket时,会向客户端发送一个FIN包,表示服务端不再发送数据,但此时客户端的TCP连接处于CLOSE_WAIT状态——客户端的发送通道其实还没被内核标记为失效。

当你调用send()时,这个函数的核心逻辑是把数据从用户态拷贝到内核的发送缓冲区,只要缓冲区有足够空间,send()就会立刻返回成功的字节数,不会立刻去检查对端是否还能接收。

此时服务端虽然已经关闭,但客户端还没收到服务端后续返回的RST包(因为服务端收到客户端首次send的数据后,发现连接已经关闭,才会发送RST包响应),所以内核认为发送操作是合法的,自然返回成功。

2. 第二次send触发SIGPIPE的原因

首次send的数据到达服务端后,服务端的TCP栈发现连接已经完全关闭,会向客户端返回一个RST包。客户端收到RST包后,内核会把该socket标记为已失效状态。

当你第二次调用send()时,内核检测到socket已经处于失效状态,就会触发SIGPIPE信号(默认行为是终止进程),同时send()返回-1errno被设置为EPIPE

复现代码示例

服务端简化代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv_addr = {AF_INET, htons(8080), INADDR_ANY};
    bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    listen(sockfd, 1);
    int connfd = accept(sockfd, NULL, NULL);
    // 等待客户端连接后,直接关闭连接
    close(connfd);
    close(sockfd);
    return 0;
}

客户端简化代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv_addr = {AF_INET, htons(8080), {inet_addr("127.0.0.1")}};
    connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    
    char buf[] = "hello";
    // 第一次send
    ssize_t ret1 = send(sockfd, buf, strlen(buf), 0);
    printf("First send returned: %zd\n", ret1);
    
    // 第二次send
    ssize_t ret2 = send(sockfd, buf, strlen(buf), 0);
    printf("Second send returned: %zd\n", ret2);
    
    close(sockfd);
    return 0;
}

操作步骤:

  • 编译并启动服务端,服务端接受连接后立刻关闭
  • 启动客户端,观察输出:第一次send返回5,第二次send触发SIGPIPE导致进程终止(如果没处理信号的话)

解决办法

如果你不想让进程被SIGPIPE终止,可以用以下几种方式处理:

  • 忽略SIGPIPE信号:在程序开头添加信号处理,这样send会返回EPIPE错误,你可以在代码中捕获并处理:

    #include <signal.h>
    signal(SIGPIPE, SIG_IGN);
    
  • 使用MSG_NOSIGNAL标志调用send:每次send时加上这个标志,内核就不会触发SIGPIPE,直接返回错误:

    ssize_t ret = send(sockfd, buf, len, MSG_NOSIGNAL);
    if (ret == -1 && errno == EPIPE) {
        // 处理连接关闭的情况
        printf("Connection closed by peer\n");
    }
    
  • 结合IO多路复用检测:用select()/poll()/epoll()先检测socket是否可写,但这种方式不能完全避免EPIPE(因为TCP状态可能在检测和send之间变化),所以还是建议结合前两种方式。

内容的提问来源于stack exchange,提问作者TsReaper

火山引擎 最新活动