LAN接口变更时TCP服务端程序卡在accept()函数的问题排查与解决方案咨询
首先纠正你的推测:插拔LAN网线并不会导致系统重置监听端口的路由——因为你的监听socket绑定的是0.0.0.0(所有网络接口),即使某个LAN口链路断开,监听socket依然会正常监听其他可用接口;当该LAN口恢复后,新的连接请求依然能正常送达。你遇到的accept()卡住,本质是阻塞式accept()会一直等待新连接,除非socket出现错误或被外部信号中断;而插拔网线的动作本身不会主动中断阻塞的accept()调用。
另外你的代码有个冗余问题:把bind()和listen()放在外层while(true)循环里,每次内层循环退出后都重复执行绑定和监听。这不仅没必要,还可能引入潜在的资源冲突(即使你用了SO_REUSEADDR)——监听socket只需要创建、绑定、监听一次,之后持续调用accept()处理新连接即可。
保持TCP监听状态的简单方案
你不需要直接切换到完全异步模式,以下几种轻量方案就能解决accept()卡住的问题:
1. 给监听socket设置超时(最简单的阻塞模式改造)
通过SO_RCVTIMEO选项给监听socket设置超时,这样accept()会在指定时间内无新连接时返回EAGAIN/EWOULDBLOCK,你可以借此机会检查状态或重新尝试accept():
#include <sys/time.h> #include <errno.h> int main() { // 创建监听socket int server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket == -1) { cerr << "Can't create a socket! Quitting" << endl; return 1; } int enable = 1; if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) { cerr << GetTime() << "Can't use SO_REUSEADDR" << endl; } // 设置accept超时(5秒) struct timeval timeout; timeout.tv_sec = 5; timeout.tv_usec = 0; if (setsockopt(server_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { cerr << GetTime() << "Can't set socket timeout" << endl; } // 绑定、监听只执行一次 int listenningport = 55555; sockaddr_in hint; hint.sin_family = AF_INET; hint.sin_port = htons(listenningport); inet_pton(AF_INET, "0.0.0.0", &hint.sin_addr); if (bind(server_socket, (sockaddr*) &hint, sizeof(hint)) == -1) { cerr << "Can't bind to IP/port" << endl; close(server_socket); return 1; } if (listen(server_socket, SOMAXCONN) == -1) { cerr << "Can't listen!" << endl; close(server_socket); return 1; } sockaddr_in client; socklen_t clientSize = sizeof(client); int msgSize = 0; // 初始化消息长度 char out_data[1024]; // 示例缓冲区,根据你的需求调整 while (true) { int clientSocket = accept(server_socket, (sockaddr*) &client, &clientSize); if (clientSocket == -1) { // 判断是否是超时错误 if (errno == EAGAIN || errno == EWOULDBLOCK) { // 超时,可以在这里检查LAN口状态(比如读取/sys/class/net/ethX/operstate) continue; } else { cerr << "Problem with client connecting!" << endl; // 真的出错,关闭socket后重建 close(server_socket); break; } } // 处理客户端数据 msgSize = 0; int bytesReceived = 0; while ((bytesReceived = recv(clientSocket, out_data + msgSize, sizeof(out_data) - msgSize - 1, 0)) > 0) { msgSize += bytesReceived; if (msgSize >= sizeof(out_data) - 1) break; // 防止缓冲区溢出 } if (bytesReceived == -1) { cerr << "Error in recv(). BytesReceived = -1" << endl; } close(clientSocket); } return 0; }
2. 用select()实现多路复用(推荐的同步非阻塞方案)
select()可以同时监听监听socket的可读事件(有新连接)和超时事件,不需要完全异步,代码复杂度低且稳定性高:
#include <sys/select.h> #include <errno.h> int main() { int server_socket = socket(AF_INET, SOCK_STREAM, 0); // ... 初始化socket、设置SO_REUSEADDR、bind、listen ... fd_set read_fds; struct timeval timeout; int msgSize = 0; char out_data[1024]; while (true) { FD_ZERO(&read_fds); FD_SET(server_socket, &read_fds); timeout.tv_sec = 5; timeout.tv_usec = 0; // 等待监听socket可读或超时 int ret = select(server_socket + 1, &read_fds, NULL, NULL, &timeout); if (ret == -1) { cerr << "select error!" << endl; break; } else if (ret == 0) { // 超时,可在这里检查网卡链路状态 continue; } // 有新连接请求 if (FD_ISSET(server_socket, &read_fds)) { sockaddr_in client; socklen_t clientSize = sizeof(client); int clientSocket = accept(server_socket, (sockaddr*) &client, &clientSize); if (clientSocket != -1) { // 处理客户端数据... msgSize = 0; int bytesReceived = 0; while ((bytesReceived = recv(clientSocket, out_data + msgSize, sizeof(out_data) - msgSize - 1, 0)) > 0) { msgSize += bytesReceived; if (msgSize >= sizeof(out_data) - 1) break; } close(clientSocket); } } } close(server_socket); return 0; }
3. 可选增强:监听网卡链路状态
如果你想在LAN口插拔时主动触发处理,可以读取/sys/class/net/ethX/operstate文件(ethX是你的LAN口名称,比如eth0),文件内容为up或down,可在超时检查时读取该文件,判断链路状态变化(一般情况下不需要,因为监听socket本身不受单网卡状态影响)。
异步模式是不是唯一方案?
绝对不是。完全异步模式(比如aio_*函数或libevent/libuv框架)是针对高并发场景的复杂方案,你的轻量应用用上述超时或多路复用方案完全足够,代码改动小,也能稳定解决accept()卡住的问题。
内容的提问来源于stack exchange,提问作者Olek

