zoukankan      html  css  js  c++  java
  • Linux C套接字选项SO_RCVTIMEO, SO_SNDTIMEO

    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。

    系统调用 有效选项 系统调用超时后的行为
    send SO_SNDTIMEO 返回-1,设置errno为EAGAIN或EWOULDBLOCK
    sendmsg SO_SNDTIMEO 返回-1,设置errno为EAGAIN或EWOULDBLOCK
    recv SO_RCVTIMEO 返回-1,设置errno为EAGAIN或EWOULDBLOCK
    recvmsg SO_RCVTIMEO 返回-1,设置errno为EAGAIN或EWOULDBLOCK
    accept SO_RCVTIMEO 返回-1,设置errno为EAGAIN或EWOULDBLOCK
    connect SO_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...
    ");
    
        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
    ");
        }
    
        return sockfd;
    }
    
    int main(int argc, char *argv[])
    {
        if (argc <= 2) {
            printf("usage: %s ip_address port_number
    ", argv[0]);
            return 1;
        }
    
        const char *ip = argv[1];
        int port = atoi(argv[2]);
        printf("connect %s:%d...
    ", 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...
    ");
    
        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: 
    ");
        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
    ", 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
    ", 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>
    ", argv[0]);
        }
    
        char *ip = argv[1];
        uint16_t port = atoi(argv[2]);
        
        printf("client start..
    ");
        printf("connect to %s:%d
    ", 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
    ", ip, port);
        printf("wait for server's response
    ");
        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 # 服务器端超时未输入数据,客户端程序运行结束
    

    参考

    [1]游双. Linux高性能服务器编程[M]. 机械工业出版社, 2013.

  • 相关阅读:
    理解FreeRTOS的任务状态机制
    stm32【按键处理:单击、连击、长按】
    stm32f4单片机 硬件浮点运算
    stm32 HAL库 串口无法接收数据的问题
    Single Sign On —— 简介(转)
    关于第三方库安装时很慢或者读取超时问题处理
    设计模式》状态机模式
    设计模式》责任链模式
    设计模式》访问者模式
    设计模式》策略者模式
  • 原文地址:https://www.cnblogs.com/fortunely/p/15055618.html
Copyright © 2011-2022 走看看