网络编程学习(五)_Select模型编程实例(函数详解+代码实例)

一、I/O复用之select原理

I/O多路复用(又被称为“事件驱动”),首先要理解的是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一 个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不 做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll之类的系统调用来实现,这些函数都可以同时 监视多个描述符的读写就绪状况,这样,**多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用 同一个线程。

select系统调用的目的是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。poll和select应该被归类为这样的系统 调用,它们可以阻塞地同时探测一组支持非阻塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是做IO,而是帮助 调用者寻找当前就绪的设备。

下面是select的原理图:
网络编程学习(五)_Select模型编程实例(函数详解+代码实例)_第1张图片

二、函数原型

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

参数说明

nfds: 被监听的文件描述符总数,它会比文件描述符表集合中的文件描述符表的最大值大1,因为文件描述符从0开始计数

readfds:需要监听的可读事件的文件描述符集合

writefds:需要监听的可写事件的文件描述符集合

exceptfds:需要监听的异常事件的文件描述符集合

timeout:即告诉内核select等待多长时间之后就放弃等待。一般设为NULL 表示无动静就阻塞等待
struct timeval
{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
};

返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。

Select()函数配套使用的四个宏

FD_CLR(int 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的全部位

三、代码实例

client.c

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

int main(int argc, char *argv[])
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(9000);

    if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0)
    {
        printf("inet_pton failed exit !\n");
        exit(1);
    }

    if(connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
    {
        printf("connect() failed exit!\n");
        exit(1);        
    }

    int len;
    char recvline[1024]; 
    char writeline[1024];

    while(1)
    {
        memset(recvline, 0, sizeof(recvline));
        memset(writeline, 0, sizeof(writeline));

        //发送消息
		printf("send to server:");
		fgets(writeline,sizeof(writeline),stdin);
		write(sockfd,writeline,strlen(writeline));

        len = read(sockfd,recvline,1024);

        if(len == 0)
        {
            printf("server is close");
        }

        printf("receive from server:%s\n",recvline);
    }

    close(sockfd); //关闭套接字
    printf("end exit !\n");
    return 0;     

}

server.c

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

int main(int argc, char *argv[])
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);

    int on = 1;
    int i;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));//设置为可重复使用的端口

    struct sockaddr_in serv_addr; //服务器的地址结构体
    memset(&serv_addr,0,sizeof(serv_addr));

    //设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
    serv_addr.sin_family = AF_INET;                //选择协议族为IPV4
    serv_addr.sin_port = htons(9000);         //绑定我们自定义的端口号,客户端程序和我们服务器程序通讯时,就要往这个端口连接和传送数据
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听本地所有的IP地址;INADDR_ANY表示的是一个服务器上所有的网卡(服务器可能不止一个网卡)多个本地ip地址都进行绑定端口号,进行侦听。

    bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    listen(listenfd, 32);

    int maxfd = listenfd;

    int client[FD_SETSIZE];//数组用于存放客服端fd 循环查询使用 FD_SETSIZE 是一个宏 默认已经最大1024
    fd_set rset;//放在select中 集合中都是有动静的fd 一般就一个
    fd_set allset;//只做添加fd
    FD_ZERO(&rset);//清空的动作
    FD_ZERO(&allset);//清空的动作

    FD_SET(listenfd,&allset);

    //初始化数组都为-1 应为标识符从0开始
    for(i=0; i<FD_SETSIZE; i++)
    {
        client[i] = -1;
    }

    int nReady;
    int connfd;

    while(1)
    {
        rset = allset;//保证每次循环 select能监听所有的文件描述符 因为rset只会剩下有动静的
        nReady = select(maxfd+1,&rset,NULL,NULL,NULL);

        if(FD_ISSET(listenfd, &rset))
        {
            struct sockaddr_in clientaddr;
            memset(&clientaddr, 0, sizeof(clientaddr));
            int len = sizeof(clientaddr);

            connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);

            char ipstr[128];//打印用到
            printf("client ip%s ,port %d\n",inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,ipstr,sizeof(ipstr)),
                   ntohs(clientaddr.sin_port));
            
            for(i=0; i<FD_SETSIZE; i++)
            {
                if(client[i] < 0)
                {
                    client[i] = connfd;
                    break;
                }
            }

            FD_SET(connfd, &allset);

            if(connfd > maxfd)
            {
                maxfd = connfd;
            }

            if(--nReady <= 0)
            {
                continue;
            }
        }
        else
        {
            for(i=0; i<FD_SETSIZE; i++)
            {
                if(FD_ISSET(client[i], &rset))
                {
                    connfd = client[i];
                    char buf[1024] = {0};
                    int nread;
                    nread = read(connfd, buf, sizeof(buf));
                    if(nread == 0)
                    {
                         //四步处理 打印说明 从集合中删除  从数组中删除 关闭客服端
                        printf("client is close..\n");
                        FD_CLR(connfd, &allset);
                        client[i] = -1;
                        close(connfd);                       
                    }
                    else
                    {
                        write(connfd,buf,nread);
                        memset(buf,0,1024); 
                    }

                    if(--nReady <= 0)
                        break;
                }
            }
        }
    }

    return 0;    
}

四、select模型的缺点

最大并发数限制,因为一个进程所打开的 fd(文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024,并且集合描述符最大也只能为1024,因此 select 模型的最大并发数就被相应限制了。

效率问题,采用循环的方式匹配数组内的fd是否在产生的动静集合中,如果连接的客户端数量很多,那么效率可想而知。

内核 / 用户空间 内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法,在FD非常多的时候,非常的耗费时间。

你可能感兴趣的:(网络编程,学习,网络,socket,c++)