Linux下TCP非阻塞连接的方法

作者: staticnetwind 2017-03-25 11:06:06

一、背景


TCP连接函数用于连接服务器端口,若服务器地址不存在时,并不能在短时间内返回连接结果;


 


二、相关知识


1、连接超时机制


在非阻塞的socket下,调用connect连接函数会一直阻塞到连接建立或者连接失败,连接建立的时候那时间比较快,而失败的时候分错误情况如连接超时ETIMEOUT将为75秒到几分钟的时间。


2、非阻塞socket


非阻塞socket在调用后立刻返回结果,不会阻塞当前线程,并能够从当前结果结合 errno 进行判断执行的情况;


非阻塞socket一般配合I/O复用模型(epoll、select)去监控、处理多个I/O;


 


三、实现


主要步骤:


1、临时设置socket属性为非阻塞;


2、进行connect函数调用;


3、通过select检测socket是否可写;


4、处理异常,恢复socket原属性;


主入口,输入地址、端口、超时时间,输出socket句柄值:


int api_tcp_connect_setup_nonblock(u32 u32ip, u16 u16port, int *pdst_fd, int timeout_sec)


{


struct sockaddr_in dst_addr = {0};


struct sockaddr_in lcl_addr = {0};


memset(&dst_addr, 0, sizeof(struct sockaddr_in));


memset(&lcl_addr, 0, sizeof(struct sockaddr_in));


dst_addr.sin_family = AF_INET;


dst_addr.sin_addr.s_addr = u32ip;


dst_addr.sin_port = u16port;


lcl_addr.sin_family = AF_INET;


lcl_addr.sin_addr.s_addr = 0;


lcl_addr.sin_port = 0;


return __connect_setup_nonblock_b(lcl_addr, dst_addr, pdst_fd, timeout_sec);


}


二层嵌套,这个主要就是根据输入的本地绑定的地址lcl_addr(可选)、连接的目的地址dst_addr去完成连接;


static int __connect_setup_nonblock_b(struct sockaddr_in lcl_addr, struct sockaddr_in dst_addr, int *pdst_fd, int timeout_sec)


{


int ret  = FAILURE;


/* socket options */


int old_option = 0;


int new_option = 0;


int reuse = 1;


/* select */


fd_set writefds;


struct timeval timeout;


/* reset socket error */


int error = 0;


socklen_t elen = sizeof(error);


if ( !pdst_fd ) {


LOGW("input err\n");


goto _E1;


}


*pdst_fd = socket(AF_INET, SOCK_STREAM, 0);


if ( *pdst_fd == FAILURE ) {


LOGE("socket\n");


goto _E1;


}


ret = setsockopt(*pdst_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse));


if ( ret != SUCCESS ) {


LOGE("Setsockopt() SO_REUSEADDR failed \n");


goto _E2;


}


ret = bind(*pdst_fd, (struct sockaddr *)&lcl_addr, sizeof(struct sockaddr_in));


if ( ret != SUCCESS ) {


LOGE("Bind fail\n");


goto _E2;


}


/* Set nonblocking */


old_option = fcntl(*pdst_fd, F_GETFL);


new_option = old_option | O_NONBLOCK;


fcntl(*pdst_fd, F_SETFL, new_option);


ret = connect(*pdst_fd, (struct sockaddr*)&dst_addr, sizeof(struct sockaddr_in));


if ( ret == 0 ) {


fcntl(*pdst_fd, F_SETFL, old_option);


LOGD("connect success immediately\n");


goto _S0;


}


else if ( errno != EINPROGRESS )


{


/* Connect error */


LOGE("connect\n");


goto _E2;


}


FD_ZERO(&writefds);


FD_SET(*pdst_fd, &writefds);


timeout.tv_sec  = timeout_sec;


timeout.tv_usec = 0;


ret = select(*pdst_fd + 1, NULL, &writefds, NULL, &timeout);


if ( ret <= 0 ) {


/* timeout or error */


LOGE("timeout or error\n");


goto _E2;


}


if ( !FD_ISSET(*pdst_fd, &writefds) ) {


/* Not socketfd found */


LOGE("Not socketfd found\n");


goto _E2;


}


ret = getsockopt(*pdst_fd, SOL_SOCKET, SO_ERROR, (char *)&error, &elen);


if ( ret < 0 ) {


LOGE("getsockopt error\n");


goto _E2;


}


if ( error != 0 ) {


/* Connect failed after select */


LOGW("Connect failed after select with the error: %d %s\n", error, strerror(error));


goto _E2;


}


LOGD("connect success after select\n");


/* Connect success, set to old fnctl option */


fcntl(*pdst_fd, F_SETFL, old_option);


goto _S0;


_E2:


CLOSE_SOCK(*pdst_fd);


_E1:


return FAILURE;


_S0:


return SUCCESS;


}


需要注意的就是 connect 之后的 select 处理去判断连接是否成功;


select 返回 0,表示连接超时;


select 返回1,检测连接是否可写,


若套接字不可写则是有异常;


若套接字可写则还得使用 SO_ERROR 看看套接字上是否存在有待处理的错误如 ECONNREFUSED、ETIMEDOUT;


 


四、总结


非阻塞connect接口主要用于控制连接的时间,防止异常连接带来的过长的CPU占用;


而需要注意的是select 对套接字有所限制,当进程已经开启1024个fd后,select 接口将会导致错误,得考虑换成 epoll_wait 接口;


并且再进一步考虑时,就得考虑可移植性了,网上所说的主要围绕 getsockopt 函数的可移植问题;


 


本文永久更新地址:http://www.linuxdiyf.com/linux/29471.html

相关资讯