网络编程(16)—— IO复用技术之select

        IO复用技术也是解决多客户端访问单个服务器的方法,它的本质是将I/O的使用分成若干时间片,提供给不同的socket使用。本文要介绍的Windows和Linux中的select函数就是IO复用技术的一种,其函数的原型如下:

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
        我们先重点关注下第2、3、4个参数的参数类型fd_set类型的指针。fd_set实际上是一个位数组,也就是说每个数组成员只能是0或者1的标志位。它包含的信息其实就是我们需要使用的socket的文件描述符。我们知道socket文件描述符的本质就是一个int类型的数字,它要保存socket文件描述符信息,比如为3,就其关联到fd_set位数组的第3位。仔细观察这三个参数的名称,相信你也会明白,他们分别关联着读、写和发生异常的socket文件描述符。使用select时我们需要先将套接字描述符在fd_set变量中注册(读写的socket文件描述符分别在各自的fd_set中注册),然后每调用一次select,如果某个socket文件描述符有数据需要读或者写,其在数组中对应的位就会被置1。这些过程都是通过宏进行操作的,下面是这些宏的用法:

FD_ZERO(fd_set*);//将fd_set指向的变量初始化清零
FD_SET(int fd,fd_set*);//在fd_set指向的变量中注册文件描述符fd 
FD_CLR(int fd,fd_set*);//在fd_set指向的变量中清除文件描述符fd 
FD_ISSET(int fd,fd_set*);//若fd_set指向的变量中含有文件操作符fd的信息,则返回真 

        再看下select的其他参数,第1个参数是这些文件操作符的个数,因为文件操作符都是有序的整数,该值实际上是最大文件操作符加1,最后一个参数是超时时间。select是阻塞参数,当等待超时间隔设置的时间段后各个文件操作符都没变化,其将返回0。超时间隔是timeval类型的指针,其定义如下
 
struct timeval 
{ 
    long tv_sec;//秒 
    long tv_usev;//毫秒 
} 
timeval只含有两个成员,分别对应秒数和毫秒数,这个比较容易理解。下面将通过例子讲解select的使用方法,该例子演示了使用select用于标准输入和输出。在linux中,0代表标准输入的文件描述符,1代表标准输出的文件描述符:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/time.h>
#include<sys/select.h>

#define BUF_SIZE 30

int main()
{
    fd_set reads,temps;
    int result,str_len;
    char buf[BUF_SIZE];
    struct timeval timeout;

    //reads清零
    FD_ZERO(&reads);
    //注册标准输入的文件描述符,0是标准输入的文件描述符
    FD_SET(0,&reads);

    while(1)
    {
        temps=reads;
        timeout.tv_sec=5;
        timeout.tv_usec=0;
        result=select(1,&temps,0,0,&timeout);
        if(result==-1)
        {
            puts("select error!");
            break;
        }
        else if(result == 0)
        {
            puts("time out!");
        }
        else
        {
            if(FD_ISSET(0,&temps))
            {
                str_len=read(0,buf,BUF_SIZE);
                buf[str_len]=0;
                printf("message fron stdin:%s\n",buf);
            }
        }
    }
    return 0;
}

第11行,我们声明了两个位数组reads和temps供后面进行使用,分别用来注册标准输入文件描述符和作为中间临时变量。

第17行,利用宏将reads清零。

第19行,利用宏将标准输入的文件描述符注册到reads中。

第23行,这里面我们利用temps保存了一个reads的副本,并且第26行传给select的是temps而非reads,主要原因是我们没select一次,被注册的文件描述符状态(有数据读或写)发生状况后,select中传入的位数组就会发生变化,简单说来就是文件描述符对应的位由0变成1,而我们再次想要select进行第二次文件描述符的监视时,又需要把该位由1变成0.所有我们这里复制了一个reads的副本,每次都是把副本穿个select,而可以保证reads里面的数据位不变,都在0的状态。

第24、25行是设置最长等待时间,这里我们设置了5秒。

第26行,我们调用select并接受其返回值。select是一个阻塞函数,如果没有文件操作符状态的变化,它会发生阻塞,直到有数据可读或可写或者过了我们设置的等待时间。它返回-1时,说明发生了错误;它返回0时,说明阻塞等待超时;它返回1时说明文件操作符状态发生了变化,那就可以进行下一步的操作了。

第28行的意思是利用宏检查是不是标准输入的文件描述符(值为0)发生了状态的变化,如果是宏将返回1,接下来我们就可以按照正常的流程操作文件描述符的读写了。这里你可能会发现问题:难道每次都要挨个检查文件描述符发生了变化?是的,我们必须挨个进行检查,如果文件描述符多的话,我们把它发到一个数组里,每次seelct后必须遍历这个数组来检查哪些文件描述符发生了变化。这也是select存在性能瓶颈的一个重要原因。

       上面的代码,实现的功能是用select监视标准输入,若用户输入了数据,读取并进行打印,执行结果如下:

[Hyman@Hyman-PC csdn]$ ./a.out 
hello
message fron stdin:hello

time out!
time out!
当我们执行程序后,很快输入字符串 hello,select会从阻塞状态退出,读取并打印hello;但是当我们在5秒钟内没有输入的话,select就等待超时了,此时就会打印出time out!

        我们可以将之前的回声服务器利用select进行修改后入下,原理都一样,就不再过多解释,只是请大家注意第61行,这里我们为了找到哪些文件操作符发生了状态的变化,遍历了从0开始的所有文件操作符。只所以能够这么实现也是因为每个新的文件操作都是在原来的基础之上加1得到的。


#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/select.h>
#include<sys/socket.h>
#include<arpa/inet.h>

#define BUF_SIZE 30

void error_handling(char* message);

int main(int argc,char* argv[])
{
    int serv_sock,clnt_sock;
    struct sockaddr_in serv_addr,clnt_addr;
    int addr_sz;
    fd_set reads,copy_reads;
    int fd_max;
    struct timeval timeout;
    int fd_num,i;
    int str_len;
    char buf[BUF_SIZE];
    if(argc!=2)
    {
        printf("Uasge %s <port>\n",argv[0]);
        exit(1);
    }

    //创建socket
    serv_sock=socket(AF_INET,SOCK_STREAM,0);
    //准备地址
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));
    //绑定
    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
    {
        error_handling("bind error");
    }
    //监听
    if(listen(serv_sock,5)==-1)
    {
        error_handling("listen error");
    }
    FD_ZERO(&reads);
    FD_SET(serv_sock,&reads);
    fd_max=serv_sock;
    while(1)
    {
        copy_reads=reads;
        timeout.tv_sec=5;
        timeout.tv_usec=0;
        
        if((fd_num=select(fd_max+1,&copy_reads,0,0,&timeout))==-1)
                break;
        if(fd_num==0)
                continue;

        for(i=0;i<fd_max+1;i++)
        {
            if(FD_ISSET(i,&copy_reads))
            {
                if(i==serv_sock)
                {
                    addr_sz=sizeof(clnt_addr);
                    //接收连接
                    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&addr_sz);
                    FD_SET(clnt_sock,&reads);
                    if(fd_max<clnt_sock)
                            fd_max=clnt_sock;
                    printf("connected client: %d \n",clnt_sock);
                }
                else
                {
                    str_len=read(i,buf,BUF_SIZE);
                    if(str_len==0)
                    {
                        FD_CLR(i,&reads);
                        close(i);
                        printf("close client: %d\n",i);
                    }
                    else
                    {
                        write(i,buf,str_len);
                    }
                }
            }
        }

    }
    close(serv_sock);
    return 0;
}

void error_handling(char* message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}

Github位置:

https://github.com/HymanLiuTS/NetDevelopment

克隆本项目:

git clone git@github.com:HymanLiuTS/NetDevelopment.git

获取本文源代码:

git checkout NL16



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值