高并发服务器-----Select模型详解(代码实例+函数详解)

概要介绍

        一般情况下,处理socket通信一个线程或者进程在处理读写的时候要么阻塞在那一直等要么非阻塞然后过会查看是否可读可写,这样会浪费大量的资源,假如需要对多个套接字进行处理读写那么得开很多个线程或者进程,IO复用技术就是解决这个问题。本节详细讲解IO复用模型 select。
       解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
往期文章
        Socket实现消息互发+函数详解
        Socket实现会射服务器

select()函数

#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);

参数说明:

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

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

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

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

  • timeout:即告诉内核select等待多长时间之后就放弃等待。一般设为NULL 表示无动静就阻塞等待

  • 返回值:表示产生动静的文件描述符个数`

函数作用:
监听等待参数集合中的两类文件描述符产生动静,产生动静后集合中只会剩下产生动静的文件描述符
在这里插入图片描述
①sockfd:专门检查有没有客服端连接的监听套接字 ------数量 1
②connfd:已经连接成功的客服端通信套接字 ------数量 n

实例补充说明:

select(maxfd+1,&rset,NULL,NULL,NULL);

由于参数二放入集合,参数一填入数字,其余位置皆为NULL,
表示我们监听的是rset文件描述符集合是否有可读事件(动静),如果有则该集合只会保留产生可读事件(动静)的文件描述符,否则就阻塞等待。
可读事件分为以下三种

  1. 新客服端连接(此时唯一sockfd放入集合)
  2. 旧客服端发消息过来(此时对应的connfd放入集合)
  3. 旧客户端断开连接(此时对应的connfd放入集合)

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

  • FD_ZERO(fd_set* set)
    理解为初始化文件描述符几何 (既把所有位都设置为0)

  • FD_SET(fd,fd_set* set)
    理解为把文件描述符加入集合(本质为对应fd位设置为1)

  • FD_CLR(fd,fd_set* set)
    理解为把文件描述符从集合取出(本质为对应fd位设置为0)

  • FD_ISSET(fd,fd_set* set)
    理解为检测改文件描述符是否在集合(本质为对应的fd位是否设置为

select模型的缺点

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

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

  3. 内核 / 用户空间 内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法,在FD非常多的时候,非常的耗费时间。
    第三点参考链接
    总结:前面两点缺陷在代码中都有体现。接下来直接参考代码

select模型代码实例—简单的服务器回射打印(附解释)

客户端代码如下

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <signal.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#define SET_PORT 8000
int main(int argc, char *argv[])
{
    int sockfd,connfd;//监听套接字 和连接套接字
    struct sockaddr_in serveraddr;
    int i;//主要用于各种for循环的i
    int on = 1;//只有下方设置可重复性使用的端口用到
     //1.创建监听套接字
    sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); 
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) ;//设置为可重复使用的端口
    
    //2.bind(通信需要套接字 我把我家的地址 门牌号绑上去 ip和端口)
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(SET_PORT);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(sockfd, (struct sockaddr *)&serveraddr,sizeof(serveraddr));
    //3.监听 和服务器连接的总和
    listen(sockfd,128) ;  
    int maxfd = sockfd;
    //初始化两个集合 一个数组
    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(sockfd,&allset);
    int nready;
    
    //初始化数组都为-1 应为标识符从0开始
    for(i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;
    while(1)
    {
        rset = allset;//保证每次循环 select能监听所有的文件描述符 因为rset只会剩下有动静的
        nready = select(maxfd+1,&rset,NULL,NULL,NULL);//参数一
        
        //新客户端
        if(FD_ISSET(sockfd,&rset))
        {
            struct sockaddr_in clientaddr;  
            memset(&clientaddr,0,sizeof(clientaddr));
            int len = sizeof(clientaddr);
            
            connfd = accept(sockfd, (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);//放入集合
            //防止超出范围//select的第一个参数必须是监视的文件描述符+1 如果不断有新的客户连接 最大值不断变大 超出就赋值
            if(connfd > maxfd)
                maxfd = connfd;
            
            //下方表示 如果同一时刻只有 一个动静 就无需进入下方的else判断处理 如果不止一个 nready-1 再进入下方判断
            if(--nready <= 0)  
            	continue;
            
        }
        else
        {
            //已连接FD产生可读事件
            for(i = 0; i < FD_SETSIZE; i++)//FD_SEISIZE 是宏 1024  //循环从数组取出元素比对
            {		
                
                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;
}

服务器代码如下

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
	int sockfd;
	struct sockaddr_in sfdaddr; //指定要连接服务器的结构体 ip 端口
	int len;
	//char buf[1024];
	char wbuf[1024];
	char rbuf[1024];
	//1.socket  通信用套接字,创建一个socket
	sockfd = socket(AF_INET,SOCK_STREAM,0);         
	
	char ipstr[] = "127.0.0.1";           //本机测试ip
	//初始化地址
	bzero(&sfdaddr,sizeof(sfdaddr));
	
	sfdaddr.sin_family = AF_INET;
	sfdaddr.sin_port = htons(8000);
	
	inet_pton(AF_INET,ipstr,&sfdaddr.sin_addr.s_addr); //转换ip 保存到结构体内 
	
//2.connect  主动连接服务器
	connect(sockfd,(struct sockaddr *)&sfdaddr,sizeof(sfdaddr));	
	//3.读写
#if 1
	
    while(1)
	{        
        memset(wbuf,0,1024);
        memset(rbuf,0,1024);
        scanf("%s",wbuf);
        write(sockfd,wbuf,strlen(wbuf));
		len=read(sockfd,rbuf,sizeof(rbuf));
		//write(STDOUT_FILENO,buf,len);
        printf("%s\n",rbuf);		
	}
	
#endif	
	//4.close
	close(sockfd);
	return 0;
}

效果图
在这里插入图片描述

结束语:凡心所向,素履所往,生如逆旅,一苇以航。
                大学时光不易,且行且珍惜。
               写博客只是想记录自己的学习旅程。
  文章如有不足和错误的地方,希望评论指出或私信
最后希望给文章点个赞,整理不易!!!
最后希望给文章点个赞,整理不易!!!
最后希望给文章点个赞,整理不易!!!

猜你喜欢

转载自blog.csdn.net/weixin_44972997/article/details/108004370