connect 函数的调用涉及到3次握手,默认connect函数为阻塞连接状态。 通常connect 会阻塞到三次握手的完成和失败,而这个connect阻塞超时时间会依赖于系统,一般为75s到几分钟时间。
一种方式可以通过该系统配置/proc/sys/net/ipv4/tcp_syn_retries,默认为6,可以改为3等。
对于不通过修改配置想要缩短这些超时时间的场景,一般可以采用非阻塞的connect进行连接。
当把一个TCP套接字设置为非阻塞后调用connect,connect会立即返回。
当连接建立成功时,connect返回0;
当connect返回-1失败时,对于非阻塞型,需要判断错误码是否为EINPROGRESS,若为EINPROGRESS,说明tcp正在建立连接,这时我们需要调用select或者poll、epoll等来检查该连接是否建立成功。
处理非阻塞connect的步骤:
第一步:创建socket,返回套接口描述符;
第二步:调用fcntl把套接口描述符设置成非阻塞;
第三步:调用connect开始建立连接;
第四步:判断连接是否成功建立;
若connect返回0,表示连接简称成功。若connect返回-1,且errno为EINPROGRESS,说明tcp正在建立连接,此时可以使用poll等继续处理,当连接建立成功后,会触发out事件,从而唤醒进程,poll返回。
Poll返回值为-1时表示出现错误;
返回值为0表示超时;
返回值大于0时,则说明检测到可读或可写的套接字描述符。但此时还不能判断是否成功,因为
1、当连接建立成功时,套接口描述符变成 可写(连接建立时,写缓冲区空闲,所以可写)
2、当连接建立出错时,套接口描述符变成 既可读又可写(由于有未决的错误,从而可读又可写)
所以此时调用getsockopt来得到套接口上待处理的错误(SO_ERROR),如果连接建立成功,这个错误值将是0。如果建立连接时遇到错误,则这个值是连接错误所对应的errno值(比如:ECONNREFUSED,ETIMEDOUT等)。
具体实现可以参考redis中 deps\hiredis 下非阻塞connect 实现。
//阻塞方式连接
redisContext *redisConnect(const char *ip, int port) {
redisContext *c;
c = redisContextInit();
c->flags |= REDIS_BLOCK;
redisContextConnectTcp(c,ip,port,NULL);
return c;
}
//阻塞且带超时时间的连接
redisContext *redisConnectWithTimeout(const char *ip, int port, const struct timeval tv) {
redisContext *c;
c = redisContextInit();
c->flags |= REDIS_BLOCK;
redisContextConnectTcp(c,ip,port,&tv);
return c;
}
//非阻塞方式建立连接
redisContext *redisConnectNonBlock(const char *ip, int port) {
redisContext *c;
c = redisContextInit();
c->flags &= ~REDIS_BLOCK;
redisContextConnectTcp(c,ip,port,NULL);
return c;
}
建立连接具体实现:
static int _redisContextConnectTcp(redisContext *c, const char *addr, int port,
const struct timeval *timeout,
const char *source_addr) {
int s, rv, n;
char _port[6]; /* strlen(“65535”); */
struct addrinfo hints, *servinfo, *bservinfo, *p, *b;
int blocking = (c->flags & REDIS_BLOCK);
int reuseaddr = (c->flags & REDIS_REUSEADDR);
int reuses = 0;
long timeout_msec = -1;
servinfo = NULL;
c->connection_type = REDIS_CONN_TCP;
c->tcp.port = port;
if (c->tcp.host != addr) {
if (c->tcp.host)
free(c->tcp.host);
c->tcp.host = strdup(addr);
}
if (timeout) {
if (c->timeout != timeout) {
if (c->timeout == NULL)
c->timeout = malloc(sizeof(struct timeval));
memcpy(c->timeout, timeout, sizeof(struct timeval));
}
} else {
if (c->timeout)
free(c->timeout);
c->timeout = NULL;
}
if (redisContextTimeoutMsec(c, &timeout_msec) != REDIS_OK) {
__redisSetError(c, REDIS_ERR_IO, “Invalid timeout specified”);
goto error;
}
if (source_addr == NULL) {
free(c->tcp.source_addr);
c->tcp.source_addr = NULL;
} else if (c->tcp.source_addr != source_addr) {
free(c->tcp.source_addr);
c->tcp.source_addr = strdup(source_addr);
}
snprintf(_port, 6, “%d”, port);
memset(&hints,0,sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if ((rv = getaddrinfo(c->tcp.host,_port,&hints,&servinfo)) != 0) {
hints.ai_family = AF_INET6;
if ((rv = getaddrinfo(addr,_port,&hints,&servinfo)) != 0) {
__redisSetError(c,REDIS_ERR_OTHER,gai_strerror(rv));
return REDIS_ERR;
}
}
for (p = servinfo; p != NULL; p = p->ai_next) {
addrretry:
if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1)
continue;
c->fd = s;
// 先设置套接字为非阻塞
if (redisSetBlocking(c,0) != REDIS_OK)
goto error;
if (c->tcp.source_addr) {
int bound = 0;
/* Using getaddrinfo saves us from self-determining IPv4 vs IPv6 */
if ((rv = getaddrinfo(c->tcp.source_addr, NULL, &hints, &bservinfo)) != 0) {
char buf[128];
snprintf(buf,sizeof(buf),“Can’t get addr: %s”,gai_strerror(rv));
__redisSetError(c,REDIS_ERR_OTHER,buf);
goto error;
}
if (reuseaddr) {
n = 1;
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*) &n,
sizeof(n)) < 0) {
goto error;
}
}
for (b = bservinfo; b != NULL; b = b->ai_next) {
if (bind(s,b->ai_addr,b->ai_addrlen) != -1) {
bound = 1;
break;
}
}
freeaddrinfo(bservinfo);
if (!bound) {
char buf[128];
snprintf(buf,sizeof(buf),“Can’t bind socket: %s”,strerror(errno));
__redisSetError(c,REDIS_ERR_OTHER,buf);
goto error;
}
}
if (connect(s,p->ai_addr,p->ai_addrlen) == -1) {
if (errno == EHOSTUNREACH) {
redisContextCloseFd©;
continue;
} else if (errno == EINPROGRESS && !blocking) {
/* This is ok. */
} else if (errno == EADDRNOTAVAIL && reuseaddr) {
if (++reuses >= REDIS_CONNECT_RETRIES) {
goto error;
} else {
redisContextCloseFd©;
goto addrretry;
}
} else {
//当connect出现EINPROGRESS 时继续处理
if (redisContextWaitReady(c,timeout_msec) != REDIS_OK)
goto error;
}
}
//若是阻塞方式连接,则设置套接字为阻塞方式
if (blocking && redisSetBlocking(c,1) != REDIS_OK)
goto error;
if (redisSetTcpNoDelay© != REDIS_OK)
goto error;
c->flags |= REDIS_CONNECTED;
rv = REDIS_OK;
goto end;
}
if (p == NULL) {
char buf[128];
snprintf(buf,sizeof(buf),“Can’t create socket: %s”,strerror(errno));
__redisSetError(c,REDIS_ERR_OTHER,buf);
goto error;
}
error:
rv = REDIS_ERR;
end:
freeaddrinfo(servinfo);
return rv; // Need to return REDIS_OK if alright
}
static int redisContextWaitReady(redisContext *c, long msec) {
struct pollfd wfd[1];
wfd[0].fd = c->fd;
wfd[0].events = POLLOUT;//若为EINPROGRESS,则继续处理完成的链接if (errno == EINPROGRESS) {
int res;
//使用poll等待连接完成
if ((res = poll(wfd, 1, msec)) == -1) {
__redisSetErrorFromErrno(c, REDIS_ERR_IO, “poll(2)”);
redisContextCloseFd©;
return REDIS_ERR;
} else if (res == 0) {
errno = ETIMEDOUT;
__redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL);
redisContextCloseFd©;
return REDIS_ERR;
}
if (redisCheckSocketError© != REDIS_OK)
return REDIS_ERR;
return REDIS_OK;
}
__redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL);
redisContextCloseFd©;
return REDIS_ERR;
}
int redisCheckSocketError(redisContext *c) {
int err = 0;
socklen_t errlen = sizeof(err);
if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) {
__redisSetErrorFromErrno(c,REDIS_ERR_IO,“getsockopt(SO_ERROR)”);
return REDIS_ERR;
}
//若err不为0,说明异常,触发的事件不是链路的完成 if (err) {
errno = err;
__redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL);
return REDIS_ERR;
}
return REDIS_OK;
}
对于调用 redisConnectNonBlock 方法进行非阻塞建立连接时,在redis中异步API函数 redisAsyncConnect 会调用该函数。
当使用redisConnectNonBlock 非阻塞调用时,c->flags 中去除了 REDIS_BLOCK标志,在_redisContextConnectTcp中不会执行redisContextWaitReady 函数,因此也就不知道connect是否成功,所以后续使用套接口时要进行判断链路是否建立成功。
在异步IO中,可写事件的回调函数是redisAeWriteEvent,该函数调用redisAsyncHandleWrite实现,在该函数中,如果上下文标志位中还没有设置REDIS_CONNECTED标记,说明目前还没有检测是否建链成功,因此调用__redisAsyncHandleConnect,判断建链是否成功,如果建链成功,则会在异步上下文的标志位中增加REDIS_CONNECTED标记,如果还没有建链成功,则直接返回。
static int __redisAsyncHandleConnect(redisAsyncContext *ac) {
redisContext *c = &(ac->c);
if (redisCheckSocketError© == REDIS_ERR) {
/* Try again later when connect(2) is still in progress. */
if (errno == EINPROGRESS)
return REDIS_OK;
if (ac->onConnect) ac->onConnect(ac,REDIS_ERR);
__redisAsyncDisconnect(ac);
return REDIS_ERR;
}
/* Mark context as connected. */
c->flags |= REDIS_CONNECTED;
if (ac->onConnect) ac->onConnect(ac,REDIS_OK);
return REDIS_OK;
}
int redisCheckSocketError(redisContext *c) {
int err = 0;
socklen_t errlen = sizeof(err);
if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) {
__redisSetErrorFromErrno(c,REDIS_ERR_IO,“getsockopt(SO_ERROR)”);
return REDIS_ERR;
}
if (err) {
errno = err;
__redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL);
return REDIS_ERR;
}
return REDIS_OK;
}
原文 链接
非阻塞的connect使用方式
喜欢本文的朋友,欢迎关注公众号 Linux码农,获取更多干货