赞
踩
在网络编程中,对套接字的I/O的系统调用(如read
,write
,connect
)进行超时处理是至关重要的,特别是在需要响应及时的实时数据或避免无限期阻塞的情境下。本文将深入介绍处理套接字I/O超时的两种方法:setsockopt
和select
。setsockopt
允许直接设置套接字的发送和接收超时时间,而select
提供了一种多路复用的机制,使得在等待多个套接字就绪时能够设置超时。
SO_SNDTIMEO
和SO_RCVTIMEO
是与套接字选项相关的两个选项,它们可以用于设置发送和接收数据的超时时间。这两个选项主要用于在套接字上设置超时,以便在指定的时间内等待发送或接收操作完成。
SO_SNDTIMEO
(发送超时)
SO_SNDTIMEO
用于设置发送数据的超时时间。通过这个选项,你可以指定在发送数据时等待的最长时间。如果在指定的时间内无法完成发送操作,系统将会返回一个错误。
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
setsockopt(socket_descriptor, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
SO_RCVTIMEO
(接收超时)
SO_RCVTIMEO
用于设置接收数据的超时时间。通过这个选项,可以指定在接收数据时等待的最长时间。如果在指定的时间内未接收到数据,系统将会返回一个错误。
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
setsockopt(socket_descriptor, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
返回值
当超时时间到达后,read
/recv
/write
/send
等函数将返回-1,同时errno.h
中的全局变量errno
将置为EWOULDBLOCK
。
例子
下面是一个简单的服务端/客户端的例子,如果3秒内没有收到数据则recv
会立即返回。
(1)服务端
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <errno.h> int main() { int server_socket, client_socket; struct sockaddr_in server_addr, client_addr; // 创建服务器套接字 server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket == -1) { perror("socket"); exit(EXIT_FAILURE); } // 设置服务器地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(8080); // 绑定服务器套接字 if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("bind"); exit(EXIT_FAILURE); } // 监听连接 if (listen(server_socket, 1) == -1) { perror("listen"); exit(EXIT_FAILURE); } printf("Server listening on port 8080...\n"); socklen_t client_addr_len = sizeof(client_addr); client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len); if (client_socket == -1) { perror("accept"); exit(EXIT_FAILURE); } printf("Client connected...\n"); // 设置发送和接收超时为3秒 struct timeval tv; tv.tv_sec = 3; tv.tv_usec = 0; setsockopt(client_socket, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); char buffer[1024]; ssize_t bytesRead, bytesWritten; // 尝试接收数据 bytesRead = recv(client_socket, buffer, sizeof(buffer), 0); if (bytesRead == -1) { //perror("recv"); if (errno == EWOULDBLOCK) { // 超时,需要进行适当的处理 printf("timeout\n"); } else { perror("recv"); // 其他错误处理 } } else if (bytesRead == 0) { printf("Connection closed by client.\n"); } else { buffer[bytesRead] = '\0'; printf("Received: %s\n", buffer); // 尝试发送数据 const char *response = "Hello, Client!"; bytesWritten = write(client_socket, response, strlen(response)); if (bytesWritten == -1) { perror("write"); } else { printf("Sent: %s\n", response); } } // 关闭套接字 close(client_socket); close(server_socket); return 0; }
(2)客户端
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main() { int client_socket; struct sockaddr_in server_addr; // 创建客户端套接字 client_socket = socket(AF_INET, SOCK_STREAM, 0); if (client_socket == -1) { perror("socket"); exit(EXIT_FAILURE); } // 设置服务器地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器地址 server_addr.sin_port = htons(8080); // 连接到服务器 if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("connect"); exit(EXIT_FAILURE); } printf("Connected to server...\n"); while(1);//客户端:不发任何消息,等待服务端recv超时 return 0; }
在上一节IO复用模型之select原理及例子中我们介绍了select
函数的使用,其中最后一个字段可以设置超时时间。select
相比setsockopt
更常用,所以这里重点介绍这个方法。先回忆一下select
的原型:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
返回值:
大于0: 表示有一个或多个文件描述符就绪(可读、可写或出错)。
等于0: 表示在指定的超时时间内没有文件描述符就绪。
select
函数的超时参数为 NULL
,它可能会一直等待,直到有文件描述符就绪或出错。等于-1: 表示出现错误。
errno
变量获取具体的错误信息。在Linux中errno
可能返回EINTR
:
EINTR
是在系统调用(select
是一个系统调用)被信号中断时返回的错误码,当一个信号(例如SIGINT
、SIGTERM
)被发送给进程,并且进程正在执行一个系统调用时,该系统调用可能会被中断,返回 EINTR
错误。
下面使用select
来封装一下读、写和连接操作的超时处理流程。
1、读操作
下面是一个监听读描述符fd
的例子,如果三秒没有数据到来,则select
将返回0。同时我们还要判断EINTR
的返回。
fd_set readfds; FD_ZERO(&readfds); FD_SET(fd, &readfds); struct timeval timeout; timeout.tv_sec = 3; timeout.tv_usec = 0; do { ret = select(fd + 1, &readfds, NULL, NULL, &timeout); } while (ret < 0 && errno == EINTR); //如果是中断信号则继续select if (ready == -1) { // 出错 } else if (ready == 0) { // 超时 } else { // 文件描述符可读 if (FD_ISSET(fd, &readfds)){...} }
2、写操作
写操作和读操作类似,实现如下:
fd_set writefds; FD_ZERO(&writefds); FD_SET(fd, &writefds); struct timeval timeout; timeout.tv_sec = 3; timeout.tv_usec = 0; do { ret = select(fd + 1, NULL, &writefds, NULL, &timeout); } while (ret < 0 && errno == EINTR); //如果是中断信号则继续select if (ready == -1) { // 出错 } else if (ready == 0) { // 超时 } else { // 文件描述符可读 if (FD_ISSET(fd, &writefds)){...} }
但在实际使用过程中,我一般将写描述符监听的超时时间设置为0,select
的返回值可以判断当前内核是否有资源可以处理写操作,比如当前内存不足了,select
将返回0。
对于系统调用connect
,在网线没插或者对端没有在listen
等情况下,connect
函数可能会阻塞几十秒才返回。所以我们很有必要设置connect
函数为非阻塞。
1、设置文件描述符为非阻塞
实现这个功能需要用到fcntl
函数,它可以用来改变已打开文件描述符的属性:
int fcntl(int fd, int cmd, ... /* arg */);
其中参数说明如下:
F_DUPFD
: 复制文件描述符F_GETFD
: 获取文件描述符标志F_SETFD
: 设置文件描述符标志F_GETFL
: 获取文件状态标志F_SETFL
: 设置文件状态标志F_GETOWN
: 获取异步I/O进程ID或套接字拥有者F_SETOWN
: 设置异步I/O进程ID或套接字拥有者我们可以使用操作命令F_SETFL
设置文件描述符的非阻塞(O_NONBLOCK
)属性来让connect
函数变为非阻塞,现在我们就可以封装两个函数:设置非阻塞模式的函数setNonBlocking
和设置阻塞模式的函数setBlocking
// 将文件描述符设置为非阻塞模式 int setNonBlocking(int sockfd) { int flags = fcntl(sockfd, F_GETFL, 0); if (flags == -1) { perror("fcntl"); return -1; } if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) { perror("fcntl"); return -1; } return 0; } // 将文件描述符设置为阻塞模式 int setBlocking(int sockfd) { int flags = fcntl(sockfd, F_GETFL, 0); if (flags == -1) { perror("fcntl"); return -1; } if (fcntl(sockfd, F_SETFL, flags & ~O_NONBLOCK) == -1) { perror("fcntl"); return -1; } return 0; }
2、connect函数的封装
假设有一个待连接的套接字sockfd
,然后我们将connect
函数设置为非阻塞,整体的代码流程如下:
(1)将套接字设置为非阻塞模式
if (setNonBlocking(sockfd) == -1) {
close(sockfd);
return 1;
}
(2)调用connect函数
在连接成功的情况下,connect
函数将返回0。但前面设置了套接字为非阻塞,所以这里connect
函数将立即返回,而大概率是不可能在这么一瞬间建立连接的,所以connect
这里会返回-1(这里就不判断返回0的情况了),然后置errno
全局变量为EINPROGRESS
,表示正在连接中。
struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8080); serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) { if (errno == EINPROGRESS) { // 连接正在进行中,可以通过select/poll/epoll来检查连接状态,下面用select来判断 printf("Connect in progress...\n"); } else { perror("connect"); close(sockfd); return 1; } }
(3)使用select判断连接是否建立成功
当连接成功建立后,套接字将可写,所以我们可以使用select
来监听写描述符,并使用timeout
超时字段来设置超时时间:
select
返回1的时候并不一定表示连接已经建立成功,如果检查套接字的错误状态(使用 getsockopt
函数和SO_ERROR
选项)发现没有错误,才表示连接成功建立int ret; fd_set wset; struct timeval tv = {.tv_sec = 5,.tv_usec = 0}; FD_ZERO(&wset); FD_SET(sockfd, &wset); do { ret = select(sockfd + 1, NULL, &wset, NULL, &tv); } while (ret < 0 && errno == EINTR); if(ret == 0) { //连接超时:在超时时间内没有连接上 return 1; }else if(ret < 0) { //可以查看errno看发生了什么错误,一般是内核的问题 }else if(ret == 1) { //检测到可写 int error, n; socklen_t len = sizeof(error); n = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len); if(n == 0) { //理论上此时已经建立连接成功,但实际发现无论网络是否正常都会返回0,所以继续使用getsockname判断 struct sockaddr_in clientAddr; socklen_t clientAddrLen; n = getsockname(sockfd, (struct sockaddr*)&clientAddr, &clientAddrLen); if(n == 0) { //此时建立连接成功 return 0; } }else { //建立连接失败 return 1; } }
(4)恢复阻塞模式
在connect
完毕后,需要恢复原来的设置:
// 恢复套接字为阻塞模式
if (setBlocking(sockfd) == -1) {
close(sockfd);
return 1;
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。