BSD Socket疑问:服务器丢弃数据包时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操作成功仅代表数据已被拷贝到内核发送缓冲区,不代表对方已接收或处理数据,结合你的测试场景,具体拆解如下:
send的本质逻辑
当调用send时,只要内核发送缓冲区还有剩余空间容纳数据,就会立即返回成功,数据会被放入缓冲区等待TCP协议栈后续处理。此时客户端完全不知道服务器已阻断入站流量——因为服务器用DROP规则静默丢弃数据包,既不会回复ACK确认,也不会发送RST断开包,客户端TCP栈无法立即感知连接异常。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)。
- 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

