凡是接触过socket编程的,对connect函数一定不陌生。因为socket是面向连接的,所以在进行读写操作前我们首先要进行连接,而所谓连接也就是我们常说的三次握手,这个过程就是在connect函数中完成的。
虽然connect函数本身不具备阻塞的功能,但是我们可以通过对socket进行设置和使用select函数可以设置阻塞时间的特性实现非阻塞。
第一,我们可以在connect时去做些别的事,毕竟三次握手需要在网络中往返多层次,我们没有必要一直在那里闲着。
第二,这一点很重要,因为connect的超时时间在75秒到几分钟之间,显然不可能去让程序阻塞那么久。
1. 设置socket
int oldOption = fcntl(sockfd, F_GETFL);
int newOption = oldOption | O_NONBLOCK;
//设置sockfd非阻塞
fcntl(sockfd, F_SETFL, newOption);
2. 执行connect
如果返回0,表示连接成功,这种情况一般在本机上连接时会出现(否则怎么可能那么快)
否则,查看error是否等于EINPROGRESS(表明正在进行连接中),如果不等于,则连接失败
int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == 0)
{
//连接成功
fcntl(sockfd, F_SETFL, oldOption);
return sockfd;
}
else if(errno != EINPROGRESS)
{
//连接没有立即返回,此时errno若不是EINPROGRESS,表明错误
perror("connect error != EINPROGRESS");
return -1;
}
3. 使用select,如果没用过select可以去看看
用select对socket的读写进行监听
那么监听结果有四种可能
1. 可写(当连接成功后,sockfd就会处于可写状态,此时表示连接成功)
2. 可读可写(在出错后,sockfd会处于可读可写状态,但有一种特殊情况见第三条)
3. 可读可写(我们可以想象,在我们connect执行完到select开始监听的这段时间内,
如果连接已经成功,并且服务端发送了数据,那么此时sockfd就是可读可写的,
因此我们需要对这种情况特殊判断)
说白了,在可读可写时,我们需要甄别此时是否已经连接成功,我们采用这种方案:
再次执行connect,然后查看error是否等于EISCONN(表示已经连接到该套接字)。
4. 错误
if(FD_ISSET(sockfd, &writeFds))
{
//可读可写有两种可能,一是连接错误,二是在连接后服务端已有数据传来
if(FD_ISSET(sockfd, &readFds))
{
if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) != 0)
{
int error=0;
socklen_t length = sizeof(errno);
//调用getsockopt来获取并清除sockfd上的错误.
if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0)
{
printf("get socket option failed\n");
close(sockfd);
return -1;
}
if(error != EISCONN)
{
perror("connect error != EISCONN");
close(sockfd);
return -1;
}
}
}
//此时已排除所有错误可能,表明连接成功
fcntl(sockfd, F_SETFL, oldOption);
return sockfd;
}
4. 恢复socket
因为我们只是需要将连接操作变为非阻塞,并不包括读写等,所以我们吃醋要将socket重新设置。
fcntl(sockfd, F_SETFL, oldOption);
到此,我们的非阻塞connect函数已经成功了。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//blokTimeMs表明非阻塞时的毫秒数,若值为-1表明阻塞
int connect(std::string ip, int port, int blockTimeMs = -1)
{
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
//阻塞
if(blockTimeMs == -1)
{
int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("connectBloking");
close(sockfd);
return -1;
}
close(sockfd);
return 1;
}
//非阻塞
int oldOption = fcntl(sockfd, F_GETFL);
int newOption = oldOption | O_NONBLOCK;
//设置sockfd非阻塞
fcntl(sockfd, F_SETFL, newOption);
int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == 0)
{
//连接成功
fcntl(sockfd, F_SETFL, oldOption);
return sockfd;
}
else if(errno != EINPROGRESS)
{
//连接没有立即返回,此时errno若不是EINPROGRESS,表明错误
perror("connect error != EINPROGRESS");
return -1;
}
fd_set readFds;
fd_set writeFds;
struct timeval timeout;
FD_ZERO(&readFds);
FD_ZERO(&writeFds);
FD_SET(sockfd, &writeFds);
FD_SET(sockfd, &readFds);
timeout.tv_sec = blockTimeMs/1000;
timeout.tv_usec = (blockTimeMs%1000)*1000;
ret = select(sockfd+1, &readFds, &writeFds, NULL, &timeout);
if(ret <= 0)
{
perror("select timeout or error");
close(sockfd);
return -1;
}
if(FD_ISSET(sockfd, &writeFds))
{
//可读可写有两种可能,一是连接错误,二是在连接后服务端已有数据传来
if(FD_ISSET(sockfd, &readFds))
{
if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) != 0)
{
int error=0;
socklen_t length = sizeof(errno);
//调用getsockopt来获取并清除sockfd上的错误.
if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0)
{
printf("get socket option failed\n");
close(sockfd);
return -1;
}
if(error != EISCONN)
{
perror("connect error != EISCONN");
close(sockfd);
return -1;
}
}
}
//此时已排除所有错误可能,表明连接成功
fcntl(sockfd, F_SETFL, oldOption);
return sockfd;
}
else
{
perror("connect failed");
close(sockfd);
return -1;
}
}
非阻塞connect
当在一个非阻塞的TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的TCP三路握手将继续前行。我们接着使用select检测这个连接或成功或失败的已建立条件。非阻塞的connect有三个用途。
(1)我们可以把三路握手叠加在其他处理上。完成一个connect要花一个RTT时间,波动范围很大,从局域网的几个毫秒到广域网的几秒。75秒或更长
(2)我们可以使用这个技术同时建立多个连接。这个用途已随着web浏览器变得流行起来,后面给出例子。
(3)使用select等待连接的建立,可以给select指定一个时间限制,使得我们就能够缩短connect的超时。实现方法就是使用非阻塞connect。
使用需要处理一些细节:
- 尽管套接字是非阻塞的,如果连接的服务器是在同一个主机上,即局域网内,那么connect通常会立即连接,我们必须处理这种情形
- 源自Brekeley的实现,有关于select和非阻塞connect的一下两个原则:1、当连接成功建立时,描述符变为可写;2、当连接遇到错误时,描述符变为即可读又可写 (下文select判断退出的依据)
本段代码结构:
1、先非阻塞调用connect
如果 返回值== 0 说明是内网,能够迅速的立即连接上
如果 返回值< 0且 != EINPROGRESS
说明出错返回 -12、调用select永久阻塞,最后一位传参为NULL,即(相当于等待connect 超时返回,最长约75秒到数分钟)
3、FD_ISSET 判断是否有读写
当连接建立成功,描述符变为可写,当连接遇到错误,描述符变为即可读又可写
以下附上代码
#include "unp.h"
int
connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
int flags, n, error;
socklen_t len;
fd_set rset, wset;
struct timeval tval;
flags = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
error = 0;
if ( (n = connect(sockfd, saptr, salen)) < 0)
if (errno != EINPROGRESS)
return(-1);
/* Do whatever we want while the connect is taking place. */
// 连接成功,服务端与客户处于同一台主机下可立即发生
if (n == 0)
goto done; /* connect completed immediately */
FD_ZERO(&rset);
FD_SET(sockfd, &rset);
wset = rset;
tval.tv_sec = nsec;
tval.tv_usec = 0;
if ( (n = Select(sockfd+1, &rset, &wset, NULL,
nsec ? &tval : NULL)) == 0) {
close(sockfd); /* timeout */ // 1、此处的超时,是设置了select的 定时参数的超时
errno = ETIMEDOUT;
return(-1);
}
if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
len = sizeof(error);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) // 2、得到error值,若连接成功,error = 0,若失败 error = 错误值,然后再下文if(error)做判断,退出
return(-1); /* Solaris pending error */
} else
err_quit("select error: sockfd not set");
done:
Fcntl(sockfd, F_SETFL, flags); /* restore file status flags */
if (error) {
close(sockfd); /* just in case */
errno = error;
return(-1);
}
return(0);
}
其中有两点需要注意:
if ( (n = Select(sockfd+1, &rset, &wset, NULL,
nsec ? &tval : NULL)) == 0) {
close(sockfd); /* timeout */ // 1、此处的超时,是设置了select的 定时参数的超时
errno = ETIMEDOUT;
return(-1);
}
这个超时是设置了select 定时参数之后的超时,若timeval值传参为NULL,则select永久阻塞,直到对端服务发来
select函数返回的依据:
1、当连接建立成功后,描述符变为可写
2、RST :表示复位,说明对端主机没有指定端口的服务等待与之连接,这是一种硬错误,客户一收到,立即返回ECONNREFUSED错误 。
产生RST有三种情况:
1、目的地为某端口的SYN到达,然而该端口没有正在监听的服务器。
2、TCP想取消一个已有连接。
3、TCP接收到一个根本不存在的连接上的分节。 (TCPv1第246~250有更详细的介绍)
3、当某个套接字上发生错误时,它将由select标记为即可读又可写
有个问题:
当select永久阻塞时,对端一直超时,也没任何响应分节到达描述符,那么select怎么退出的,是根据以上第3点,描述符发生错误吗?
第二点需要注意:
if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
len = sizeof(error);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) // 2、得到error值,若连接成功,error = 0,若失败 error = 错误值,然后再下文if(error)做判断,退出
return(-1); /* Solaris pending error */
} else
err_quit("select error: sockfd not set");
如果描述符变为可读或可写,我们就调用getsockopt 取得套接字的待处理错误(使用SO_ERROR套接字选项)
1、如果成功建立连接,error = 0
2、如果连接发生错误,error = 对应的错误error值(ECONNREFUSED(说明对端发来RST)、ETIMEDOUT)等 ,
这个ETIMEDOUT超时,是connect()函数超时之后返回的
3、移植性问题:Berkeley 实现 :error 返回待处理错误值,getsockopt 本身返回0;
Solaris实现: errno返回错误值, getsockopt 返回 -1;
但是本程序可以同时兼容处理以上两种移植性。
1、connect失败后,返回值判断是否为 EINTR ()
2、调用poll 或者select 阻塞或者定时检测sockfd 事件发生
3、getsockopt() 取得 SO_ERROR 中错误码, 若ret == 0 , 则连接成功 ;否则 连接失败
poll:
int CheckConnectIsOn(int iSocket) {
struct pollfd fd;
int ret = 0;
socklen_t len = 0;
fd.fd = iSocket;
fd.events = POLLOUT;
while ( poll (&fd, 1, -1) == -1 ) {
if( errno != EINTR ){
perror("poll");
return -1;
}
}
len = sizeof(ret);
if ( getsockopt (iSocket, SOL_SOCKET, SO_ERROR, &ret, &len) == -1 ) {
perror("getsockopt");
return -1;
}
if(ret != 0) {
fprintf (stderr, "socket %d connect failed: %s\n",
iSocket, strerror (ret));
return -1;
}
return 0;
}
调用时 如下:
if(connnect()) {
if(errno == EINTR) {
if(CheckConnectIsOn() < 0) {
perror();
return -1;
}
else {
printf("connect is success!\n");
}
}
else {
perror("connect");
return -1;
}
}