(二)socket编程(四)

目录

socket编程(十一)

套接字I/O超时设置方法

一)使用alarm 函数设置超时

(二)套接字选项: SO_SNDTIMEO, SO_RCVTIMEO,调用setsockopt设置读/写超时时间

(三)使用select函数实现超时

用select实现超时

read_timeout函数封装

write_timeout函数封装

accept_timeout函数封装

connect_timeout函数封装

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

socket编程(十二)

select限制

 poll

socket编程(十三)

epoll使用

epoll与select、poll区别

epoll LT/ET模式


16socket编程(十一)

套接字超时就是当套按字在你所设定的时间内没有读或写事件发生,那么就会返回0,你可以根据这个返回值进行处理,继续等待或中断或其他操作

套接字I/O超时设置方法

一、使用alarm 函数设置超时

#include <unistd.h> 
unsigned int alarm(unsigned int seconds); 

它的主要功能是设置信号传送闹钟。

信号SIGALRM在经过seconds指定的秒数后传送给目前的进程,如果在定时未完成的时间内再次调用了alarm函数,则后一次定时器设置将覆盖前面的设置,当seconds设置为0时,定时器将被取消。

它返回上次定时器剩余时间,如果是第一次设置则返回0。
 

void sigHandlerForSigAlrm(int signo)  
{  
    return ;  
}  
  
signal(SIGALRM, sigHandlerForSigAlrm);  
alarm(5);  
int ret = read(sockfd, buf, sizeof(buf));  
if (ret == -1 && errno == EINTR)  
{  
    // 阻塞并且达到了5s,超时,设置返回错误码  
    errno = ETIMEDOUT;  
}  
else if (ret >= 0)  
{  
    // 正常返回(没有超时), 则将闹钟关闭  
    alarm(0);  
}  

如果read一直处于阻塞状态被SIGALRM信号中断而返回,则表示超时,否则未超时已读取到数据,取消闹钟。但这种方法不常用,因为有时可能在其他地方使用了alarm会造成混乱。

(二)套接字选项: 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;  
}  

   SO_RCVTIMEO是接收超时,SO_SNDTIMEO是发送超时。这种方式也不经常使用,因为这种方案不可移植,并且有些套接字的实现不支持这种方式。

(三)使用select函数实现超时

#include <sys/select.h>   
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

返回:做好准备的文件描述符的个数,超时为0,错误为 -1.

用select实现超时

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;  
} 

FD_ZERO宏将一个 fd_set类型变量的所有位都设为 0,使用FD_SET将变量的某个位置位。清除某个位时可以使用 FD_CLR,我们可以使用FD_ISSET来测试某个位是否被置位。   

当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位,操作如

fd_set rset;   
int fd;   
FD_ZERO(&rset);   
FD_SET(fd, &rset);   
FD_SET(stdin, &rset);

 select返回后,用FD_ISSET测试给定位是否置位:

if(FD_ISSET(fd, &rset)   
{ ... }

write_timeout函数封装

实现方式和read_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;  
} 

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;  
}

connect_timeout函数封装

/* 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来获取一下。退出之前还需重新将套接字设置为阻塞。 

17socket编程(十二)

select限制

用select实现的并发服务器,能达到的并发数,受两方面限制

  • 1、一个进程能打开的最大文件描述符限制。这可以通过调整内核参数。可以通过ulimit -n来调整或者使用setrlimit函数设置, 但一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看
  • 2、select中的fd_set集合容量的限制(FD_SETSIZE,一般为1024) ,这需要重新编译内核。

 poll

函数原形:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

所属头文件:

#include <poll.h>

返回值

-1:出错了,错误代码在errno中
 0:设置了超时时间,这里表示超时了
>0:数组中fds准备好读、写、或异常的那些描述符的总数量

参数说明:

fds:一般是一个struct pollfd类型的数组,
nfds:要监视的描述符的数目。
timeout:超时时间,-1表示不会超时。0表示立即返回,不阻塞进程。 >0表示等待数目的毫秒数。



struct pollfd {
  int   fd;         /* file descriptor */
  short events;     /* requested events  请求的事件,具体哪些值见下面 */
  short revents;    /* returned events  返回的事件,有点像传出参数。哪个事件发生了就存储在这里*/
           };

events和revents的值可以是下面:

 

 

18socket编程(十三)

epoll使用

函数原形:

 #include <sys/epoll.h>
 int epoll_create(int size);
 int epoll_create1(int flags);
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数说明:

1、epoll_create1 产生一个epoll 实例,返回的是实例的句柄。flag 可以设置为0 或者EPOLL_CLOEXEC,为0时函数表现与epoll_create一致,EPOLL_CLOEXEC标志与open 时的O_CLOEXEC 标志类似,即进程被替换时会关闭打开的文件描述符。

2、epoll_ctl :

(1)epfd:epoll 实例句柄;

(2)op:对文件描述符fd 的操作,主要有EPOLL_CTL_ADD、 EPOLL_CTL_DEL等;

(3)fd:需要操作的目标文件描述符;

(4)event:结构体指针

  typedef union epoll_data {
                void        *ptr;
                int          fd;
                uint32_t     u32;
                uint64_t     u64;
            } epoll_data_t;
 
  struct epoll_event {
  uint32_t     events;      /* Epoll events */
                 epoll_data_t  data;        /* User data variable */
            };
 

events 参数主要有EPOLLIN、EPOLLOUT、EPOLLET、EPOLLLT等;一般data 共同体我们设置其成员fd即可,也就是epoll_ctl 函数的第三个参数。

3、epoll_wait:

(1)epfd:epoll 实例句柄;

(2)events:结构体指针

(3)maxevents:事件的最大个数

(4)timeout:超时时间,设为-1表示永不超时

 

epoll与select、poll区别

  • 1、相比于select与poll,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。内核中的select与poll的实现是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
  • 2、epoll的实现是基于回调的,如果fd有期望的事件发生就通过回调函数将其加入epoll就绪队列中,也就是说它只关心“活跃”的fd,与fd数目无关。
  • 3、epoll不仅会告诉应用程序有I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个fd集合。
  • 4、当已连接的套接字数量不太大,并且这些套接字都非常活跃,那么对于epoll 来说一直在调用callback 函数(epoll 内部的实现更复杂,更复杂的代码逻辑),可能性能没有poll 和 select 好,因为一次性遍历对活跃的文件描述符处理,在连接数量不大的情况下,性能更好,但在处理大量连接的情况时,epoll 明显占优

epoll LT/ET模式

1、EPOLLLT:完全靠kernel epoll驱动,应用程序只需要处理从epoll_wait返回的fds,这些fds我们认为它们处于就绪状态。此时epoll可以认为是更快速的poll。

2、EPOLLET:此模式下,系统仅仅通知应用程序哪些fds变成了就绪状态,一旦fd变成就绪状态,epoll将不再关注这个fd的任何状态信息,(从epoll队列移除)直到应用程序通过读写操作(非阻塞)触发EAGAIN状态,epoll认为这个fd又变为空闲状态,那么epoll又重新关注这个fd的状态变化(重新加入epoll队列)。随着epoll_wait的返回,队列中的fds是在减少的,所以在大并发的系统中,EPOLLET更有优势,但是对程序员的要求也更高,因为有可能会出现数据读取不完整的问题,举例如下:

假设现在对方发送了2k的数据,而我们先读取了1k,然后这时调用了epoll_wait,如果是边沿触发,那么这个fd变成就绪状态就会从epoll 队列移除,很可能epoll_wait 会一直阻塞,忽略尚未读取的1k数据,与此同时对方还在等待着我们发送一个回复ack,表示已经接收到数据;如果是电平触发,那么epoll_wait 还会检测到可读事件而返回,我们可以继续读取剩下的1k 数据。

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值