对于面向连接的socket类型(SOCK_STREAM、SOCK_SEQPACKET、SOCK_RDM),在通信双方读写数据之前都需要先建立连接,connect()函数用于完成面向连接的socket的连接建立过程。而connect建立连接的模式可以分为阻塞和非阻塞两种,默认情况是阻塞模式。
多数实现中,connect的超时时间在75秒到几分钟之间。有时程序希望在等待一定时间内结束,使用非阻塞connect可以防止阻塞75秒。在多线程网络编程中,非阻塞就显得尤其必要。 例如,有一个通过建立线程与其他主机进行socket通信的应用程序,如果建立的线程使用阻塞connect与远程进行通信,当有几百个线程并发的时候,由于网络延迟而全部阻塞,阻塞的线程不会释放系统的资源,同一时刻阻塞线程超过一定数量时候,系统就不再允许建立新的线程(每个进程由于进程空间的原因能产生的线程有限),如果使用非阻塞的connect,连接失败时,使用select只需等待很短的时间,如果还没有连接成功,线程立刻结束释放资源,防止大量线程阻塞而使程序崩溃。
CONNECT(2) Linux Programmer’s Manual CONNECT(2)
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include /* See NOTES */
#include
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
ERRORS
The following are general socket errors only. There may be other domain-specific error codes.
EINPROGRESS
The socket is non-blocking and the connection cannot be completed immediately. It is possible to select(2) or poll(2) for completion by selecting
the socket for writing. After select(2) indicates writability, use getsockopt(2) to read the SO_ERROR option at level SOL_SOCKET to determine
whether connect() completed successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the usual error codes listed here, explaining the
reason for the failure).
EISCONN
The socket is already connected.
通过man命令,可以看到connect返回的错误码中有一个是EINPROGRESS,对应的描述大致是:在非阻塞模式下,如果连接不能马上建立成功就会返回该错误码。如果返回该错误码,可以通过使用select或者poll来查看套接字是否可写,如果可写,再调用getsockopt来获取套接字层的错误码来确定连接是否真的建立成功,如果getsockopt返回的错误码是0则表示连接真的来建立成功,否则返回对应失败的错误码。
默认情况下connect是阻塞模式的,这里不细说,重点说一下非阻塞模式的实现。套接字执行I/O操作也有阻塞和非阻塞两种模式,在阻塞模式下,在I/O操作完成之前,执行操作的函数会一直阻塞在这里。而在非阻塞模式下,套接字函数不管/IO操作是否完成都会理解返回并继续运行该线程。
客户端调用connect发起对服务端socket的连接,如果客户端是默认的阻塞模式,则connect()函数会一直阻塞,直到与服务端成功建立连接或者超时(75秒到几分钟)。如果是非阻塞模式,则调用connect()函数后会立即返回EINPROGRESS,此时与服务端的连接仍在进行,后续再通过man命令中介绍的办法来检测连接是否建立成功。这里说一下man命令中描述的调用select()检测非阻塞connect是否完成,因为select()指定的超时时间可以比connect()的超时时间短并且可以配置,而connect()超时时间在linux内核中配置,无法修改。因此可以缩短阻塞时间。
select()检测的判断原则:
因此,当发现套接字描述符是可读或可写时,需要进一步判断是连接是成功还是出错。这里必须将上述B和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时select()返回的非阻塞socket描述符同样是既可读又可写的。此时可通过调用getsockopt()来检测描述符集合是连接成功还是出错:
a. 如果连接建立是成功的,则通过getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len) 获取的error 值将是0。
b. 如果建立连接时遇到错误,则errno 的值是连接错误所对应的errno值,比如ECONNREFUSED,ETIMEDOUT 等。
还可以使用另外一种比较简单的方法来进行判断。我们注意到man命令中列出的connect返回错误码中还有一个是EISCONN,表示连接已经建立。所以我们可以再一次调用connect()函数,如果返回的错误码是EISCONN,则表示连接建立成功,否则认为连接建立失败。
int sock = socket(AF_INET, SOCK_STREAM, 0);
int flags = fcntl(sock , F_GETFL, 0);
fcntl(sock , F_SETFL, flags|O_NONBLOCK);
int imode = 1;
ioctl(sock , FIONBIO, &imode);
/* 最后我会介绍一下fcntl和ioctl系统调用 */
struct sockaddr_in server_socket;
memset(&server_socket, 0, sizeof(server_socket));
server_socket.sin_family = AF_INET;
inet_pton(AF_INET, SERVER_IP, &server_socket.sin_addr);
server_socket.sin_port = htons(SERVER_PORT);
int conn = connect(sock, (struct sockaddr *)&server_socket, sizeof(server_socket));
if (conn == 0) {
printf("Socket Connect Success Immediately.\n");
}
else {
printf("Get The Connect Result by select().\n");
if (errno == EINPROGRESS)
{
....
}
}
//connect会立即返回,可能返回成功,也可能返回失败。如果连接的服务器在同一台主机上,那么在调用connect 建立连接时,连接通常会立即建立成功。
4.调用select(),通过FD_ISSET()检查套接口是否可写,确定连接请求是否完成
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)){
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");
/*
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;
}
*/
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;
}
}else{
printf("haha\n");
}
}
if (-1 != selres && (ret != 0)){
printf("check connect result again... %d\n", times);
continue;
}else{
break;
}
}
fcntl定义函数:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
//fcntl()针对(文件)描述符提供控制.参数fd 是被参数cmd操作(如下面的描述)的描述符。
//针对cmd的值,fcntl能够接受第三个参数int arg
ioctl函数影响由fd 参数引用的一个打开的文件。
函数定义:
NAME
ioctl - control device
SYNOPSIS
#include
int ioctl(int d, int request, ...);
ERRORS
EBADF d is not a valid descriptor.
EFAULT argp references an inaccessible memory area.
EINVAL Request or argp is not valid.
ENOTTY d is not associated with a character special device.
ENOTTY The specified request does not apply to the kind of object that the descriptor d references.
第三个参数总是一个指针,但指针的类型依赖于request 参数。我们可以把和网络相关的请求划分为6 类:
下表列出了网络相关ioctl 请求的request 参数以及arg 地址必须指向的数据类型:
套接口操作:
明确用于套接口操作的ioctl 请求有三个, 它们都要求ioctl 的第三个参数是指向某个整数的一个指针。
其他类型请参考:ioctl函数详细说明(网络)