C++ TCP端口扫描函数偶发挂起问题排查与修复咨询
TCP端口扫描偶发挂起的问题分析与修复
核心问题根源
1. 非阻塞模式设置失败未处理
set_non_blocking函数在设置非阻塞模式失败时(比如fcntl调用出错)直接返回,未通知调用者,此时socket仍处于阻塞模式。后续connect调用会触发内核级的TCP连接超时(通常长达数分钟),导致函数挂起。
2. Select被信号中断后未重试
当select被系统信号(如SIGPIPE、SIGALRM)中断时,返回-1且errno=EINTR,当前代码直接返回PORT_ERROR,未重新发起select调用,可能导致超时逻辑失效,间接引发挂起。
3. 重复关闭Socket
send_tcp_packet中当connect成功时直接调用close(sock),但tcp_scan_port后续又调用close_socket(sock),触发double close,可能引发未定义行为(包括挂起、崩溃)。
4. Select返回后的状态判断逻辑错误
当前代码仅通过FD_ISSET判断socket是否在可读/可写集合就返回PORT_OPEN,但即使socket被触发,也可能是连接失败(如对方返回RST),需通过getsockopt(SO_ERROR)确认实际连接状态。
修复方案与代码改进
1. 修复非阻塞模式设置函数
让set_non_blocking返回状态,调用者需检查设置是否成功,失败则直接终止扫描流程:
bool set_non_blocking(int sock) { #ifdef _WIN32 u_long iMode = 1; return ioctlsocket(sock, FIONBIO, &iMode) == 0; #else int flags = fcntl(sock, F_GETFL, 0); if (flags == -1) { return false; } return fcntl(sock, F_SETFL, flags | O_NONBLOCK) != -1; #endif }
2. 修正扫描入口函数
检查create_socket和set_non_blocking的返回值,避免无效socket进入后续流程:
int tcp_scan_port(const char* host, int port, int timeout_ms){ int sock = create_socket(); if (sock == -1) { return PORT_ERROR; } if (!set_non_blocking(sock)) { close_socket(sock); return PORT_ERROR; } struct sockaddr_in target_address; memset(&target_address, 0, sizeof(target_address)); target_address.sin_family = AF_INET; target_address.sin_port = htons(port); if (inet_pton(AF_INET, host, &target_address.sin_addr) != 1) { close_socket(sock); return PORT_ERROR; } int result = send_tcp_packet(sock, &target_address, timeout_ms); close_socket(sock); return result; }
3. 修复TCP报文发送函数
- 处理
select被信号中断的重试逻辑 - 移除重复的
close(sock)调用 - 统一通过
getsockopt(SO_ERROR)判断连接状态
int send_tcp_packet(int sock, const struct sockaddr_in* target_address, int timeout_ms) { int ret = connect(sock, (struct sockaddr*)target_address, sizeof(*target_address)); if (ret == 0) { return PORT_OPEN; } int error_code = 0; #ifdef _WIN32 error_code = WSAGetLastError(); #else error_code = errno; #endif if (error_code != EINPROGRESS && error_code != EINTR) { return (error_code == ECONNREFUSED) ? PORT_CLOSED : PORT_ERROR; } fd_set read_fds, write_fds; struct timeval tv; int result; do { FD_ZERO(&read_fds); FD_ZERO(&write_fds); FD_SET(sock, &read_fds); FD_SET(sock, &write_fds); tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; result = select(sock + 1, &read_fds, &write_fds, NULL, &tv); if (result == -1 && errno != EINTR) { perror("select"); return PORT_ERROR; } } while (result == -1 && errno == EINTR); if (result == 0) { int err; socklen_t err_len = sizeof(err); if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (void *)&err, &err_len) != 0) { perror("getsockopt"); return PORT_ERROR; } if (err == ECONNREFUSED || err == ETIMEDOUT) { return PORT_CLOSED; } else if (err == EHOSTUNREACH || err == EADDRINUSE) { return PORT_ERROR; } else { return PORT_FILTERED; } } else { int err; socklen_t err_len = sizeof(err); if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (void *)&err, &err_len) != 0) { perror("getsockopt"); return PORT_ERROR; } if (err == 0) { return PORT_OPEN; } else if (err == ECONNREFUSED) { return PORT_CLOSED; } else { return PORT_FILTERED; } } }
额外建议
- 确保
create_socket函数正确创建TCP socket,并设置SO_REUSEADDR、SO_REUSEPORT选项,避免端口占用问题。 - 多线程扫描时,控制并发线程数,避免系统资源耗尽导致的异常。
- 对于域名解析需求,需单独实现异步解析逻辑,避免阻塞扫描流程。
内容的提问来源于stack exchange,提问作者lomaster
相关产品推荐
相关产品推荐

