您现在的位置是:网站首页 -> 底层开发 文章内容
Linux socket非阻塞connect方法-itarticl.cc-IT技术类文章记录&分享
发布时间: 9年前【底层开发】 185人已围观【返回】
socket non-blocking mode connect
对于面向连接的socket类型(SOCK_STREAM,SOCK_SEQPACKET),在读写数据之前必须建立连接,connect()函数用于完成面向连接的socket的建链过程,对于TCP,也就是三次握手过程。
connect()函数
头文件:
#include<sys/types.h>
#include<sys/socket.h>
声明:
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
功能:
使用套接字sockfd建立到指定网络地址serv_addr的socket连接,参数addrlen为serv_addr指向的内存空间大小,即sizeof(struct sockaddr_in)。
返回值:
1)成功返回0,表示连接建立成功(如服务器和客户端是同一台机器上的两个进程时,会发生这种情况)
2)失败返回SOCKET_ERROR,相应的设置errno,通过errno获取错误信息。常见的错误有对方主机不可达或者超时错误,也可能是对方主机没有进程监听对应的端口。
非阻塞connect(non-block mode connect)
套接字执行I/O操作有阻塞和非阻塞两种模式。在阻塞模式下,在I/O操作完成前,执行操作的函数一直等候而不会立即返回,该函数所在的线程会阻塞在这里。相反,在非阻塞模式下,套接字函数会立即返回,而不管I/O是否完成,该函数所在的线程会继续运行。
客户端调用connect()发起对服务端的socket连接,如果客户端的socket描述符为阻塞模式,则connect()会阻塞到连接建立成功或连接建立超时(linux内核中对connect的超时时间限制是75s, Soliris 9是几分钟,因此通常认为是75s到几分钟不等)。如果为非阻塞模式,则调用connect()后函数立即返回,如果连接不能马上建立成功(返回-1),则errno设置为EINPROGRESS,此时TCP三次握手仍在继续。此时可以调用select()检测非阻塞connect是否完成。select指定的超时时间可以比connect的超时时间短,因此可以防止连接线程长时间阻塞在connect处。
select判断规则:
1)如果select()返回0,表示在select()超时,超时时间内未能成功建立连接,也可以再次执行select()进行检测,如若多次超时,需返回超时错误给用户。
2)如果select()返回大于0的值,则说明检测到可读或可写的套接字描述符。源自 Berkeley 的实现有两条与 select 和非阻塞 I/O 相关的规则:
A) 当连接建立成功时,套接口描述符变成 可写(连接建立时,写缓冲区空闲,所以可写)
B) 当连接建立出错时,套接口描述符变成 既可读又可写(由于有未决的错误,从而可读又可写)
因此,当发现套接口描述符可读或可写时,可进一步判断是连接成功还是出错。这里必须将B)和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时select同样会返回非阻塞socket描述符既可读又可写。
□对于Unix环境,可通过调用getsockopt来检测描述符集合是连接成功还是出错(此为《Unix Network Programming》一书中提供的方法,该方法在Linux环境上测试,发现是无效的):
A)如果连接建立是成功的,则通过getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len) 获取的error 值将是0
B)如果建立连接时遇到错误,则errno 的值是连接错误所对应的errno值,比如ECONNREFUSED,ETIMEDOUT 等
□一种更有效的判断方法,经测试验证,在Linux环境下是有效的:
再次调用connect,相应返回失败,如果错误errno是EISCONN,表示socket连接已经建立,否则认为连接失败。
综上所述,这里总结一下非阻塞connect的实现过程。
非阻塞connect的实现过程
1. 创建套接字sockfd
/* 1. obtain a socket */
int sock_fd;
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
2. 设置套接字为非阻塞模式
/* 2. set non-blocking mode no socket */
#if 1
int flags = fcntl(sock_fd, F_GETFL, 0);
fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK);
#else
int imode = 1;
ioctl(sock_fd, FIONBIO, &imode);
3. 调用connect进行连接
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(PEER_PORT);
addr.sin_addr.s_addr = inet_addr(PEER_IP);
int ret = connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr));
if (0 == res)
{
printf("socket connect succeed immediately.\n");
ret = 0;
}
else
{
printf("get the connect result by select().\n");
if (errno == EINPROGRESS)
{
....
}
}
connect会立即返回,可能返回成功,也可能返回失败。如果连接的服务器在同一台主机上,那么在调用connect 建立连接时,连接通常会立即建立成功(我们必须处理这种情况)。
4.调用select(),通过FD_ISSET()检查套接口是否可写,确定连接请求是否完成
fd_set rfds, wfds;
struct timeval tv;
FD_ZERO(&rfds);FD_ZERO(&wfds);
FD_SET(sock_fd, &rfds);
FD_SET(sock_fd, &wfds);
/* set select() time out */
tv.tv_sec = 10;
tv.tv_usec = 0;
int selres = select(sock_fd + 1, &rfds, &wfds, NULL, &tv);
switch (selres)
{
case -1:
printf("select error\n");
ret = -1;
break;
case 0:
printf("select time out\n");
ret = -1;
break;
default:
if (FD_ISSET(sock_fd, &rfds) || FD_ISSET(sock_fd, &wfds))
{
.....
}
}
对于无连接的socket类型(SOCK_DGRAM),客户端也可以调用connect进行连接,此连接实际上并不建立类似SOCK_STREAM的连接,而仅仅是在本地保存了对端的地址,这样后续的读写操作可以默认以连接的对端为操作对象。
Linux下常见的socket错误码:
EACCES, EPERM:用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败。
EADDRINUSE 98:Address already in use(本地地址处于使用状态)
EAFNOSUPPORT 97:Address family not supported by protocol(参数serv_add中的地址非合法地址)
EAGAIN:没有足够空闲的本地端口。
EALREADY 114:Operation already in progress(套接字为非阻塞套接字,并且原来的连接请求还未完成)
EBADF 77:File descriptor in bad state(非法的文件描述符)
ECONNREFUSED 111:Connection refused(远程地址并没有处于监听状态)
EFAULT:指向套接字结构体的地址非法。
EINPROGRESS 115:Operation now in progress(套接字为非阻塞套接字,且连接请求没有立即完成)
EINTR:系统调用的执行由于捕获中断而中止。
EISCONN 106:Transport endpoint is already connected(已经连接到该套接字)
ENETUNREACH 101:Network is unreachable(网络不可到达)
ENOTSOCK 88:Socket operation on non-socket(文件描述符不与套接字相关)
ETIMEDOUT 110:Connection timed out(连接超时)
测试代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include<sys/ioctl.h>
//inet_addr()
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PEER_IP "192.254.1.1"
#define PEER_PORT 7008
int main(int argc, char **argv)
{
int ret = 0;
int sock_fd;
int flags;
struct sockaddr_in addr;
/* obtain a socket */
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
/* set non-blocking mode on socket*/
#if 1
flags = fcntl(sock_fd, F_GETFL, 0);
fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK);
#else
int imode = 1;
ioctl(sock_fd, FIONBIO, &imode);
#endif
/* connect to server */
addr.sin_family = AF_INET;
addr.sin_port = htons(PEER_PORT);
addr.sin_addr.s_addr = inet_addr(PEER_IP);
int res = connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if (0 == res)
{
printf("socket connect succeed immediately.\n");
ret = 0;
}
else
{
printf("get the connect result by select().\n");
if (errno == EINPROGRESS)
{
int times = 0;
while (times++ < 5)
{
fd_set rfds, wfds;
struct timeval tv;
printf("errno = %d\n", errno);
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(sock_fd, &rfds);
FD_SET(sock_fd, &wfds);
/* set select() time out */
tv.tv_sec = 10;
tv.tv_usec = 0;
int selres = select(sock_fd + 1, &rfds, &wfds, NULL, &tv);
switch (selres)
{
case -1:
printf("select error\n");
ret = -1;
break;
case 0:
printf("select time out\n");
ret = -1;
break;
default:
if (FD_ISSET(sock_fd, &rfds) || FD_ISSET(sock_fd, &wfds))
{
#if 0 // not useable in linux environment, suggested in <<Unix network programming>>
int errinfo, errlen;
if (-1 == getsockopt(sock_fd, SOL_SOCKET, SO_ERROR, &errinfo, &errlen))
{
printf("getsockopt return -1.\n");
ret = -1;
break;
}
else if (0 != errinfo)
{
printf("getsockopt return errinfo = %d.\n", errinfo);
ret = -1;
break;
}
ret = 0;
printf("connect ok?\n");
#else
#if 1
connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
int err = errno;
if (err == EISCONN)
{
printf("connect finished 111.\n");
ret = 0;
}
else
{
printf("connect failed. errno = %d\n", errno);
printf("FD_ISSET(sock_fd, &rfds): %d\n FD_ISSET(sock_fd, &wfds): %d\n", FD_ISSET(sock_fd, &rfds) , FD_ISSET(sock_fd, &wfds));
ret = errno;
}
#else
char buff[2];
if (read(sock_fd, buff, 0) < 0)
{
printf("connect failed. errno = %d\n", errno);
ret = errno;
}
else
{
printf("connect finished.\n");
ret = 0;
}
#endif
#endif
}
else
{
printf("haha\n");
}
}
if (-1 != selres && (ret != 0))
{
printf("check connect result again... %d\n", times);
continue;
}
else
{
break;
}
}
}
else
{
printf("connect to host %s:%d failed.\n", PEER_IP, PEER_PORT);
ret = errno;
}
}
if (0 == ret)
{
send(sock_fd, "12345", sizeof("12345"), 0);
}
else
{
printf("connect to host %s:%d failed.\n", PEER_IP, PEER_PORT);
}
close(sock_fd);
return ret;
}
问题总结:
1. 非阻塞socket可调用fcntl或ioctl设置
2.connect的返回值
非阻塞connect返回-1,并不一定是连接失败,可能是连接过程未完成,此时errno为EINPROGRESS,可通过select检查连接何时完成
3. select的返回值
1)-1,表示select出错,可以关闭socket,重新发起连接过程
2)0,表示select超时,此时可能connect还在进行中,可再次进行select,反复数次仍超时,可认为连接失败,需返回失败错误
3)1,表示存在套接口描述字可读或可写,需根据规则进一步判断是连接成功还是出错
4. 通过socket可读/可写能否判断连接状态
源自 Berkeley 的实现有两条与 select 和非阻塞 I/O 相关的规则:
A) 当连接建立成功时,套接口描述符变成 可写(连接建立时,写缓冲区空闲,所以可写)
B) 当连接建立出错时,套接口描述符变成 既可读又可写(由于有未决的错误,从而可读又可写)
当发现套接口描述符可读或可写时,可进一步判断是连接成功还是出错。这里必须将B)和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时select同样会返回非阻塞socket描述符既可读又可写。
因此,仅从socket可读或可写无法判断socket连接的状态。
5. 如何有效判断连接状态:
1)getsockopt方法在linux环境下无效
2)再次执行connect,检查errno值,该方法在Linux环境下有效。测试中发现的问题:
一次select之后,发现此时套接口描述字可读或可写,再次执行connect,此时errno始终不变,仍未EINPROGRESS,增加select的超时时间结果也一样。
之后尝试在select返回值为0,或返回值为1,且connect后errno仍为EINPROGRESS(115)时,再次执行select+connect,即再次检测连接状态。此时errno被置为EISCONN(106),connect成功。
还没有搞清楚原因。
7. socket连接成功后是否重置为阻塞模式
这要看连接需要什么样的效果。
如果连接建立成功后,重置socket为阻塞模式。在给服务器发送信息等待接收数据时,如果服务器很忙,而服务器也没设计好,每到来一个客户端就服务,那么后来的要排队,客户端多的话,会导致后来的请求长时间得不到应答,线程一直被阻塞。
然而,在非阻塞模式下,send和recv也会立即返回。测试发现,非阻塞模式下,recv常常未能接收到数据,返回错误。而在建链之后将socket重置为阻塞,recv的接收正常。
8. socket使用完成后要进行释放
close(sock_fd)
9. 具有非阻塞属性的接口
发布时间: 9年前【底层开发】185人已围观【返回】【回到顶端】
很赞哦! (1)
相关文章
点击排行

站长推荐

猜你喜欢
站点信息
- 建站时间:2016-04-01
- 文章统计:728条
- 文章评论:82条
- QQ群二维码:扫描二维码,互相交流
