我们先来讲connect函数(本文针对的是TCP)
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);
参数大家应该都知道,我在这里提一下,为什么第二个参数要用 struct sockaddr*?
- 这是因为ANSI C标准为我们定义了一个标准套接字的结构体,就是 struct sockaddr, 我们在之前声明的结构如 ipv4中struct sockaddr_in,它们的结构体长度是相同的,证明在强制转换的过程之前定义的结构体中数据没有丢失。如果我们不强制转换,编译器就会报错。
一般调用connect函数的都是客户端,客户端在调用connect之前不必非得调用bind函数进行地址绑定,因为一般内核会确定源IP地址,并选择一个临时端口作为源端口。
调用connect函数后将会激发TCP的三路握手过程
connect函数只在连接建立成功或者出错时返回,其中出错返回可能有以下几种情况
- TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT。(内核不会立即返回,而是会在固定的时间内多次发送SYN分节,如果都没有响应,则会返回错误)。
- 如果服务端对客户的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接,客户一接到RST就会立即返回ECONNREFUSED错误。
RST是TCP发生错误时发送的一种分节。产生RST的三个条件:
1、目的地为某端口的SYN到达,然而该端口没有正在监听的服务器。(这就是上面描述的情况)
2、TCP想取消一个已有连接
3、TCP接收到一个根本不存在的连接上的分节。
-若客户发出的SYN分节在中间的某个路由器上引发了一个"destination unreachable"(目的地不可达)ICMP错误,则认为是一种软错误(soft error)。客户主机内核保存该消息,并按第一种情况所述的时间间隔继续发送SYN。若在某个规定时间后仍未收到响应,则把保存的消息(ICMP错误)作为EHOSTUNREACH或ENETUREACH错误返回给进程。
上文讲了这么多,那到底为什么要用非阻塞connect
那先来说说阻塞connect。在用阻塞模式的connect时,如果出现了上文中第一个错误,connect迟迟没有完成连接,用户程序就会阻塞在connect函数,一般的connect的超时时间在75秒到几分钟之间,那用户程序就需要等待这么久才能返回接着运行,这样就降低了效率。
如果使用非阻塞的connect,如果连接没有立即建立,就要判断它的错误码,我们期望的是EINPROGRESS,表示连接建立已经启动但是尚未完成。这个时候我们就要将套接字加入到select的可写事件集中,然后我们设置一个时间,这个时间就是我们想要等待连接再进行多久的时间,如果这个时间段内select返回并且套接字可写,这个时候connect也不一定成功,我们还要接着判断,因为在POSIX中套接字出错也是会变成即可读又可写的,所以我们还需要用getsockopt函数来获取错误码,如果错误码为0则表示connect成功,否则就失败。
看到这大家也应该能了解到非阻塞connect的优点了。那就是我们可以自己定义connect调用的时间,而不是阻塞的去等待系统规定的时长。
下面就是我们非阻塞connect实现的代码
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<iostream>
#include<error.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<sys/epoll.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<signal.h>
#include<sys/select.h>
int main(int argc, char* argv[])
{
fd_set rset, wset;
struct timeval tval;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int flag = fcntl(sockfd, F_GETFL);
//将套接字设置为非阻塞
fcntl(sockfd, F_SETFL, flag | O_NONBLOCK);
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(atoi(argv[2]));
client_addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sockfd, (sockaddr*)&client_addr, sizeof(client_addr));
if(ret == 0)
std::cout << "socket connect succeed immdiately\n";
else
{
std::cout << "get the connect result by select(). \n";
if(errno != EINPROGRESS)
return -1; //连接失败
else if(errno == EINPROGRESS)
{
FD_ZERO(&rset);
FD_SET(sockfd, &rset);
wset = rset;
tval.tv_sec = 2;
tval.tv_usec = 0;
ret = select(sockfd + 1, &rset, &wset, NULL, &tval);
if(ret < 0)
{
perror("select erro\n");
exit(-1);
}
if(ret == 0)
{
//超过timeval的时间还没有连接成功
close(sockfd);
errno = ETIMEDOUT;
exit(errno);
}
else if(FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset))
{
int error = 0;
socklen_t len = sizeof(error);
//如果error为0则表明connect成功连接,关闭其在select中写描述符集对应的位。
ret = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
if(ret < 0)
{
close(sockfd);
return -1;
}
if(error != 0)
{
perror("connect failed\n");
close(sockfd);
exit(-1);
}
else
{
std::cout << "connect success\n";
}
}
}
}
//连接成功后设置为阻塞模式
fcntl(sockfd, F_SETFL, flag);
/*
...
*/
close(sockfd);
}
在我的机器上跑得结果如下所示:
- 首先,在调用select之前有可能连接已经建立并有来自对端的数据到达。这种情况下即使套接字不发生错误,套接字也是既可读又可写,这和连接建立失败情况下套接字的读条件一样。如上程序中,代码通过调用getsockopt并检查套接上是否存在待处理错误来处理这种情形。
还有什么方法能判断连接是否成功呢?
- 调用getpeername代替getsockopt。如果getpeername以ENOTCONN错误失败返回,那么连接建立已经失败,我们必须接着以SO_ERROR调用getsockopt取得套接字上待处理的错误。
- 以值为0的长度参数调用read。如果read失败,那么connect已经失败,read返回的errno给出了连接失败的原因。如果连接建立成功,那么read应该返0。
- 在调用connect一次。它应该失败,如果错误是EISCONN,那么套接字已经连接,也就是说第一次连接已经成功。