You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

BSD Socket疑问:服务器丢弃数据包时send为何仍返回成功?

问题:配置TCP保活后,服务器端阻断入站数据包,客户端send仍返回成功的原因

测试代码(client.c)

#include <arpa/inet.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

int set_keep_alive(int sockfd) {
    int optval = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) == -1) {
        perror("setsockopt");
        close(sockfd);
        return -1;
    }
    int flags = 6;
    if (setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, (void *)&flags, sizeof(flags))) {
        return -1;
    };

    flags = 3;
    if (setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, (void *)&flags, sizeof(flags))) {
        return -1;
    };

    flags = 3;
    if (setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, (void *)&flags, sizeof(flags))) {
        return -1;
    };
    return 0;
}

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "usage: %s <ip> <port>\n", argv[0]);
        return -1;
    }
    char const *IP = argv[1];
    int PORT = atoi(argv[2]);

    int fd = socket(AF_INET, SOCK_STREAM, 0);
    set_keep_alive(fd);
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    inet_pton(AF_INET, IP, &addr.sin_addr);
    int ret;
    ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret < 0) {
        printf("connect failed, ret = %d, errno=%d\n", ret, errno);
        perror("");
        return 1;
    }

    for(int i = 0;;i++, sleep(1)) {
        char buf[100];
        sprintf(buf, "hello%d", i%10);
        int ret = send(fd, buf, strlen(buf), MSG_CONFIRM);
        printf("%d,write ret = %d\n", i, ret);
    }
    close(fd);
    return 0;
}

测试步骤

  • 执行gcc client.c编译生成a.out;
  • 服务器端运行nc -l -p <PORT>监听指定端口(请替换为实际端口);
  • 客户端执行./a.out <Server IP> <PORT>(请替换为实际服务器IP和端口),持续调用send,控制台输出返回值为6(表示成功);
  • 服务器端执行port-close.sh脚本(通过iptables丢弃该端口的入站数据包),脚本内容如下:
#!/bin/sh
iptables -I INPUT -p tcp --dport $1 -j DROP

等待15秒(已设置的TCP保活时间)后,客户端的send调用仍返回成功。请问这一现象的原因是什么?


回答

这一现象的核心原因是TCP的send操作成功仅代表数据已被拷贝到内核发送缓冲区,不代表对方已接收或处理数据,结合你的测试场景,具体拆解如下:

  1. send的本质逻辑
    当调用send时,只要内核发送缓冲区还有剩余空间容纳数据,就会立即返回成功,数据会被放入缓冲区等待TCP协议栈后续处理。此时客户端完全不知道服务器已阻断入站流量——因为服务器用DROP规则静默丢弃数据包,既不会回复ACK确认,也不会发送RST断开包,客户端TCP栈无法立即感知连接异常。

  2. TCP保活的生效逻辑
    你配置的保活参数为:

  • TCP_KEEPIDLE=6:连接闲置6秒后发送第一次保活探测包
  • TCP_KEEPINTVL=3:两次探测包的间隔为3秒
  • TCP_KEEPCNT=3:连续3次探测无响应则判定连接失效

但DROP规则下,客户端发送的所有探测包都没有回应,TCP栈需要走完完整的探测周期才会标记连接失效:总时长为TCP_KEEPIDLE + TCP_KEEPCNT * TCP_KEEPINTVL = 6 + 3*3 = 15秒。即使到了15秒连接被标记为失效,当前正在执行的send依然可能返回成功——因为它只负责把数据拷贝到缓冲区,只有当后续协议栈尝试发送缓冲区数据、多次失败后,才会在下一次系统调用(send或read)中返回错误(如EPIPE)。

  1. DROP与REJECT的差异
    如果服务器用iptables -I INPUT -p tcp --dport $1 -j REJECT规则,会主动返回RST包,客户端TCP栈会立刻感知连接断开,send会直接返回错误。但DROP是静默丢弃,只能靠TCP超时机制判断,这个过程存在延迟,且不会立刻影响正在执行的send调用。

另外,代码中使用的MSG_CONFIRM标志仅用于辅助路径MTU发现,不会改变send的基本返回逻辑,依然只保证数据进入内核缓冲区,不保证对方接收。

总结:在服务器静默丢弃数据包的场景下,TCP保活需要走完完整探测周期才会判定连接失效,而send操作的成功仅代表数据进入内核缓冲区,不会立刻反映连接的实际状态,只有后续协议栈尝试发送数据失败后,才会在后续系统调用中返回错误。


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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 04:25:31