Linux C套接字选项SO_RCVTIMEO, SO_SNDTIMEO介绍

套接字选项SO_RCVTIMEO: 用来设置socket接收数据的超时时间; 套接字选项SO_SNDTIMEO: 用来设置socket发送数据的超时时间;

比如,一般情况下,调用accept/connect/send/recv, 进程会阻塞,但是如果对端异常,进行可能无法正常退出等待。如何让这些调用自动定时退出? 可以使用诸如alarm定时器、I/O复用设置定时器,还可以使用socket编程里函数级别的socket套接字选项SO_RCVTIMEO和SO_SNDTIMEO,仅针对与数据接收和发送相关,而无需设置专门的信号捕获函数。

能够作用的系统调用包括:send、sendmsg、recv、recvmsg、accept、connect。

系统调用有效选项系统调用超时后的行为
sendSO_SNDTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
sendmsgSO_SNDTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
recvSO_RCVTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
recvmsgSO_RCVTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
acceptSO_RCVTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
connectSO_SNDTIMEO返回-1,设置errno为EINPROGRESS

注意

  1. EAGAIN通常和EWOULDBLOCK是同一个值;

  2. SO_RCVTIMEO, SO_SNDTIMEO不要求系统调用对应fd是非阻塞(nonblocking)的,但是使用了该套接字选项的sock fd,会成为nonblocking(即使之前是blocking)的。参见man手册ERRORS EAGAIN/EWOULDBLOCK的描述;

man send关于EAGAIN / EWOULDBLOCK描述:

EAGAIN or EWOULDBLOCK The socket is marked nonblocking and the requested operation would block. POSIX.1-2001 allows either error to be returned for this case, and does not require these constants to have the same value, so a portable application should check for both possibilities.

示例1:设置connect超时时间

根据系统调用accept的返回值,以及errno判断超时时间是否已到,从而决定是否开始处理超时定时任务。

客户端程序:超时连接服务器

/**
 * 客户端程序
 * 连接服务器,超时报错、返回
 * build:
 * $ gcc timeout_connect.c
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <assert.h>

/* 超时连接 */
int timeout_connect (const char *ip, int port, int time)
{
    int ret = 0;
    struct sockaddr_in servaddr;
	
    printf("client start...\n");

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &servaddr.sin_addr);
    servaddr.sin_port = htons(port);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd >= 0);

    /* 通过选项SO_RCVTIMEO和SO_SNDTIMEO设置的超时时间的类型时timeval, 和select系统调用的超时参数类型相同 */
    struct timeval timeout;
    timeout.tv_sec = time;
    timeout.tv_usec = 0;

    socklen_t len = sizeof(timeout);
    ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
    if (ret == -1) {
        perror("setsockopt error");

        return -1;
    }

    if ((ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) < 0) {
	/* 超时对于errno 为EINPROGRESS. 下面条件如果成立,就可以处理定时任务了 */
        if (errno == EINPROGRESS) {
            perror("connecting timeout, process timeout logic");
            return -1;
        }

        perror("error occur when connecting to server\n");
    }

    return sockfd;
}

int main(int argc, char *argv[])
{
    if (argc <= 2) {
        printf("usage: %s ip_address port_number\n", argv[0]);
        return 1;
    }

    const char *ip = argv[1];
    int port = atoi(argv[2]);
    printf("connect %s:%d...\n", ip, port);

    int sockfd = timeout_connect(ip, port, 10);
    if (sockfd < 0) {
        perror("timeout_connect error");
        return 1;
    }

    return 0;
}

运行结果(随意输入一个服务器IP、端口):

$ ./timeout_connect 192.168.0.105 8000
connect 192.168.0.105:8000...
client start...
connecting timeout, process timeout logic: Operation now in progress
timeout_connect error: Operation now in progress

可以看到,本来阻塞的connect调用,10秒后返回-1,并且errno设置为EINPROGRESS。

示例2:超时接收(服务器数据)

服务器端

监听本地任意IP地址,端口8001 从键盘输入一行数据,就发送给用户;如果没有数据,就阻塞。

/**
 * 服务器程序
 * 示例:超时接收服务器数据,超时时间例程中设置为10秒
 * 编译: $ gcc timeout_recv_server.c -o server
 * 运行方式:
 * $ ./server
 * 默认监听端口8001(根据实际情况修改)
 * 服务器功能:从键盘接收用户输入,每接收一行就向客户输出一行。如果没有用户输入,
 * 则阻塞。
 * 客户端需要跟服务器安装在同一网段上,为了测试方便,就直接都安装到同一机器上
 */
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <signal.h>

int sockfd = -1;

void sig_func(int sig_no)
{
    if (sig_no == SIGINT || sig_no == SIGTERM) {
        if (sockfd >= 0) {
            close(sockfd);
        }
        exit(1);
    }
}

int main()
{
    struct sockaddr_in servaddr, cliaddr;
    int listenfd;

    signal(SIGINT, sig_func);

    printf("server start...\n");

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket error");
        exit(1);
    }
    sockfd = listenfd;

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
        perror("setsocketopt error");
        exit(1);
    }

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    // servaddr.sin_addr.s_addr = INADDR_ANY;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(8001);

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind error");
        exit(1);
    }

    if (listen(listenfd, 5) < 0) {
        perror("listen error");
        exit(1);
    }

    char buf[1024];
    socklen_t clilen = sizeof(cliaddr);
    int connfd;

    if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {
        perror("accept error");
        exit(1);
    }

    printf("input a line string: \n");
    int nbytes;
    while (fgets(buf, sizeof(buf), stdin)) {
        nbytes = send(connfd, buf, strlen(buf), 0);
        if (nbytes < 0) {
            perror("send error");
            break;
        }
        else if (nbytes == 0) {
        
        }
        printf("send: %s\n", buf);
    }
    
    close(connfd);
    close(listenfd);

    return 0;
}

客户端 

设置10秒超时,接收服务器数据。客户端10秒以内,接收到服务器数据,则直接打印;超过10秒,就报错退出。

/**
 * 客户端程序
 * 示例:超时接收服务器数据,超时时间例程中设置为10秒
 * 编译: $ gcc timeout_recv_client.c -o client
 * 运行方式:
 * 如本地运行(对应服务器实际监听的IP地址和端口号) $ ./client 127.0.0.1 8001
 */
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>

int timeout_recv(int fd, char *buf, int len,  int nsec)
{
    struct timeval timeout;
    timeout.tv_sec = nsec;
    timeout.tv_usec = 0;

    printf("timeout_recv called, timeout %d seconds\n", nsec);

    if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
        perror("setsockopt error");
        exit(1);
    }

    int n = recv(fd, buf, len, 0);

    return n;
}

int main(int argc, char *argv[])
{
    if (argc != 3) {
        printf("usage: %s <ip address> <port>\n", argv[0]);
    }

    char *ip = argv[1];
    uint16_t port = atoi(argv[2]);
    
    printf("client start..\n");
    printf("connect to %s:%d\n", ip, port);

    int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket error");
        exit(1);
    }
    
    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &servaddr.sin_addr);
    servaddr.sin_port = htons(port);
    
    int connfd;
    if ((connfd = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) < 0) {
        perror("connect error");
        exit(1);
    }

    printf("success to connect server %s:%d\n", ip, port);
    printf("wait for server's response\n");
    char buf[100];
    while (1) {
        int nread;
        
        nread = timeout_recv(sockfd, buf, sizeof(buf), 10);
        if (nread < 0) {
            perror("timeout_recv error");
            exit(1);
        }
        else if (nread == 0) {
            shutdown(sockfd, SHUT_RDWR);
            break;
        }

        write(STDOUT_FILENO, buf, nread);
    }

    return 0;
}

客户端运行结果: 可以看到,超过10秒后,客户端自动退出程序,而不再阻塞在recv。

$ ./client 127.0.0.1 8001
client start..
connect to 127.0.0.1:8001
success to connect server 127.0.0.1:8001
wait for server's response
timeout_recv called, timeout 10 seconds
hello # 服务器端用户输入数据
timeout_recv called, timeout 10 seconds
nihao # 服务器端用户输入数据
timeout_recv called, timeout 10 seconds
timeout_recv error: Resource temporarily unavailable # 服务器端超时未输入数据,客户端程序运行结束

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值