Linux下高级I/O多路转接之select服务器

系统提供select函数来实现多路复用输入/输出模型。select系统调用是用来让我们的程序监视 多个文件句柄的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或 多个发生了状态改变。关于文件句柄(socket),其实就是一个整数,我们最熟悉的句柄是0、1、2三 个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE * 结构的表示就是stdin、stdout、stderr。

select函数:
Linux下高级I/O多路转接之select服务器_第1张图片
参数:
在介绍参数前先来说说第二,三,四个参数的类型:fd_set
fd_set:这是一个独立的类型,为说明方便,取fd_set长度为1字节,fd_set中的每一个bit位 可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set 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被清空。

nfds是select函数要监视的文件描述符的最大值加1。

readfds,writefds,exceptfds,分别代表着可读文件描述符,可写文件描述符,和异常文件描述符的集合,对于需要监视的文件描述符,可以根据他们是要进行怎样的读写操作进行设定。当没有可监视的文件描述符时,设置为NULL。

timeout是一个结构体参数,用于描述一段时间,在这段时间里,如果没有要监视的文件描述符,读写就绪,那么select函数会返回0。

这里写图片描述
结构体中有两个成员变量,分别代表两个不同单位的时间变量。

返回值:
监视到有文件描述符读写就绪时,返回就绪文件描述符的个数,当超出timeout中设置的时间时,还没有文件描述符就绪时,返回0,函数出错时返回-1。

对于select函数还有四个宏函数来对其监视的三个文件描述符集进行设置:
FD_CLR(inr fd,fd_set* set);用来清除描述词组set中相关fd 的位
FD_ISSET(int fd,fd_set *set);用来测试描述词组set中相关fd 的位是否为真
FD_SET(int fd,fd_set*set);用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set);用来清除描述词组set的全部位

下面是我写的一个基于select函数的多路转接服务器:

#include
#include
#include
#include
#include
#include
#include
#include
#include

#define FDS_NUM (sizeof(fd_set)*8)


int fds[FDS_NUM];//创建一个数组来保存要监视的文件描述

static void usage(char* proc)
{
    printf("usage:%s [local_ip] [local_port]\n",proc);
}

int startup(char* ip,int port)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        exit(2);
    }

    int opt = 1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = inet_addr(ip);

    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
        perror("bind");
        exit(3);
    }

    if(listen(sock,5)<0)
    {
        perror("listen");
        exit(4);
    }
    return sock;
}



int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        return 1;
    }
    int listen_sock = startup(argv[1],atoi(argv[2]));//封装一个创建listen_sock的函数
    fd_set rfds;//创建一个读字符集

    int i = 0;
    for(;i1;
    }
    while(1)
    {
        FD_ZERO(&rfds);
        int max = 0;//select参数nfds设置
        struct timeval timeout = {1,0};//select参数timeout设置
        fds[0] = listen_sock;//将创建好的监听套接字放到保存数组中
        for(i=0;i//将保存数组中的文件描述符设置进select的监视文件描述符集
        {
            if(fds[i]>0)
            {
                FD_SET(fds[i],&rfds);
                if(max<=fds[i])
                {
                    max = fds[i]+1;
                }
            }
        }
        switch(select(max,&rfds,NULL,NULL,&timeout))
        {
            case 0:
                printf("wait timeout!\n");//函数超时
                break;
            case -1:
                perror("select");//函数出错
                break;
            default://函数成功,返回有事件发生的fd的个数
                {

                    for(i=0;iif(fds[i] == -1)
                            continue;

                        if((i == 0) && (FD_ISSET(fds[i],&rfds)))//有外部链接访问
                        {
                            struct sockaddr_in client;
                            socklen_t len = sizeof(client);
                            int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
                            if(new_sock < 0)
                            {
                                perror("accept");
                                close(new_sock);
                                break;
                            }
                            printf("get a client#[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
                            int j = 0;
                            for(;jif(fds[j] == -1)//将new_sock添加到保存数组中
                                {
                                    fds[j] = new_sock;
                                    break;
                                }
                                if(j == FDS_NUM)//select的文件描述符集达到上限
                                {
                                    close(new_sock);
                                }
                            }
                        }
                        else if((i != 0)&&(FD_ISSET(fds[i],&rfds)))//读事件就绪
                        {
                            char buf[1024];

                                ssize_t s = read(fds[i],buf,sizeof(buf)-1);
                                if(s > 0)//读到数据
                                {
                                    buf[s] = 0;
                                    printf("client: %s\n",buf);
                                }else if(s == 0)//链接断开
                                {
                                    printf("client quit!\n");
                                    close(fds[i]);
                                    fds[i] = -1;
                                    break;
                                }else
                                {
                                    perror("read");
                                    close(fds[i]);
                                    fds[i] = -1;
                                    break;
                                }

                        }   
                    }
                }
                break;
        }
    }
    return 0;
}

由于select函数每次返回时都会把没有发生事件发生的文件描述符清除掉,因此每次调用时都需要把要监视的文件描述符重新设定进select函数所监视的文件描述符集,因此我们创建一个数组,来保存,所要监视的文件描述符集,也便于把要监视的文件描述符设定进select函数的文件描述符集。

当外部有链接请求时,select会监听到listen_sock事件就绪,并返回1,然后利用accept获得这个链接,成功建立连接后,会产生一个新的文件描述符,把这个新的文件描述符放到保存数组中,然后select函数结束,这时就要把保存数组中的文件描述符重新设定进select的文件描述符集中,select函数又开始监听。因此select函数需要放在一个死循环中

select服务器的优缺点:
优点:
1、始终只有一个进程
2、select目前几乎在所有的平台上都支持,其良好跨平台支持也是它的一个优点
缺点:
1、代码编写时,调用前需要特别设定
2、当用户增多时,函数会不停的在内核态和用户态之间交替
3、每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
4、同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
5、select最大的缺点就是支持的文件描述符数量太小了,默认是1024,也就是说最多同时支持1023个用户同时连接服务器

你可能感兴趣的:(网络编程,Linux,linux,select,socket,io)