Linux socket编程(7):I/O系统调用(读/写/连接)的超时处理

在网络编程中,对套接字的I/O的系统调用(如read,write,connect)进行超时处理是至关重要的,特别是在需要响应及时的实时数据或避免无限期阻塞的情境下。本文将深入介绍处理套接字I/O超时的两种方法:setsockoptselectsetsockopt允许直接设置套接字的发送和接收超时时间,而select提供了一种多路复用的机制,使得在等待多个套接字就绪时能够设置超时。

1 setsockopt

SO_SNDTIMEOSO_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;
}

2 select

在上一节IO复用模型之select原理及例子中我们介绍了select函数的使用,其中最后一个字段可以设置超时时间。select相比setsockopt更常用,所以这里重点介绍这个方法。先回忆一下select的原型:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

返回值

  1. 大于0: 表示有一个或多个文件描述符就绪(可读、可写或出错)。

    • 返回值表示就绪的文件描述符的数量。
  2. 等于0: 表示在指定的超时时间内没有文件描述符就绪。

    • 如果 select 函数的超时参数为 NULL,它可能会一直等待,直到有文件描述符就绪或出错。
    • 如果超时参数设置为一个时间值,表示等待指定时间内是否有文件描述符就绪。
  3. 等于-1: 表示出现错误。

    • 可以通过查看errno变量获取具体的错误信息。

在Linux中errno可能返回EINTR

EINTR是在系统调用(select是一个系统调用)被信号中断时返回的错误码,当一个信号(例如SIGINTSIGTERM)被发送给进程,并且进程正在执行一个系统调用时,该系统调用可能会被中断,返回 EINTR 错误。

2.1 读/写操作的超时处理

下面使用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。

2.2 连接操作(connect)的超时处理

对于系统调用connect,在网线没插或者对端没有在listen等情况下,connect函数可能会阻塞几十秒才返回。所以我们很有必要设置connect函数为非阻塞。

1、设置文件描述符为非阻塞

实现这个功能需要用到fcntl函数,它可以用来改变已打开文件描述符的属性:

int fcntl(int fd, int cmd, ... /* arg */);

其中参数说明如下:

  • fd:文件描述符,表示要操作的文件或套接字。
  • cmd:操作命令,指定对文件描述符进行何种操作。常用的命令包括:
    • F_DUPFD: 复制文件描述符
    • F_GETFD: 获取文件描述符标志
    • F_SETFD: 设置文件描述符标志
    • F_GETFL: 获取文件状态标志
    • F_SETFL: 设置文件状态标志
    • F_GETOWN: 获取异步I/O进程ID或套接字拥有者
    • F_SETOWN: 设置异步I/O进程ID或套接字拥有者
  • arg:可选参数,取决于操作命令,根据不同的命令,参数可能是一个整数、一个结构体指针等

我们可以使用操作命令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;
}

猜你喜欢

转载自blog.csdn.net/tilblackout/article/details/134498060
今日推荐