SOCKET【2】-IO多路复用select

文章目录

  • 前言
  • 一、socket阻塞模式
  • 二、socket非阻塞模式
  • 三、select 相关API介绍与使用
    • 3.1 seletct API
    • 3.2 seletct 使用范例
  • 四、深入理解select 模型
  • 五、用select处理外带数据
  • 六、用select来解决socket中的多客户端问题
  • 总结


前言

提示:展示select如何使用


一、socket阻塞模式

所谓阻塞模式block是指线程或者进程执行这些函数时必须等待直到某一事件的发生才能返回,如果事件不发生,进程或者线程被阻塞,函数不能立即返回

二、socket非阻塞模式

所谓非阻塞方式non-block是指进程或者线程执行这些函数时不必等待某一时间的发生立即返回,通过返回不同的状态码来反应函数的运行状态,如果事件发生函数返回,和阻塞模式一样,若事件不发生则返回一个代码来告知事件未发生,而进程或者线程执行,所以效率较高

三、select 相关API介绍与使用

3.1 seletct API


#include 
#include 
#include 
#include 
int select(int maxfdp, fd_set *readset, fd_set *writeset,fd_set *exceptset, struct timeval *timeout);

参数说明:
maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的;
readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。
timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间
timeval结构体定义如下:
返回值
成功返回大于0的整数,这个整数表示就绪描述符的数目。
超时返回0;
失败返回-1;
错误原因存于errno,此时参数readfds,writefds,exceptfdstimeout的值变成不可预测。errno值:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足

struct timeval
{      
    long tv_sec;   /*秒 */
    long tv_usec;  /*微秒 */   
};

以下介绍与select函数相关的常见的几个宏:

#include 
int FD_ZERO(fd_set *fdset);/*一个fd_set 类型变量的所有位都设为0*/
int FD_CLR(int fd, fd_set *fdset);/*清除某个位时可以使用*/
int FD_SET(int fd, fd_set *fd_set);/*设置变量的某个位置*/
int FD_ISSET(int fd, fd_set *fdset);/*测试某个位是否被置位*/

3.2 seletct 使用范例

  • 当声明一个文件描述符集时,必须使用FD_ZERO将其初始化,所有的位都置0,之后将我们感兴趣的文件描述符添加到描述集中。

    fd_set rset;
    int fd;
    FD_ZERO(&rset);
    FD_SET(fd, &rset);
    FD_SET(stdin,&rset);
    
  • 然后调用select阻塞等待文件描述符事件的到来;如果超过设定的时间,则不再等待,继续往下执行

    select(fd+1;&reset,NULL,NULL,NULL);
    
  • select 返回后使用FD_ISSET测试给定位是否置位

    	if(FD_ISSET(fd,&rset))
    	{
    		....
    		//do something
    	}
    
  • select 模型的关键是使用一种有序的方式,对多个套接字统一管理与调度
    SOCKET【2】-IO多路复用select_第1张图片

如上所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的

四、深入理解select 模型

理解select模型的关键在于理解fd_set位说明方便,取fd_set长度位1字节,fd_set中的每一个bit可以对应一个文件描述符fd,则1字节长的fd_set最大可以对应8个fd。

  1. 执行fd_set;FD_ZERO(&set);则set用位表示是0000,0000
  2. 若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置1)
  3. 若再加入fd=2,fd=1,则set变为0001,0011
  4. 执行select(6,&set,0,0,0)阻塞等待
  5. 若fd=1,fd=2上都发生可读事件,则select返回,此时set 变为0000,0011。注意 :没有事件发生的fd=5被清空

基于以上的讨论.可以轻松得出select模型的特点:

  1. 可监控的文件描述符的上限取决于sizeof(fd_set)的值, 我这里的FD_SETSIZE=1024
  2. 将fd加入select监控集的同时,还要再使用数据结构array保存放到select监控集中的fd.
    一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。
    二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫面array的同时取得fd最大值maxfd,用于select的第一次参数。
  3. 可见select模型必须再select前循环加入fd,取maxfd,select返回后利用FD_ISSET判断是否有事件发生

五、用select处理外带数据

网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据。

什么是带外数据?

带外数据(out—of—band data),有时也称为加速数据(expedited data),
是指连接双方中的一方发生重要事情,想要迅速地通知对方。
这种通知在已经排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。
带外数据设计为比普通数据有更高的优先级。
带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。

我们写的select程序经常都是用于接收普通数据的,当我们的服务器需要同时接收普通数据和带外数据,我们如何使用select进行处理二者呢?

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
 
int main(int argc, char* argv[])
{
    if(argc <= 2)
    {
        printf("usage: ip address + port numbers\n");
        return -1;
    }
    
    const char* ip = argv[1];
    int port = atoi(argv[2]);
 
        printf("ip: %s\n",ip);
        printf("port: %d\n",port);
    
    int ret = 0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port = htons(port);
    
    int listenfd = socket(PF_INET,SOCK_STREAM,0);
    if(listenfd < 0)
    {
        printf("Fail to create listen socket!\n");
        return -1;
    }
    
    ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    if(ret == -1)
    {
        printf("Fail to bind socket!\n");
        return -1;
    }
    
    ret = listen(listenfd,5); //监听队列最大排队数设置为5
    if(ret == -1)
    {
        printf("Fail to listen socket!\n");
        return -1;
    }
    
    struct sockaddr_in client_address;  //记录进行连接的客户端的地址
    socklen_t client_addrlength = sizeof(client_address);
    int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
    if(connfd < 0)
    {
        printf("Fail to accept!\n");
        close(listenfd);
    }
    
    char buff[1024]; //数据接收缓冲区
    fd_set read_fds;  //读文件操作符
    fd_set exception_fds; //异常文件操作符
    FD_ZERO(&read_fds);
    FD_ZERO(&exception_fds);
    
    while(1)
    {
        memset(buff,0,sizeof(buff));
        /*每次调用select之前都要重新在read_fds和exception_fds中设置文件描述符connfd,因为事件发生以后,文件描述符集合将被内核修改*/
        FD_SET(connfd,&read_fds);
        FD_SET(connfd,&exception_fds);
        
        ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL);
        if(ret < 0)
        {
            printf("Fail to select!\n");
            return -1;
        }
        
        
        if(FD_ISSET(connfd, &read_fds))
        {
            ret = recv(connfd,buff,sizeof(buff)-1,0);
            if(ret <= 0)
            {
                break;
            }
            
            printf("get %d bytes of normal data: %s \n",ret,buff);
            
        }
        else if(FD_ISSET(connfd,&exception_fds)) //异常事件
        {
            ret = recv(connfd,buff,sizeof(buff)-1,MSG_OOB);
            if(ret <= 0)
            {
                break;
            }
            
            printf("get %d bytes of exception data: %s \n",ret,buff);
        }
        
    }
    
    close(connfd);
    close(listenfd);
    
    
    return 0;
}

六、用select来解决socket中的多客户端问题

上面提到过,,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。在网络编程中,当涉及到多客户访问服务器的情况,我们首先想到的办法就是fork出多个进程来处理每个客户连接。现在,我们同样可以使用select来处理多客户问题,而不用fork。
服务器端

#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include 
 
int main() 
{ 
    int server_sockfd, client_sockfd; 
    int server_len, client_len; 
    struct sockaddr_in server_address; 
    struct sockaddr_in client_address; 
    int result; 
    fd_set readfds, testfds; 
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket 
    server_address.sin_family = AF_INET; 
    server_address.sin_addr.s_addr = htonl(INADDR_ANY); 
    server_address.sin_port = htons(8888); 
    server_len = sizeof(server_address); 
    bind(server_sockfd, (struct sockaddr *)&server_address, server_len); 
    listen(server_sockfd, 5); //监听队列最多容纳5个 
    FD_ZERO(&readfds); 
    FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中
    while(1) 
    {
        char ch; 
        int fd; 
        int nread; 
        testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量 
        printf("server waiting\n"); 
 
        /*无限期阻塞,并测试文件描述符变动 */
        result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //FD_SETSIZE:系统默认的最大文件描述符
        if(result < 1) 
        { 
            perror("server5"); 
            exit(1); 
        } 
 
        /*扫描所有的文件描述符*/
        for(fd = 0; fd < FD_SETSIZE; fd++) 
        {
            /*找到相关文件描述符*/
            if(FD_ISSET(fd,&testfds)) 
            { 
              /*判断是否为服务器套接字,是则表示为客户请求连接。*/
                if(fd == server_sockfd) 
                { 
                    client_len = sizeof(client_address); 
                    client_sockfd = accept(server_sockfd, 
                    (struct sockaddr *)&client_address, &client_len); 
                    FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中
                    printf("adding client on fd %d\n", client_sockfd); 
                } 
                /*客户端socket中有数据请求时*/
                else 
                { 
                    ioctl(fd, FIONREAD, &nread);//取得数据量交给nread
                    
                    /*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
                    if(nread == 0) 
                    { 
                        close(fd); 
                        FD_CLR(fd, &readfds); //去掉关闭的fd
                        printf("removing client on fd %d\n", fd); 
                    } 
                    /*处理客户数据请求*/
                    else 
                    { 
                        read(fd, &ch, 1); 
                        sleep(5); 
                        printf("serving client on fd %d\n", fd); 
                        ch++; 
                        write(fd, &ch, 1); 
                    } 
                } 
            } 
        } 
    } 
 
    return 0;
}
	
**客户端**
	
```c
//客户端
#include  
#include  
#include  
#include  
#include  
#include  
#include 
#include 
 
int main() 
{ 
    int client_sockfd; 
    int len; 
    struct sockaddr_in address;//服务器端网络地址结构体 
     int result; 
    char ch = 'A'; 
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket 
    address.sin_family = AF_INET; 
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(8888); 
    len = sizeof(address); 
    result = connect(client_sockfd, (struct sockaddr *)&address, len); 
    if(result == -1) 
    { 
         perror("oops: client2"); 
         exit(1); 
    } 
    //第一次读写
    write(client_sockfd, &ch, 1); 
    read(client_sockfd, &ch, 1); 
    printf("the first time: char from server = %c\n", ch); 
    sleep(5);
    
    //第二次读写
    write(client_sockfd, &ch, 1); 
    read(client_sockfd, &ch, 1); 
    printf("the second time: char from server = %c\n", ch);
    
    close(client_sockfd); 
   
    return 0; 
}

总结

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  1. 单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以cat/proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

  2. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

  3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

你可能感兴趣的:(#,linux,计算机网络,网络,select,socket)