linux网络编程之socket(十一):套接字I/O超时设置方法和用select实现超时

注:如无特殊说明,sockfd 原始状态都是阻塞的。

一、使用alarm 函数设置超时

void handler(int sig)
{
}
signal(SIGALRM, handler);

alarm(5);
int ret = read(fd, buf, sizeof(buf));
if (ret == -1 && errno == EINTR)
    errno = ETIMEOUT;
else if (ret >= 0)
    alarm(0);
.................

程序大概框架如上所示,如果read在5s内被SIGALRM信号中断而返回,则表示超时,否则未超时已读取到数据,取消闹钟。但这种方法不常用,因为有时可能在其他地方使用了alarm会造成混乱。

二、使用套接字选项SO_SNDTIMEO、SO_RCVTIMEO

struct timeval timeout = {3,0}; 

setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(struct timeval));

int ret = read(sock, buf, sizeof(buf));
if (ret == -1 && errno == EWOULDBLOCK)
    errno = ETIMEOUT;
..........

即使用setsockopt 函数进行设置,但这种方法可移植性比较差,不是每种系统实现都有这些选项。

三、使用select 实现超时

下面程序包含read_timeout、write_timeout、accept_timeout、connect_timeout 四个函数封装

/*************************************************************************
    > File Name: sysutil.c
    > Author: Simba
    > Mail: dameng34@163.com
    > Created Time: Sat 02 Mar 2013 10:53:06 PM CST
 ************************************************************************/

#include "sysutil.h"

/* read_timeout - 读超时检测函数,不含读操作
 * fd:文件描述符
 * wait_seconds:等待超时秒数, 如果为0表示不检测超时;
 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */

int read_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {

        fd_set read_fdset;
        struct timeval timeout;

        FD_ZERO(&read_fdset);
        FD_SET(fd, &read_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;

        do
        {
            ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout); //select会阻塞直到检测到事件或者超时
            // 如果select检测到可读事件发送,则此时调用read不会阻塞
        }
        while (ret < 0 && errno == EINTR);

        if (ret == 0)
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)
            return 0;

    }

    return ret;
}

/* write_timeout - 写超时检测函数,不含写操作
 * fd:文件描述符
 * wait_seconds:等待超时秒数, 如果为0表示不检测超时;
 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */

int write_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {

        fd_set write_fdset;
        struct timeval timeout;

        FD_ZERO(&write_fdset);
        FD_SET(fd, &write_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;

        do
        {
            ret = select(fd + 1, NULL, &write_fdset, NULL, &timeout);
        }
        while (ret < 0 && errno == EINTR);

        if (ret == 0)
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)
            return 0;

    }

    return ret;
}

/* accept_timeout - 带超时的accept
 * fd: 套接字
 * addr: 输出参数,返回对方地址
 * wait_seconds: 等待超时秒数,如果为0表示正常模式
 * 成功(未超时)返回已连接套接字,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */

int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if (wait_seconds > 0)
    {

        fd_set accept_fdset;
        struct timeval timeout;
        FD_ZERO(&accept_fdset);
        FD_SET(fd, &accept_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;

        do
        {
            ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
        }
        while (ret < 0 && errno == EINTR);

        if (ret == -1)
            return -1;
        else if (ret == 0)
        {
            errno = ETIMEDOUT;
            return -1;
        }
    }

    if (addr != NULL)
        ret = accept(fd, (struct sockaddr *)addr, &addrlen);
    else
        ret = accept(fd, NULL, NULL);
    if (ret == -1)
        ERR_EXIT("accpet error");

    return ret;
}

/* activate_nonblock - 设置IO为非阻塞模式
 * fd: 文件描述符
 */
void activate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1)
        ERR_EXIT("fcntl error");

    flags |= O_NONBLOCK;
    ret = fcntl(fd, F_SETFL, flags);
    if (ret == -1)
        ERR_EXIT("fcntl error");
}

/* deactivate_nonblock - 设置IO为阻塞模式
 * fd: 文件描述符
 */
void deactivate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1)
        ERR_EXIT("fcntl error");

    flags &= ~O_NONBLOCK;
    ret = fcntl(fd, F_SETFL, flags);
    if (ret == -1)
        ERR_EXIT("fcntl error");
}

/* connect_timeout - 带超时的connect
 * fd: 套接字
 * addr: 输出参数,返回对方地址
 * wait_seconds: 等待超时秒数,如果为0表示正常模式
 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
 */
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if (wait_seconds > 0)
        activate_nonblock(fd);

    ret = connect(fd, (struct sockaddr *)addr, addrlen);
    if (ret < 0 && errno == EINPROGRESS)
    {

        fd_set connect_fdset;
        struct timeval timeout;
        FD_ZERO(&connect_fdset);
        FD_SET(fd, &connect_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;

        do
        {
            /* 一旦连接建立,套接字就可写 */
            ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout);
        }
        while (ret < 0 && errno == EINTR);

        if (ret == 0)
        {
            errno = ETIMEDOUT;
            return -1;
        }
        else if (ret < 0)
            return -1;

        else if (ret == 1)
        {
            /* ret返回为1,可能有两种情况,一种是连接建立成功,一种是套接字产生错误
             * 此时错误信息不会保存至errno变量中(select没出错),因此,需要调用
             * getsockopt来获取 */
            int err;
            socklen_t socklen = sizeof(err);
            int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);
            if (sockoptret == -1)
                return -1;
            if (err == 0)
                ret = 0;
            else
            {
                errno = err;
                ret = -1;
            }
        }
    }

    if (wait_seconds > 0)
        deactivate_nonblock(fd);


    return ret;
}

下面来解析一下这些函数的封装:

1、read_timeout :如注释所写,这只是读超时检测函数,并不包含读操作,如果从此函数成功返回,则此时调用read将不再阻塞,测试代码可以这样写:

int ret;
ret = read_timeout(fd, 5);
if (ret == 0)
    read(fd, buf, sizeof(buf));
else if (ret == -1 && errno == ETIMEOUT)
    printf("timeout...\n");
else
    ERR_EXIT("read_timeout");

如果 read_timeout(fd, 0); 则表示不检测超时,函数直接返回为0,此时再调用read 将会阻塞。

当wait_seconds 参数大于0,则进入if 括号执行,将超时时间设置为select函数的超时时间结构体,select会阻塞直到检测到事件发生或者超时。如果select返回-1且errno 为EINTR,说明是被信号中断,需要重启select;如果select返回0表示超时;如果select返回1表示检测到可读事件;否则select返回-1 表示出错。

2、write_timeout :此函数跟read_timeout 函数类似,只是select 关心的是可写事件,不再赘述。

3、accept_timeout :此函数是带超时的accept 函数,如果能从if (wait_seconds > 0) 括号执行后向下执行,说明select 返回为1,检测到已连接队列不为空,此时再调用accept 不再阻塞,当然如果wait_seconds == 0 则像正常模式一样,accept 阻塞等待,注意,accept 返回的是已连接套接字。

4、connect_timeout :在调用connect前需要使用fcntl 函数将套接字标志设置为非阻塞,如果网络环境很好,则connect立即返回0,不进入if 大括号执行;如果网络环境拥塞,则connect返回-1且errno == EINPROGRESS,表示正在处理。此后调用select与前面3个函数类似,但这里关注的是可写事件,因为一旦连接建立,套接字就可写。还需要注意的是当select 返回1,可能有两种情况,一种是连接成功,一种是套接字产生错误,由这里可知,这两种情况都会产生可写事件,所以需要使用getsockopt来获取一下。退出之前还需重新将套接字设置为阻塞。

我们可以写个小程序测试一下connect_timeout 函数,客户端程序如下:

#include "sysutil.h"

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");


    int ret = connect_timeout(sock, &servaddr, 5);
    if (ret == -1 && errno == ETIMEDOUT)
    {
        printf("timeout...\n");
        return 1;
    }
    else if (ret == -1)
        ERR_EXIT("connect_timeout");

    struct sockaddr_in localaddr;
    socklen_t addrlen = sizeof(localaddr);
    if (getsockname(sock, (struct sockaddr *)&localaddr, &addrlen) < 0)
        ERR_EXIT("getsockname");

    printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));


    return 0;
}

因为是在本机上测试,所以不会出现超时的情况,但出错的情况还是可以看到的,比如不要启动服务器端程序,而直接启动客户端程序,输出如下:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli_timeout  connect_timeout: Connection refused

很明显是connect_timeout 函数返回了-1,我们也可以推算出connect_timeout 函数中,select返回1,但却是套接字发生错误的情况,errno = ECONNREFUSED,所以打印出Connection refused。

在这里可以粗略说下tcp connect 的机制,connect 只是完成发送 syn 的过程,后续的两次握手由协议栈完成。如果 fd 是 阻塞的,则 connect 会一直等到超时或者连接成功返回;如果 fd 是非阻塞的,则 connect 会立刻返回,但此时协议栈是否已经完成连接要判断下返回值和 errno;无论 fd 阻塞还是非阻塞,如果没有设置超时,则当重传 syn 次数达到 sysctl net.ipv4.tcp_syn_retries  时才超时结束,重传 syn 的时间采取指数退避的方式,假设 syn_retries 为5, 则分别为 1, 2, 4, 8, 16, 32 ... 即在目标 ip 不可达时要几十秒才 timeout(如果是ip 可达,但没有对应的监听端口,则在一次重试后,对端机器会发送reset 标志,连接结束,耗时 1s 多),故:

如果是非阻塞方式,按照stevens 建议,如上面的做法即可;

如果是阻塞方式,可以用 setsockopt 设置 SO_SNDTIMEO 即可。

参考:

《Linux C 编程一站式学习》

《TCP/IP详解 卷一》

《UNP》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

编写一个go gRPC的服务

前置条件: 获取 gRPC-go 源码 $ go get google.golang.org/grpc 简单例子的源码位置: $ cd $GOPATH/src/...

4227
来自专栏陈树义

注解的那些事儿(二)| 如何自定义注解

自定义注解是自己写框架的必备技能,使用注解能极大地提升开发效率,因此自定义注解是一个高级开发者必备的技能。

712
来自专栏函数式编程语言及工具

泛函编程(9)-异常处理-Option

     Option是一种新的数据类型。形象的来描述:Option就是一种特殊的List,都是把数据放在一个管子里;然后在管子内部对数据进行各种操作。所以Op...

1746
来自专栏Python

web框架

http协议 HTTP简介 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World ...

3356
来自专栏前端布道

浏览器同源策略及跨域的解决方法

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 ...

882
来自专栏Hongten

正则表达式大全

正则表达式是一个描述字符模式的对象。 JavaScript的RegExp对象和String对象定义了使用正则表达式来执行强大的模式匹配和文本检索与替换函数的方...

1272
来自专栏Golang语言社区

Go语言语法汇总

最近看了看GoLang,把Go语言的语法总结了一下,做个快速参考 数据类型 ---- var varName type,var var1,var2… type,...

33513
来自专栏java一日一条

一个Java对象到底占用多大内存

大家可以用这个代码边看边验证,注意的是,运行这个程序需要通过javaagent注入Instrumentation,具体可以看原博客。我今天主要是总结下手动计算J...

421
来自专栏猿天地

注解面试题-请了解下

金三银四,三四月是找工作最好的时期。错过了三月千万别放弃四月。 在面试的时候,有些面试官会问注解相关的问题, 注解最典型的代表框架就是Spring了,特别是Sp...

3079
来自专栏北京马哥教育

Python高级编程技巧

正文: 本文展示一些高级的Python设计结构和它们的使用方法。在日常工作中,你可以根据需要选择合适的数据结构,例如对快速查找性的要求、对数据一致 性的要求或...

3544

扫码关注云+社区