前言
一般来说,在TCP建立连接的过程中,我们的客户端使用connect发起连接,默认使用的是阻塞模式,只有在有明确结果的时候才会返回(连接成功或连接失败)。这样可能照成当我们向一个“比较远”的服务器发起连接时,我们的程序会阻塞在connect好几秒,这并不是我们想要看到的,如果解决这种问题呢?
引入
正是由于阻塞connect可能照成上述问题,所以在实际项目中我们一般使用非阻塞connect来解决这个问题。connect在出错时存在有一种errno值:**EINPROGRESS.**这种错误出现在对非阻塞的socket调用connect,而连接又没有建立时。根据man文档解释,我们可以调用select,poll等函数返后,再利用getsockopt来读取错误码并清除该socket上的错误。如果错误码是0表示建立连接成功,否则连接失败。
使用
- 创建socket,并将 socket 设置成非阻塞模式
- 调用 connect 函数,此时无论 connect 函数是否连接成功会立即返回;如果返回 -1 并不一定表示连接出错,如果此时错误码是EINPROGRESS,则表示正在尝试连接;
- 接着调用 select 函数,在指定的时间内判断该 socket 是否可写,如果可写,使用getsockopt获取错误码,判断错误码是否为0,错误码为0表示连接成功,否则失败
代码实现
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <string.h>
#include <iostream>
#define BUFFER_SIZE 1023
int setnonblocking(int fd)
{
int old_flag = fcntl(fd,F_GETFL);
int new_flag = old_flag|O_NONBLOCK;
fcntl(AF_INET,F_SETFL,new_flag);
return old_flag;
}
int unblock_connect(const char* ip,int port,int time)
{
int ret = 0;
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr.s_addr);
address.sin_port = htons(port);
int sockfd = socket(PF_INET,SOCK_STREAM,0);
int fdopt = setnonblocking(sockfd);
ret = connect(sockfd,(struct sockaddr*)&address,sizeof(address));
if(ret == 0)
{
//连接成功,则恢复sockfd的属性立即返回
printf("connect with server immediately\n");
fcntl(sockfd,F_SETFL,fdopt);
return sockfd;
}
else if(errno != EINPROGRESS)
{
//表示出错
printf("unblock connect not support\n");
return -1;
}
//使用select检测连接的成功或失败
fd_set readfds;
fd_set writefds;
struct timeval timeout;
FD_ZERO(&readfds);
FD_SET(sockfd,&writefds);
timeout.tv_sec = time;
timeout.tv_usec = 0;
ret = select(sockfd+1,NULL,&writefds,NULL,&timeout);
if(ret <= 0)
{
//select超时或者出错,立即返回
printf("connection time out\n");
close(sockfd);
return -1;
}
if(!FD_ISSET(sockfd,&writefds))
{
printf("no events on sockfd found\n");
close(sockfd);
return -1;
}
int error = 0;
socklen_t length = sizeof(error);
//调用getsockopt来获取并清除sockfd上的错误
if(getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&length) < 0)
{
printf("get socket opt failed\n");
close(sockfd);
return -1;
}
//错误号不为0表示连接出错
if(error != 0)
{
printf("connection failed after select with the error:%d\n",error);
close(sockfd);
return -1;
}
//连接成功
printf("connection ready after select with the socket:%d\n",sockfd);
fcntl(sockfd,F_SETFL,fdopt);
return sockfd;
}
int main(int argc,char** argv)
{
if(argc <= 2)
{
printf("usage:%s ip_address port_number\n",basename(argv[1]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int sockfd = unblock_connect(ip,port,10);
if(sockfd < 0)
{
return 1;
}
close(sockfd);
return 0;
}
要点
- 在建立连接成功后,恢复sockfd的属性
- 通过select检测可写事件时,不能以是否有可写事件来判断是否连接成功,因为在Linux系统下,在建立连接前,使用select检测是否有可写事件,会得可写的结果(巨坑啊!!!),所以必须使用errno错误码来判断。
- 使用getsockopt来获取socket上的错误码并清除socket上的错误
- int error = 0;
- getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&length);
- 通过判断错误码error是否为0来确定连接是否成功,error = 0表示连接成功。
非阻塞connect的优点
- 可以把三次握手叠加到其他处理上,既在完成connect的事件内执行其他动作
- 可以同时建立多个连接
- 使用select等待连接可以设置事件限制,缩短connect超时。