Socket编程实践(9) --套接字IO超时设置方法

引:超时设置3种方案

1. alarm超时设置方法

//代码实现: 这种方式较少用
void sigHandlerForSigAlrm(int signo)
{
    return ;
}

signal(SIGALRM, sigHandlerForSigAlrm);
alarm(5);
int ret = read(sockfd, buf, sizeof(buf));
if (ret == -1 && errno == EINTR)
{
    // 超时被时钟打断
    errno = ETIMEDOUT;
}
else if (ret >= 0)
{
    // 正常返回(没有超时), 则将闹钟关闭
    alarm(0);
}

2. 套接字选项: SO_SNDTIMEO, SO_RCVTIMEO

调用setsockopt设置读/写超时时间

//示例: read超时
int seconds = 5;
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &seconds, sizeof(seconds)) == -1)
    err_exit("setsockopt error");
int ret = read(sockfd, buf, sizeof(buf));
if (ret == -1 && errno == EWOULDBLOCK)
{
    // 超时,被时钟打断
    errno = ETIMEDOUT;
}

3. select方式[重点]

_timeout函数封装

1. read_timeout封装

/**
 *read_timeout - 读超时检测函数, 不包含读操作
 *@fd: 文件描述符
 *@waitSec: 等待超时秒数, 0表示不检测超时
 *成功(未超时)返回0, 失败返回-1, 超时返回-1 并且 errno = ETIMEDOUT
**/
int read_timeout(int fd, long waitSec)
{
    int returnValue = 0;
    if (waitSec > 0)
    {
        fd_set readSet;
        FD_ZERO(&readSet);
        FD_SET(fd,&readSet);    //添加

        struct timeval waitTime;
        waitTime.tv_sec = waitSec;
        waitTime.tv_usec = 0;       //将微秒设置为0(不进行设置),如果设置了,时间会更加精确
        do
        {
            returnValue = select(fd+1,&readSet,NULL,NULL,&waitTime);
        }
        while(returnValue < 0 && errno == EINTR);   //等待被(信号)打断的情况, 重启select

        if (returnValue == 0)   //在waitTime时间段中一个事件也没到达
        {
            returnValue = -1;   //返回-1
            errno = ETIMEDOUT;
        }
        else if (returnValue == 1)  //在waitTime时间段中有事件产生
            returnValue = 0;    //返回0,表示成功
        // 如果(returnValue == -1) 并且 (errno != EINTR), 则直接返回-1(returnValue)
    }

    return returnValue;
}

2. write_timeout封装

/**
 *write_timeout - 写超时检测函数, 不包含写操作
 *@fd: 文件描述符
 *@waitSec: 等待超时秒数, 0表示不检测超时
 *成功(未超时)返回0, 失败返回-1, 超时返回-1 并且 errno = ETIMEDOUT
**/
int write_timeout(int fd, long waitSec)
{
    int returnValue = 0;
    if (waitSec > 0)
    {
        fd_set writeSet;
        FD_ZERO(&writeSet);      //清零
        FD_SET(fd,&writeSet);    //添加

        struct timeval waitTime;
        waitTime.tv_sec = waitSec;
        waitTime.tv_usec = 0;
        do
        {
            returnValue = select(fd+1,NULL,&writeSet,NULL,&waitTime);
        } while(returnValue < 0 && errno == EINTR); //等待被(信号)打断的情况

        if (returnValue == 0)   //在waitTime时间段中一个事件也没到达
        {
            returnValue = -1;   //返回-1
            errno = ETIMEDOUT;
        }
        else if (returnValue == 1)  //在waitTime时间段中有事件产生
            returnValue = 0;    //返回0,表示成功
    }

    return returnValue;
}

3. accept_timeout函数封装

/**
 *accept_timeout - 带超时的accept
 *@fd: 文件描述符
 *@addr: 输出参数, 返回对方地址
 *@waitSec: 等待超时秒数, 0表示不使用超时检测, 使用正常模式的accept
 *成功(未超时)返回0, 失败返回-1, 超时返回-1 并且 errno = ETIMEDOUT
**/
int accept_timeout(int fd, struct sockaddr_in *addr, long waitSec)
{
    int returnValue = 0;
    if (waitSec > 0)
    {
        fd_set acceptSet;
        FD_ZERO(&acceptSet);
        FD_SET(fd,&acceptSet);    //添加

        struct timeval waitTime;
        waitTime.tv_sec = waitSec;
        waitTime.tv_usec = 0;
        do
        {
            returnValue = select(fd+1,&acceptSet,NULL,NULL,&waitTime);
        }
        while(returnValue < 0 && errno == EINTR);

        if (returnValue == 0)  //在waitTime时间段中没有事件产生
        {
            errno = ETIMEDOUT;
            return -1;
        }
        else if (returnValue == -1) // error
            return -1;
    }

    /**select正确返回:
        表示有select所等待的事件发生:对等方完成了三次握手,
        客户端有新的链接建立,此时再调用accept就不会阻塞了
    */
    socklen_t socklen = sizeof(struct sockaddr_in);
    if (addr != NULL)
        returnValue = accept(fd,(struct sockaddr *)addr,&socklen);
    else
        returnValue = accept(fd,NULL,NULL);

    return returnValue;
}

4. connect_timeout函数封装

(1)我们为什么需要这个函数?

TCP/IP在客户端连接服务器时,如果发生异常,connect(如果是在默认阻塞的情况下)返回的时间是RTT(相当于客户端阻塞了这么长的时间,客户需要等待这么长的时间,显然这样的客户端用户体验并不好(完成三次握手需要使用1.5RTT时间));会造成严重的软件质量下降.

(2)怎样实现connect_timeout?

1)sockfd首先变成非阻塞的; 然后试着进行connect,如果网络状况良好,则立刻建立链接并返回,如果网络状况不好,则链接不会马上建立,这时需要我们的参与:调用select,设置等待时间,通过select管理者去监控sockfd,一旦能够建立链接,则马上返回,然后建立链接,这样就会大大提高我们的软件质量.

2)需要注意:select机制监控到sockfd可写(也就是可以建立链接时),并不代表调用connect就一定能够成功(造成sockfd可写有两种情况: a.真正的链接可以建立起来了; b.建立链接的过程中发生错误,然后错误会回写错误信息,造成sockfd可写);

通过调用getsockopt做一个容错即可(见下例)!

(3)代码实现:

/**设置文件描述符fd为非阻塞/阻塞模式**/
bool setUnBlock(int fd, bool unBlock)
{
    int flags = fcntl(fd,F_GETFL);
    if (flags == -1)
        return false;

    if (unBlock)
        flags |= O_NONBLOCK;
    else
        flags &= ~O_NONBLOCK;

    if (fcntl(fd,F_SETFL,flags) == -1)
        return false;
    return true;
}
/**
 *connect_timeout - connect
 *@fd: 文件描述符
 *@addr: 要连接的对方地址
 *@waitSec: 等待超时秒数, 0表示使用正常模式的accept
 *成功(未超时)返回0, 失败返回-1, 超时返回-1 并且 errno = ETIMEDOUT
**/
int connect_timeout(int fd, struct sockaddr_in *addr, long waitSec)
{
    if (waitSec > 0)    //设置为非阻塞模式
        setUnBlock(fd, true);

    socklen_t addrLen = sizeof(struct sockaddr_in);
    //首先尝试着进行链接
    int returnValue = connect(fd,(struct sockaddr *)addr,addrLen);
    //如果首次尝试失败(并且errno == EINPROGRESS表示连接正在处理当中),则需要我们的介入
    if (returnValue < 0 && errno == EINPROGRESS)
    {
        fd_set connectSet;
        FD_ZERO(&connectSet);
        FD_SET(fd,&connectSet);
        struct timeval waitTime;
        waitTime.tv_sec = waitSec;
        waitTime.tv_usec = 0;
        do
        {
            /*一旦建立链接,则套接字可写*/
            returnValue = select(fd+1, NULL, &connectSet, NULL, &waitTime);
        }
        while (returnValue < 0 && errno == EINTR);
        if (returnValue == -1) //error
            return -1;
        else if (returnValue == 0)   //超时
        {
            returnValue = -1;
            errno = ETIMEDOUT;
        }
        else if (returnValue == 1)  //正确返回,有一个套接字可写
        {
            /**由于connectSet只有一个文件描述符, 因此FD_ISSET的测试也就省了**/

            /**注意:套接字可写有两种情况:
                1.连接建立成功
                2.套接字产生错误(但是此时select是正确的, 因此错误信息没有保存在errno中),需要调用getsockopt获取
            */
            int err;
            socklen_t errLen = sizeof(err);
            int sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&errLen);
            if (sockoptret == -1)
                return -1;

            // 测试err的值
            if (err == 0)   //确实是链接建立成功
                returnValue = 0;
            else    //连接产生了错误
            {
                errno = err;
                returnValue = -1;
            }
        }
    }
    if (waitSec > 0)
        setUnBlock(fd, false);
    return returnValue;
}

/**测试:使用connect_timeout的client端完整代码(server端如前)**/
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
        err_exit("socket error");

    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8001);
    serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int ret = connect_timeout(sockfd, &serverAddr, 5);
    if (ret == -1 && errno == ETIMEDOUT)
    {
        cerr << "timeout..." << endl;
        err_exit("connect_timeout error");
    }
    else if (ret == -1)
        err_exit("connect_timeout error");

    //获取并打印对端信息
    struct sockaddr_in peerAddr;
    socklen_t peerLen = sizeof(peerAddr);
    if (getpeername(sockfd, (struct sockaddr *)&peerAddr, &peerLen) == -1)
        err_exit("getpeername");
    cout << "Server information: " << inet_ntoa(peerAddr.sin_addr)
                 << ", " << ntohs(peerAddr.sin_port) << endl;
    close(sockfd);
}

附-RTT(Round-Trip Time)介绍:

RTT往返时延:在计算机网络中它是一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。

RTT由三个部分决定:即链路的传播时间、末端系统的处理时间以及路由器的缓存中的排队和处理时间。其中,前面两个部分的值作为一个TCP连接相对固定,路由器的缓存中的排队和处理时间会随着整个网络拥塞程度的变化而变化。所以RTT的变化在一定程度上反映了网络拥塞程度的变化。简单来说就是发送方从发送数据开始,到收到来自接受方的确认信息所经历的时间。

时间: 2025-01-13 21:25:21

Socket编程实践(9) --套接字IO超时设置方法的相关文章

Socket编程实践(7)   --TCP粘包解决方法2

包尾加\n编程实践 SYNOPSIS #include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); 与read相比,只能用于套接字文件描述符,而且多了一个flags Flags常用取值: MSG_OOB(紧急指针,带外数据) This flag requests receipt of out-of-band data that wo

Socket编程实践(10) --select的限制与poll的使用

select的限制 用select实现的并发服务器,能达到的并发数一般受两方面限制: 1)一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n(number)来调整或者使用setrlimit函数设置,但一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看 /**示例: getrlimit/setrlimit获取/设置进程打开文件数目**/ int main() { struct rlimit rl;

Socket编程实践(2)

Socket API基本编程模型 TCP客户/服务器模型 简单echo服务器模型 Socket 基础API实践 socket函数 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> 功能:创建一个套接字用于通信 原型 int socket(int domain, int type, int protocol); 参数 domain:指定通信协议族(protocol family),常用取值AF_INET(IP

Socket编程实践(6) --TCPNotes服务器

僵尸进程过程 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中加入 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法.解决僵尸进程 signal(SIGCHLD,onSignalCatch); void onSignalCatch(int signalNumber) { wait(NULL); } 3) 假设多个客户端同一时候关闭, 问题描写叙述如以下两幅图所看到的: watermark/2/text/aHR0cDovL2Jsb2cuY

Socket编程实践(6) --TCP服务端注意事项

僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 signal(SIGCHLD,onSignalCatch); void onSignalCatch(int signalNumber) { wait(NULL); } 3) 如果多个客户端同时关闭, 问题描述如下面两幅图所示: /** client端实现的测试代码**/ int main() { int s

Unix网络编程--卷一:套接字联网API 读书笔记

UNIX网络编程--卷一:套接字联网API 本书面对的读者是那些希望自己编写的程序能够使用成为套接字(socket)的API进行彼此通信的人. 目录: 1.简介 2.传输层:TCP.UDP和SCTP 3.套接字编程简介 4.基本TCP套接字编程 5.TCP客户/服务器程序例子 6.I/O复用:select和poll函数 7.套接字选项 8.基本UDP套接字编程 9.基本SCTP套接字编程 10.SCTP客户/服务器程序例子 11.名字与地址转换 12.IPV4与IPV6互操作性 13.守护进程和

Linux网络编程:原始套接字的魔力【上】

基于原始套接字编程 在开发面向连接的TCP和面向无连接的UDP程序时,我们所关心的核心问题在于数据收发层面,数据的传输特性由TCP或UDP来保证: 也就是说,对于TCP或UDP的程序开发,焦点在Data字段,我们没法直接对TCP或UDP头部字段进行赤裸裸的修改,当然还有IP头.换句话说,我们对它们头部操作的空间非常受限,只能使用它们已经开放给我们的诸如源.目的IP,源.目的端口等等. 今天我们讨论一下原始套接字的程序开发,用它作为入门协议栈的进阶跳板太合适不过了.OK闲话不多说,进入正题. 原始

Unix网络编程随手记——套接字接口函数

套接字接口(socket interface)是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用.大多数现代系统上都实现套接字接口,包括所有的Unix变种.Windows和Macintosh. 1.套接字的基本结构 struct sockaddr 这个结构用来存储套接字地址. 数据定义: 1 struct sockaddr 2 { 3 unsigned short sa_family; /* address族, AF_xxx */ 4 char sa_data[14]; /* 14

Socket编程实践(3) --Socket API

socket函数 #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); 创建一个套接字用于通信 參数: domain:指定通信协议族(protocol family),经常使用取值AF_INET(IPv4) type:指定socket类型, 流式套接字SOCK_STREAM.数据报套接字SOCK_DGRAM,原始套接字SOCK_RAW protoc