linux_TCP_connect_阻塞处理

Linux TCP connect 阻塞处理

问题

实现网关modbus-tcp 采集功能时需要实现tcp client,自测时发现程序初始化阶段阻塞在了tcp connect处,原因是我用来测试的tcp server还没准备好,在初始化时阻塞会导致整个程序运行不下去,是非常严重的问题。本来想改为非阻塞就解决,网上查了一下资料,了解到别人是为了解决网络不好,三次握手相关问题的。之前面试被问到三次握手,自己心里还嘀咕,连上就连上,连不上重连,十几年没遇到这种问题,怎么老是有人问,现在知道原来会阻塞,记录一下。
别人的问题是"使用阻塞connect会阻塞太久,非阻塞connect遇到EINPROGRESS错误"。在linux下"man connect"看了一下说明:

EINPROGRESS
The socket is nonblocking and the connection cannot be completed immediately. It is possible to select(2) or poll(2) for completion by selecting the socket for writing. After select(2) indicates
writability, use getsockopt(2) to read the SO_ERROR option at level SOL_SOCKET to determine whether connect() completed successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the usual
error codes listed here, explaining the reason for the failure).

直接复制别人的说明:

对于阻塞式套接字,调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或者出错时才返回,阻塞时长几十秒到几分钟不等;
对于非阻塞式套接字,如果调用connect函数会之间返回-1(表示出错),且错误为EINPROGRESS,表示连接建立,建立启动但是尚未完成;
如果返回0,则表示连接已经建立,这通常是在服务器和客户在同一台主机上时发生。

解决方法

别人是使用select的超时机制实现的,方法如下(也是抄来的):

(1) 创建socket,并利用fcntl将其设置为非阻塞
(2) 调用connect函数,如果返回0,则连接建立;如果返回-1,检查errno ,如果值为 EINPROGRESS,则连接正在建立;
(3) 为了控制连接建立时间,将该socket描述符加入到select的可读可写集合中,采用select函数设定超时;
(4) 如果规定时间内成功建立,则描述符变为可写;否则,采用getsockopt函数捕获错误信息;
(5) 恢复套接字的文件状态并返回。

因为自己的代码跟别人不一样,不能直接复制,参考着改造了一下:

/**
 * @brief 建立tcp连接
 * 
 * @param tc tcp client 对象指针
 * @return int 成功返回0,否则<0
 */
int tcp_client_connect(struct tcp_client_s *tc)
{
    int sockfd, ret;
    struct addrinfo hints, *res, *ressave;
    char tcp_service[64] = {0};
    int prev_flag, flag;
    struct timeval timeout;
    fd_set fdr, fdw;

    sprintf(tcp_service, "%d", tc->port);
    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    ret = getaddrinfo(tc->ip, tcp_service, &hints, &res);
    if (ret != 0)
    {
        log_print(LOG_ERROR, "tcp connect for %s:%s error, ret = %s", tc->ip, tcp_service, gai_strerror(ret));
        return -1;
    }

    ressave = res;
    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sockfd < 0)
            continue;

        flag = fcntl(sockfd, F_GETFL, 0);
        if (flag < 0)
        {
            close(sockfd);
            continue;
        }

        prev_flag = flag;
        flag |= O_NONBLOCK; // 设置为非阻塞
        flag = fcntl(sockfd, F_SETFL, flag);
        if (flag < 0) 
        {
            close(sockfd);
            continue;
        }

        ret = connect(sockfd, res->ai_addr, res->ai_addrlen);
        if (ret == 0) // 连接已建立
        {
            fcntl(sockfd, F_SETFL, prev_flag); // 恢复为阻塞
            break;
        }
        else 
        {
            if (errno == EINPROGRESS) // 如果connect返回-1,errno == EINPROGRESS,则连接正在建立
            {
                FD_ZERO(&fdr);
                FD_ZERO(&fdw);
                FD_SET(sockfd, &fdr);
                FD_SET(sockfd, &fdw);
                timeout.tv_sec = 5;
                timeout.tv_usec = 0;
                ret = select(sockfd + 1, &fdr, &fdw, NULL, &timeout);
                if ((ret == 1) && (FD_ISSET(sockfd, &fdw))) // 连接成功
                {
                    fcntl(sockfd, F_SETFL, prev_flag); // 恢复为阻塞
                    break;
                }
            }
        }

        close(sockfd); // 到这里代表没有连接成功
    } while ((res = res->ai_next) != NULL);

    freeaddrinfo(ressave);
    if (res == NULL)
    {
        log_print(LOG_DEBUG, "tcp_connect fail for %s:%s", tc->ip, tcp_service);
        ret = -1;
    }
    else
    {
        tc->sock_fd = sockfd;
        ret = 0;
    }

    return ret;
}

这个代码有可能返回失败的,就是说初始化时失败不要紧,后面外部有个线程做轮询时会先检查socket fd的,异常或未连接就尝试重连。

参考

linux下connect阻塞处理

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值