select

1 select 函数原型

#include <sys/select.h>
/* According to earlier standards */
#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);
/*
	nfds: 		监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
	readfds:	监控有读数据到达文件描述符集合,传入传出参数
	writefds:	监控写数据到达文件描述符集合,传入传出参数
	exceptfds:	监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
	timeout:	定时阻塞监控时间,3种情况
				1.NULL,永远等下去
				2.设置timeval,等待固定时间
				3.设置timeval里时间均为0,检查描述字后立即返回,轮询
	struct timeval {
		long tv_sec;  	//seconds
		long tv_usec; 	// microseconds
	};

返回值:
	> 0 : 所有监听集合中,满足对于事件的总数
	0   : 没有满足监听条件的文件描述符
	-1  : errno
*/
	void FD_CLR(int fd, fd_set *set); 	//把文件描述符集合set里fd位清0
	int FD_ISSET(int fd, fd_set *set); 	//文件描述符集合set里fd是否置1		返回值:1-已经置1; 0-没有置1
	void FD_SET(int fd, fd_set *set); 	//把文件描述符集合里fd位置1
	void FD_ZERO(fd_set *set); 			//把文件描述符集合里所有位清0


2 select实现多路IO转接(代码)

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


int main()
{
    int listenfd, connfd;   // 监听套接字、连接套接字
    int maxfd = 0;          // 最大的套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == listenfd)
    {
        perror("socket error");
        exit(1);
    }

    // 端口复用
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in saddr, caddr;

    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port   = htons(8000);
    saddr.sin_addr.s_addr = inet_addr("192.168.71.132");
    int ret = bind(listenfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if (-1 == ret)
    {
        perror("bind error");
        close(listenfd);
        exit(1);
    }

    ret = listen(listenfd, 5);
    if (-1 == ret)
    {
        perror("listen error");
        exit(1);
    }

    maxfd = listenfd;                       // 最大文件描述符

    fd_set rset, allset;                    // 定义读集合rset、备份集合allset
    FD_ZERO(&rset); FD_ZERO(&allset);       // 清空集合
    FD_SET(listenfd, &allset);              // 将listenfd添加到allset集合中

    int nReady = 0;
    while (1)
    {
        rset = allset;
        nReady = select(maxfd+1, &rset, NULL, NULL, NULL);   // 阻塞监听等待有事件发生
        if (-1 == nReady)
        {
            perror("select error");
            exit(1);
        }
        else if(0 == nReady)	// 因为是阻塞的监听,所以此条件一定不满足
        {
        }
        else
        {
            if (FD_ISSET(listenfd, &rset))      // 满足监听 读事件
            {
                socklen_t caddrLen = sizeof(caddr);
                connfd = accept(listenfd, (struct sockaddr*)&caddr, &caddrLen); // 与客户端连接
                if (-1 == connfd)
                {
                    perror("accept error");
                    exit(1);
                }
                printf("建立连接成功:%s:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

                FD_SET(connfd, &allset);        // 将新产生的fd,添加到allset集合中,监听数据读事件

                if (maxfd < connfd)             // 修改maxfd
                {
                    maxfd = connfd;
                }

                if (nReady == 1)                // 说明select只返回一个,并且是listenfd,无需后续执行
                {
                    continue;
                }
            }

            for (int ii = listenfd+1; ii <= maxfd; ++ii)     // 处理满足读事件的fd
            {
                if (FD_ISSET(ii, &rset))                     // 找到满足读事件的fd
                {
                    // 通信
                    char buf[1024] = {0};
                    memset(buf, 0, sizeof(buf));
                    int n = recv(ii, buf, sizeof(buf), 0);
                    if (-1 == n)
                    {
                        perror("recv error");
                        exit(1);
                    }
                    else if (0 == n)                         // 监测到客户端已经关闭连接
                    {
                        printf("断开连接\n");
                        FD_CLR(ii, &allset);                 // 移除出监听集合,关闭fd
                        close(ii);
                        continue;
                    }
                    printf("Recv:%s\n", buf);
                }
            }
        }
    }
    
    close(listenfd);
    return 0;
}


3 select优缺点

  • 缺点

    • 监听上限文件描述符。最大1024
    • 监测满足条件的fd是轮询模式,需要自己添加业务逻辑提高效率,增加了编码难度
  • 优点

    • 跨平台。win、Linux、macOS、Unix、类Unix、mips

4 优化

监测满足条件的fd是轮询模式,需要自己添加业务逻辑提高效率。

可以通过添加一个数组,将需要监听的connfd(与客户端连接的套件字)加入到此数组中,通过轮询数组而不是轮询到最大的文件描述符。以此来提高效率。

以下为参考代码

//IO多路复用技术select函数的使用(优化)
// INET_ADDRSTRLEN /* #define INET_ADDRSTRLEN 16 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>

int main()
{
	int i;
	int n;
	int lfd;
	int cfd;
	int ret;
	int nready;
	int maxfd;//最大的文件描述符
	char buf[FD_SETSIZE];	// FD_SETSIZE默认为1024
	socklen_t len;
	int maxi;  //有效的文件描述符最大值
	int connfd[FD_SETSIZE]; //有效的文件描述符数组
	fd_set tmpfds, rdfds; //要监控的文件描述符集
	struct sockaddr_in svraddr, cliaddr;

	//创建socket
	lfd = socket(AF_INET, SOCK_STREAM, 0);
	if(lfd<0)
	{
		perror("socket error");
		return -1;
	}

	//允许端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

	//绑定bind
	svraddr.sin_family = AF_INET;
	svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	svraddr.sin_port = htons(8888);
	ret = bind(lfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));
	if(ret<0)
	{
		perror("bind error");
		return -1;
	}

	//监听listen
	ret = listen(lfd, 5);
	if(ret<0)
	{
		perror("listen error");
		return -1;
	}

	//文件描述符集初始化
	FD_ZERO(&tmpfds);
	FD_ZERO(&rdfds);

	//将lfd加入到监控的读集合中
	FD_SET(lfd, &rdfds);

	//初始化有效的文件描述符集, 为-1表示可用, 该数组不保存lfd
	for(i=0; i<FD_SETSIZE; i++)
	{
		connfd[i] = -1;
	}

	maxfd = lfd;
	len = sizeof(struct sockaddr_in);

	//将监听文件描述符lfd加入到select监控中
	while(1)
	{
		//select为阻塞函数,若没有变化的文件描述符,就一直阻塞,若有事件发生则解除阻塞,函数返回
		//select的第二个参数tmpfds为输入输出参数,调用select完毕后这个集合中保留的是发生变化的文件描述符
		tmpfds = rdfds;
		nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
		if(nready>0)
		{
			//发生变化的文件描述符有两类, 一类是监听的, 一类是用于数据通信的
			//监听文件描述符有变化, 有新的连接到来, 则accept新的连接
			if(FD_ISSET(lfd, &tmpfds))	
			{
				cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);			
				if(cfd<0)
				{
					if(errno==ECONNABORTED || errno==EINTR)
					{
						continue;
					}
					break;
				}

				//先找位置, 然后将新的连接的文件描述符保存到connfd数组中
				for(i=0; i<FD_SETSIZE; i++)
				{
					if(connfd[i]==-1)
					{
						connfd[i] = cfd;
						break;
					}
				}
				//若连接总数达到了最大值,则关闭该连接
				if(i==FD_SETSIZE)
				{	
					close(cfd);
					printf("too many clients, i==[%d]\n", i);
					//exit(1);
					continue;
				}

				//确保connfd中maxi保存的是最后一个文件描述符的下标
				if(i>maxi)
				{
					maxi = i;
				}

				//打印客户端的IP和PORT
				char sIP[16];
				memset(sIP, 0x00, sizeof(sIP));
				printf("receive from client--->IP[%s],PORT:[%d]\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, sIP, sizeof(sIP)), htons(cliaddr.sin_port));

				//将新的文件 描述符加入到select监控的文件描述符集合中
				FD_SET(cfd, &rdfds);
				if(maxfd<cfd)
				{
					maxfd = cfd;
				}

				//若没有变化的文件描述符,则无需执行后续代码
				if(--nready<=0)
				{
					continue;
				}	
			}

			//下面是通信的文件描述符有变化的情况
			//只需循环connfd数组中有效的文件描述符即可, 这样可以减少循环的次数
			for(i=0; i<=maxi; i++)
			{
				int sockfd = connfd[i];
				//数组内的文件描述符如果被释放有可能变成-1
				if(sockfd==-1)
				{
					continue;
				}

				if(FD_ISSET(sockfd, &tmpfds))
				{
					memset(buf, 0x00, sizeof(buf));
					n = read(sockfd, buf, sizeof(buf));
					if(n<0)
					{
						perror("read over");
						close(sockfd);
						FD_CLR(sockfd, &rdfds);
						connfd[i] = -1; //将connfd[i]置为-1,表示该位置可用
					}
					else if(n==0)
					{
						printf("client is closed\n");	
						close(sockfd);
						FD_CLR(sockfd, &rdfds);
						connfd[i] = -1; //将connfd[i]置为-1,表示该位置可用
					}
					else
					{
						printf("[%d]:[%s]\n", n, buf);
						write(sockfd, buf, n);
					}

					if(--nready<=0)
					{
						break;  //注意这里是break,而不是continue, 应该是从最外层的while继续循环
					}
				}	
			}
		}	
	}

	//关闭监听文件描述符
	close(lfd);

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值