非阻塞connect编写方法介绍

本文详细介绍了非阻塞connect的工作原理及其在实际编程中的应用。包括如何通过fcntl函数设置socket为非阻塞模式,如何使用select函数进行超时控制,以及如何通过getsockopt函数获取错误信息等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

TCP连接的建立涉及到一个三次握手的过程,且SOCKET中connect函数需要一直等到客户接收到对于自己的SYN的ACK为止才返回,这意味着每个connect函数总会阻塞其调用进程至少一个到服务器的RTT时间,而RTT波动范围很大,从局域网的几个毫秒到几百个毫秒甚至广域网上的几秒。这段时间内,我们可以执行其他处理工作,以便做到并行。在此,需要用到非阻塞connect。本文主要介绍了非阻塞connect的编写方法以及应用场景。

1. 基础知识

(1) fcntl函数

fcntl函数可执行各种描述符的控制操作,对于socket描述符,常用应用是将其设置为阻塞式IO,代码如下:

int flags;
if((flags = fcntl(fd, F_GETFL)) < 0) //获取当前的flags标志
    err_sys(“F_GETFL error!”);
flags |= O_NONBLOCK; //修改非阻塞标志位
if(fcntl(fd, F_SETFL, flags) < 0)
    err_sys(“F_SETFL error!”);

(2) connect函数

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


 
if (connect(fd, (struct sockaddr*)&sa, sizeof(sa)) == -1)
   if (errno != EINPROGRESS) {
       return -1;
   }
   if(n == 0)
       goto done;

(3) select函数

select是一种IO多路复用机制,它允许进程指示内核等待多个事件的任何一个发生,并且在有一个或者多个事件发生或者经历一段指定的时间后才唤醒它。

connect本身并不具有设置超时功能,如果想对套接字的IO操作设置超时,可使用select函数。

fd_set wfd; 
FD_ZERO(&wfd); 
FD_SET(fd, &wfd); 
if(select(FD_SETSIZE, NULL, &wfd, NULL, toptr) == -1) {   
    __redisSetError(c, REDIS_ERR_IO, sdscatprintf(sdsempty(),"select(2): %s",strerror(errno)));   
    close(fd);   
    return REDIS_ERR;
} 
对于select和非阻塞connect,注意两点:

[1] 当连接成功建立时,描述符变成可写; [2] 当连接建立遇到错误时,描述符变为即可读,也可写,遇到这种情况,可调用getsockopt函数。

(4) getsockopt函数

可获取影响套接字的选项,比如SOCKET的出错信息:

err = 0;
errlen = sizeof(err);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) {
   sprintf("getsockopt(SO_ERROR): %s", strerror(errno)));
   close(fd);
   return ERR;
}
 
if (err) {
   errno = err;
   close(fd);
   return ERR;
}

2. 实现非阻塞式connect

分以下几步:

(1) 创建socket,并利用fcntl将其设置为非阻塞

(2) 调用connect函数,如果返回0,则连接建立;如果返回-1,检查errno ,如果值为 EINPROGRESS,则连接正在建立。

(3) 为了控制连接建立时间,将该socket描述符加入到select的可写集合中,采用select函数设定超时。

(4) 如果规定时间内成功建立,则描述符变为可写;否则,采用getsockopt函数捕获错误信息

(5) 恢复套接字的文件状态并返回。

3. 应用实例

(1)实例一

《unix网络编程》卷1的16.5节有一个Netscape 的web客户端的程序实例,客户端先建立一个与某个web服务器的HTTP连接,然后获取该网站的主页。该主页往往含有多个对于其他网页的引用,客户可以使用非阻塞connect同时获取多个网页,以此取代每次只获取一个网页的串行获取手段。

(2)实例二

Redis客户端CLI (command line interface),位于源代码的src/deps/hiredis下面。实际上,不仅是Redis客户端,其他类似的client/server架构中,client均可采用非阻塞式connect实现。

int redisContextConnectTcp(redisContext *c, const char *addr, int port, struct timeval *timeout) {
    int s;
    int blocking = (c->flags & REDIS_BLOCK);
    struct sockaddr_in sa;
 
    if ((s = redisCreateSocket(c,AF_INET)) < 0)
        return REDIS_ERR;
    if (redisSetBlocking(c,s,0) != REDIS_OK)
        return REDIS_ERR;
    sa.sin_family = AF_INET;
    sa.sin_port = htons(port);
    if (inet_aton(addr, &sa.sin_addr) == 0) {
        struct hostent *he;
    he = gethostbyname(addr);
    if (he == NULL) {
        __redisSetError(c,REDIS_ERR_OTHER,  sdscatprintf(sdsempty(),"Can't resolve: %s",addr));
        close(s);
        return REDIS_ERR;
    }
    memcpy(&sa.sin_addr, he->h_addr, sizeof(struct in_addr));
 
    if (connect(s, (struct sockaddr*)&sa, sizeof(sa)) == -1) {
        if (errno == EINPROGRESS && !blocking) {
        /* This is ok. */
    } else {
        if (redisContextWaitReady(c,s,timeout) != REDIS_OK)
            return REDIS_ERR;
    }
 
    /* Reset socket to be blocking after connect(2). */
    if (blocking && redisSetBlocking(c,s,1) != REDIS_OK)
        return REDIS_ERR;
 
    if (redisSetTcpNoDelay(c,s) != REDIS_OK)
        return REDIS_ERR;
 
    c->fd = s;
    c->flags |= REDIS_CONNECTED;
    return REDIS_OK;
}
 
static int redisContextWaitReady(redisContext *c, int fd, const struct timeval *timeout) {
    struct timeval to;
    struct timeval *toptr = NULL;
    fd_set wfd;
    int err;
    socklen_t errlen;
 
 /* Only use timeout when not NULL. */
    if (timeout != NULL) {
        to = *timeout;
        toptr = &to;
    }
 
    if (errno == EINPROGRESS) {
        FD_ZERO(&wfd);
        FD_SET(fd, &wfd);
 
        if (select(FD_SETSIZE, NULL, &wfd, NULL, toptr) == -1) {
            __redisSetError(c,REDIS_ERR_IO,
            sdscatprintf(sdsempty(), "select(2): %s", strerror(errno)));
            close(fd);
            return REDIS_ERR;
        }
 
        if (!FD_ISSET(fd, &wfd)) {
            errno = ETIMEDOUT;
            __redisSetError(c,REDIS_ERR_IO,NULL);
            close(fd);
            return REDIS_ERR;
        }
 
        err = 0;
        errlen = sizeof(err);
        if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) {
            __redisSetError(c,REDIS_ERR_IO,
            sdscatprintf(sdsempty(), "getsockopt(SO_ERROR): %s", strerror(errno)));
            close(fd);
            return REDIS_ERR;
        }
 
        if (err) {
            errno = err;
            __redisSetError(c,REDIS_ERR_IO,NULL);
            close(fd);
            return REDIS_ERR;
        }
 
   return REDIS_OK;
}
 
 __redisSetError(c,REDIS_ERR_IO,NULL);
   close(fd);
   return REDIS_ERR;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值