C/C++ Socket套接字设置接收超时方法(设置非阻塞、超时时间)(fcntl、select、setsockopt)

发布时间:2024年01月18日

C/C++ Socket设置非阻塞模式接收超时时间的多种方法

网络编程中经常需要处理的一个问题就是如何正确地处理Socket超时。对于C/C++,有几种常用的技术可以用来设置Socket接收超时时间。在这篇文章中,我们将详细介绍如何在C/C++中设置Socket的非阻塞模式以及如何配置接收超时时间。

非阻塞模式(fcntl)

默认情况下,Socket操作都是阻塞的。这意味着当调用某个Socket函数时(例如recv),如果数据还未就绪,函数会阻塞等待,直到有数据可用为止。然而,在许多情况下,让函数阻塞并不是最佳解决方案(容易造成卡死)。这时,就需要使用非阻塞模式。

设置非阻塞模式

要将Socket设置为非阻塞模式,可以使用fcntl函数。以下是一段示例代码:

int flags = fcntl(sock_fd, F_GETFL, 0);
fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK);

上述代码首先获取了Socket当前的文件状态标志,然后将O_NONBLOCK标志位添加到文件状态标志中,最后使用F_SETFL命令将新的文件状态标志设置回Socket。此时,Socket已经处于非阻塞模式。

非阻塞模式下的接收超时

在非阻塞模式下,如果没有数据可用,recv函数会立即返回一个错误,并设置errno为EWOULDBLOCKEAGAIN。因此,可以通过检查errno来确定是否超时。以下是一段示例代码:

#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define MAX_RETRIES 5
#define SLEEP_DURATION 1000000 // One second
#define BUFFER_SIZE 1024

int retries = 0;
char buffer[BUFFER_SIZE];

while(retries < MAX_RETRIES) {
    memset(buffer, 0, sizeof(buffer)); // Clear the buffer
    ssize_t recv_status = recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);

    if(recv_status < 0) {
        if(errno == EWOULDBLOCK || errno == EAGAIN) {
            usleep(SLEEP_DURATION);
            retries++;
        } else {
            perror("Error in recv"); // Print error message
            break;
        }
    } else if(recv_status == 0) { // Socket is closed
        printf("Socket is closed by the peer\n");
        break;
    } else { 
        // Handle received data
        printf("Received data: %s\n", buffer);
        break;
    }
}

if(retries >= MAX_RETRIES) {
    printf("Failed to receive data after %d retries\n", MAX_RETRIES);
}

在上述代码中,我们在一个循环中不断地尝试接收数据。如果recv返回了错误,并且errno被设置为EWOULDBLOCKEAGAIN,我们就让进程睡眠一段时间,然后重试。如果尝试了指定的次数还未能成功接收到数据,那么我们就认为已经超时。

这种方法的优点是简单直观。但缺点是可能会占用大量的CPU资源,因为在超时期间,程序会不断地在循环中运行。

使用select函数

另一种处理Socket超时的方法是使用select函数。select函数可以监听一组文件描述符,等待它们中的任何一个进入就绪状态(例如,数据可读),或者直到超时。这种方法的优点是可以同时监听多个Socket,并且不会占用过多的CPU资源。

使用select设置接收超时

以下是一段使用select设置接收超时的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define TIMEOUT_SECONDS 5
#define BUFFER_SIZE 1024

int main() {
    fd_set set;
    struct timeval timeout;
    char buffer[BUFFER_SIZE];
    int sock_fd;

    // TODO: Initialize the socket here. You need to write your own logic to do this.

    FD_ZERO(&set);
    FD_SET(sock_fd, &set);

    timeout.tv_sec = TIMEOUT_SECONDS;
    timeout.tv_usec = 0;

    int rv = select(sock_fd + 1, &set, NULL, NULL, &timeout);
    if(rv == 0) {
        // Timeout
        printf("Timeout occurred! No data after %d seconds.\n", TIMEOUT_SECONDS);
    } else if(rv < 0) {
        // Error occurred
        perror("Error occurred in select");
    } else {
        // Socket ready, can receive data now
        ssize_t bytes_received = recv(sock_fd, buffer, BUFFER_SIZE - 1, 0); // leave space for '\0'
        if(bytes_received < 0) {
            // Error occurred in recv
            perror("Error occurred in recv");
        } else {
            // Null-terminate the received data
            buffer[bytes_received] = '\0';
            printf("Received data: %s\n", buffer);
        }
    }

    // Clean up and close the socket
    if(close(sock_fd) < 0) {
        perror("Error occurred while closing the socket");
    }

    return 0;
}

在上述代码中,我们首先初始化了一个文件描述符集合和一个时间间隔结构体。然后,我们将目标Socket添加到文件描述符集合中,并设置了超时时间。最后,我们调用select函数并检查其返回值。如果select返回0,表示已经超时。如果select返回负数,表示发生了错误。如果select返回正数,表示有文件描述符已经就绪,此时我们就可以调用recv来接收数据了。

setsockopt方法设置Socket超时

除了上述介绍的非阻塞模式和select函数,还有一种常用的方法是使用setsockopt函数来直接设置Socket的超时时间。

setsockopt函数概述

setsockopt函数用于设置指定的Socket选项。它的原型如下:

int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);

这个函数接收五个参数:sockfd是要设置的Socket的文件描述符;level指定选项所在的协议层;optname是需要设置的选项的名称;optval指向包含新选项值的缓冲区;optlenoptval缓冲区的大小。

使用setsockopt设置接收超时

在Socket编程中,SO_RCVTIMEOSO_SNDTIMEO选项可以分别用来设置接收和发送超时。这两个选项都位于套接字层,所以在调用setsockopt函数时,level参数应设为SOL_SOCKET

以下是一段示例代码,展示如何使用setsockopt设置接收超时:

struct timeval timeout;
timeout.tv_sec = TIMEOUT_SECONDS;
timeout.tv_usec = 0;

if (setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
    // Error occurred
}

在上述代码中,我们首先创建了一个timeval结构体,并设置了超时时间。然后,我们调用setsockopt函数,将SO_RCVTIMEO选项的值设置为指向timeout结构体的指针。如果setsockopt返回负数,表示发生了错误。

需要注意的是,SO_RCVTIMEOSO_SNDTIMEO选项设置的超时时间是一个总时间,而不是在Socket函数阻塞时每次等待的时间。这意味着,如果你在一个循环中多次调用recv函数,那么这些函数调用的总时间将不会超过你设置的超时时间。

完整示例代码

下面是一个unix domain socket使用setsockopt函数设置接收超时的示例代码(用文件套接字通信),其中FILE_PATH是文件路径。

bool nonBlockingRecv()
{
    struct sockaddr_un addr;
    int sock_fd;
    char buffer[BUFFER_SIZE] = "REQ";
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, FILE_PATH.c_str());
    sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_fd < 0)
    {
        std::cout << "Request socket failed\n";
        return false;
    }

    if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
    {
        std::cout << "Connect socket failed\n";
        close(sock_fd);
        return false;
    }

    //1.send command
    SEND_INFO(COMMAND);

    // Set recv timeout to 100ms
    struct timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 100000; // 100 ms
    if (setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) 
    {
        std::cout << "Setting socket timeout failed\n";
        close(sock_fd);
        return false;
    }

    //2.receive response of register req
    memset(buffer, 0, BUFFER_SIZE);
    int recv_status = recv(sock_fd, buffer, BUFFER_SIZE, 0);
    if (recv_status < 0) 
    {
        if (errno == EWOULDBLOCK || errno == EAGAIN) 
        {
            std::cout << "Receive timeout\n";
        } 
        else 
        {
            std::cout << "Receive error\n";
        }
        close(sock_fd);
        return false;
    }

    std::cout << "Received [" << buffer << "] from manager" << std::endl;

    //3.check result
    if (NULL != strstr(buffer, SUCCESS.c_str()))//receive success.
    {
        std::cout << "Received success\n";
        close(sock_fd);
        return true;
    }
    else
    {
        std::cout << "Received fail\n";
        close(sock_fd);
        return false;
    }
}

总结

使用setsockopt函数设置SO_RCVTIMEO选项是一种直接且有效的方法来设置Socket接收超时。这种方法的优点是简单直观,只需要一行代码就可以完成设置。然而,它的缺点是灵活性较差,因为它只能设置一个固定的超时时间,而不能动态地根据网络状况调整超时时间。

总结

在C/C++中,有多种方法可以用来设置Socket接收超时时间。非阻塞模式和select函数亦或setsockopt函数都是处理这个问题的有效工具。需要注意的是,选择哪种方法取决于具体的应用场景。例如,如果你需要同时处理多个Socket,那么select函数可能是更好的选择。如果想要方便,setsockopt函数可以考虑。

文章来源:https://blog.csdn.net/Dontla/article/details/135651017
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。