C++基于epoll的TCP客户端断连后无法自动重连问题求助
问题根因
- 断连后未关闭旧套接字直接复用调用
connect,触发Transport endpoint is already connected错误:TCP套接字一旦经历过连接失败/断连,无法直接复用发起新连接,必须关闭旧fd重新创建。 - 重连逻辑直接绑定在套接字读事件上,断连后旧fd仍在epoll监听列表,会持续触发事件导致重连被疯狂调用,占用大量CPU且无退避机制,容易被系统限流。
- 非阻塞
connect逻辑判断错误:非阻塞模式下connect返回-1且errno为EINPROGRESS属于正常状态,需要监听可写事件判断连接结果,当前代码直接判定为连接失败不符合非阻塞IO规范。 - 重连后新生成的套接字未重新注册到epoll,导致新连接无法收到读写事件。
修复方案
1. 调整TcpClient类定义
新增重连定时器、退避间隔、Reactor引用成员,新增重连相关方法:
class TcpClient { public: TcpClient(std::string host, int port, Reactor& reactor); // 传入reactor引用用于注册事件/定时器 void Start(); void Recive(); bool IsConnected() const { return _connected; } int sock() const { return _sock; } private: void CleanupOldSocket(); // 清理旧套接字 void TryReconnect(); // 重连逻辑 void OnConnectResult(); // 非阻塞连接结果判断 std::string _host; int _port, _sock; bool _connected; struct sockaddr_storage _addr; socklen_t _addr_len; // 存储地址结构长度 Reactor& _reactor; int _retry_interval = 1; // 初始重连间隔1s const int MAX_RETRY_INTERVAL = 30; // 最大重连间隔30s };
2. 实现套接字清理逻辑
断连时先移除epoll监听、关闭旧套接字:
void TcpClient::CleanupOldSocket() { if (_sock != -1) { _reactor.RemoveHandler(_sock); // 从epoll移除旧fd close(_sock); _sock = -1; } _connected = false; }
3. 修改Recive方法
断连时触发定时器重连,不要直接调用connect:
void TcpClient::Recive() { uint8_t data[256]; if (IsConnected() == true) { int res = recv(_sock, reinterpret_cast<char*>(data), 256, 0); if (res == -1 || res == 0) { Error << "Connection to " << _host << ":" << _port << " disconnected. (" << std::strerror(errno) << ")\n"; CleanupOldSocket(); // 注册定时器,_retry_interval秒后执行重连 _reactor.RegisterTimer(_retry_interval * 1000, std::bind(&TcpClient::TryReconnect, this)); // 重连间隔指数退避 _retry_interval = std::min(_retry_interval * 2, MAX_RETRY_INTERVAL); } else { // 处理数据逻辑 } } }
4. 实现重连逻辑
重新创建套接字发起连接,监听可写事件判断连接结果:
void TcpClient::TryReconnect() { if (_connected) return; // 已经连接成功直接跳过 // 重新创建套接字 int family = _addr.ss_family; _sock = socket(family, SOCK_STREAM | SOCK_NONBLOCK, 0); if (_sock == -1) { Error << "socket create failed: " << std::strerror(errno) << "\n"; // 重连失败,注册下一次定时器 _reactor.RegisterTimer(_retry_interval * 1000, std::bind(&TcpClient::TryReconnect, this)); _retry_interval = std::min(_retry_interval * 2, MAX_RETRY_INTERVAL); return; } int ret = connect(_sock, (const struct sockaddr*)&_addr, _addr_len); if (ret == 0) { // 连接直接成功 _connected = true; _retry_interval = 1; // 重置重连间隔 Info << "Connection to " << _host << ":" << _port << " established.\n"; // 注册读事件到epoll _reactor.RegisterHandler(_sock, std::bind(&TcpClient::Recive, this), TypeEvent::kRead); } else if (errno == EINPROGRESS) { // 非阻塞连接进行中,监听可写事件判断结果 _reactor.RegisterHandler(_sock, std::bind(&TcpClient::OnConnectResult, this), TypeEvent::kWrite); } else { // 连接失败 CleanupOldSocket(); _reactor.RegisterTimer(_retry_interval * 1000, std::bind(&TcpClient::TryReconnect, this)); _retry_interval = std::min(_retry_interval * 2, MAX_RETRY_INTERVAL); Error << "Connect failed: " << std::strerror(errno) << "\n"; } }
5. 实现非阻塞连接结果判断
void TcpClient::OnConnectResult() { int err = 0; socklen_t len = sizeof(err); getsockopt(_sock, SOL_SOCKET, SO_ERROR, &err, &len); if (err == 0) { // 连接成功 _connected = true; _retry_interval = 1; Info << "Connection to " << _host << ":" << _port << " established.\n"; // 取消可写事件监听,注册读事件 _reactor.RemoveHandler(_sock); _reactor.RegisterHandler(_sock, std::bind(&TcpClient::Recive, this), TypeEvent::kRead); } else { // 连接失败 Error << "Connect failed: " << strerror(err) << "\n"; CleanupOldSocket(); _reactor.RegisterTimer(_retry_interval * 1000, std::bind(&TcpClient::TryReconnect, this)); _retry_interval = std::min(_retry_interval * 2, MAX_RETRY_INTERVAL); } }
6. 修改主函数
修正reactor拼写错误,传入reactor引用到TcpClient:
int main() { Reactor reactor; TcpClient client("127.0.0.1", 1234, reactor); client.Start(); while(1) reactor.Wait(); }
补充说明
- 原Start方法中需要把addrinfo返回的ai_addrlen存到类成员
_addr_len,不要硬编码sizeof(_addr)传参给connect。 - 首次启动如果连接失败,也需要在Start方法末尾调用
CleanupOldSocket后触发第一次定时器重连。 - 如果你的Reactor不支持单次定时器,需要自行处理定时器的注销逻辑,避免重连成功后还有遗留的重连定时器被触发。
内容的提问来源于stack exchange,提问作者fafaadda
相关产品推荐
相关产品推荐

