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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.08 17:27:04