Socket 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。

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

示例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 # 服务器端超时未输入数据,客户端程序运行结束

写超时

[mapan@localhost sockOption]$ ls
client.cpp  makefile  server.cpp
[mapan@localhost sockOption]$ cat client.cpp 
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#define MAXLINE 4096
 
 
int main(int argc,char **argv)
{
   int connfd,ret;
   char sendbuf[400000]={0};
   struct sockaddr_in servaddr;
 
   if(argc != 2)
   {
      printf("error\n");
   }
 
   connfd=socket(AF_INET,SOCK_STREAM,0);
   memset(&servaddr,0,sizeof(servaddr));
   servaddr.sin_family=AF_INET;
   servaddr.sin_port=htons(6666);
   inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
 
   connect(connfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
   
   struct timeval stTimeValStruct;
   stTimeValStruct.tv_sec=5;
   stTimeValStruct.tv_usec=0;
 
   setsockopt(connfd,SOL_SOCKET,SO_SNDTIMEO,&stTimeValStruct,sizeof(stTimeValStruct));
   while(1)
   {
     ret= write(connfd,sendbuf,sizeof(sendbuf));
     printf("ret=%d\n",ret);
   }
  
   close(connfd);
   return 0;
}
 
 
[mapan@localhost sockOption]$ cat server.cpp 
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#define MAXLINE 4096
 
 
int main()
{
   int listenfd,acceptfd;
   struct sockaddr_in servaddr;
 
 
   listenfd=socket(AF_INET,SOCK_STREAM,0);
   memset(&servaddr,0,sizeof(servaddr));
   servaddr.sin_family=AF_INET;
   servaddr.sin_port=htons(6666);
   servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
 
   bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
   listen(listenfd,10);
 
   acceptfd=accept(listenfd,(struct sockaddr *)NULL,NULL);   
    
   getchar();
   close(acceptfd);
   close(listenfd); 
   return 0;
}
 
[mapan@localhost sockOption]$ cat makefile 
all:server client
 
server.o:server.cpp
    g++ -c server.cpp
client.o:client.cpp
    g++ -c client.cpp
server:server.o
    g++ -o server server.o
client:client.o
    g++ -o client client.o
 
clean:
    rm -f server client *.o
[mapan@localhost sockOption]$ 

编译并运行,客户端需要新打开一个窗口执行。

[mapan@localhost sockOption]$ make
g++ -c server.cpp
g++ -o server server.o
g++ -c client.cpp
g++ -o client client.o
[mapan@localhost sockOption]$ ./server 
[mapan@localhost sockOption]$ ./client 127.0.0.1
ret=400000
ret=400000
ret=400000
ret=254012
ret=-1
ret=-1
^C
[mapan@localhost sockOption]$

再看看看接收缓冲区和发送缓冲区:

socket缓冲区配置

cat /proc/sys/net/ipv4/tcp_wmem
4096 65535 33554432

65535 : tcp 发送缓冲区的默认值

cat /proc/sys/net/ipv4/tcp_rmem
4096 65535 16777216

65535 :tcp接收缓冲区的默认值

cat /proc/sys/net/core/wmem_max

33554432
33554432: tcp 或 udp 发送缓冲区最大可设置值的一半。

tcp 或udp收发缓冲区最小值

tcp 或udp接收缓冲区的最小值为 256 bytes,由内核的宏决定;

tcp 或udp发送缓冲区的最小值为 2048 bytes,由内核的宏决定

ss -nt | grep 6666

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335