1、前言
1.1、IO模型
下面用服务器比成车站,客户端比喻成小明。
- 传统阻塞模型:小明去车站买票,没买到票就在车站等待,直到有车票为止。
- 非阻塞模型:小明去车站买票,没票的话,他没过一段时间就去看看有没有票,没有票就回去。他消耗了来回的这一个过程,但是不用等待。
- 多路转接IO复用:委托黄牛来购票。select模型就是属于这一类。
下面我用两张图来描述两种
1.2、select模型概念
- select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程 打开的文件描述符个数,并不能改变select监听文件个数。
- 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用 的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力(即超过了这个1024就不适合使用这个模型了)
1.3、select模型使用场景
一般常用在局域网,一些部分公司有在用,现在一般不常用,但是要学其他模型的话,这个模型是基础。
2、select模型需要用到的函数
2.1、模型建立主要函数
- 需要的头文件:
- #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,因为此参数会告诉内核检测前多少个文件描述符的状态 。比如在下面的文件描述符表中,最大的是在28位,那么我们在这个参数就要写28+1个
-
readfds:监控有读数据到达文件描述符集合,传入传出参数
-
writefds:监控写数据到达文件描述符集合,传入传出参数
-
exceptfds:监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
-
timeout:定时阻塞监控时间,3种情况
- NULL,永远等下去
- 设置timeval,等待固定时间
- 设置timeval里时间均为0,检查描述字后立即返回(意思就是轮询)
-
- 返回值:所监听的所有文件描述符 满足的总数,返回的是int类型。
比如在下图中,2、3、4分别表示的是第二、第三、第四个参数,他们都传进去的A、B、C三个字符集,画黄色圈的就是符合对应功能的(比如2集合的,b就符合可读)。那么这个函数的返回值将会是4。
2.2、文件描述符集合处理函数
这一小节主要就是要为第2、3、4个参数服务的,我们要做的就是将文件操作符加入到字符集中。
void FD_CLR(int fd, fd_set *set); 把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); 测试文件描述符集合里fd是否置1,如果有返回1,无则返回0
void FD_SET(int fd, fd_set *set); 把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); 把文件描述符集合里所有位清0
使用步骤:
- 建立fd_set 类型文件描述符集合 变量
- 使用FD_ZERO将该集合清零(就类似于初始化的步骤)
- 使用FD_SET将客户端的文件操作符加入指定集合
- 使用上文2.1中的select函数测得符合条件的总数。
- 遍历循环,使用FD_ISSET判断某个文件描述符,在指定的集合中是否符合条件。(看谁就绪了)
3、示例代码——生产消费者模型
在下面的代码会有点多,我主要分成3大部分
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 8000 //端口号
int main()
{
int i, maxi, maxfd, connfd, sockfd;
int listenfd; //监听套接字建立
int nready, client[FD_SETSIZE]; //FD_SETSIZE 时FD的一个宏,默认为 1024
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16
socklen_t cliaddr_len;
struct sockaddr_in cliaddr, servaddr; //socket需要的sockaddr_in变量
/*第一部分,socket基础部分*/
//套接字初始化
listenfd = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //任意ip号
servaddr.sin_port = htons(SERV_PORT); //绑定端口号
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
//监听
listen(listenfd, 20); //最多监听20个用户
//寻找最大的文件描述符,并且清空一下client数组
maxfd = listenfd; //在文件创建符里,目前最大的是listenfd,因为你这个代码里面现在只创建了一个listenfd文件操作符
maxi = -1; // client[]的下标,具体请看后面的解析
for (i = 0; i < FD_SETSIZE; i++)
{
client[i] = -1; // 用-1初始化client[]
}
//FD_ZERO初始化集合,把监听套接字加入FD_SET
FD_ZERO(&allset); //清空allset字符集
FD_SET(listenfd, &allset); //把listenfd加入到allset字符集,后面用于select函数的监听
/*第二部分,判断谁就绪了*/
for ( ; ; )
{
rset = allset; /* 每次循环时都重新设置select监控信号集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL); //用nready 来接收select返回的总数,这里我们只监听一个rset文件操作符集,符合可读条件的文件操作符个数将会返回
//如果没有符合的就报错
if (nready < 0)
{
perror("select error");
}
//如果listenfd在rset里为1的话,就代表有客户端要对服务器访问
if (FD_ISSET(listenfd, &rset))
{
/*新的client连接 */
cliaddr_len = sizeof(cliaddr);
//客户端连接上,并且记录IP和端口号
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
{
//在client[]找到空位,保存,因为我前面有给client[]初始化过全部为-1
if (client[i] < 0)
{
client[i] = connfd; //保存accept返回的文件描述符到client[]里
break;
}
}
// 达到select能监控的文件个数上限 1024
if (i == FD_SETSIZE)
{
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); // 添加一个新的文件描述符到监控信号集里
if (connfd > maxfd)
{
/* select第一个参数需要,因为你刚刚添加了connfd文件操作符,
那么在client[]中最大的文件操作符将会换成connfd*/
maxfd = connfd;
}
if (i > maxi)
{
maxi = i; /* 更新client[]最大下标值 */
}
//重置一下nready 给下一次使用
if (--nready == 0)
{
continue;
}
}
/*第三部分,对就绪者进行读写数据*/
for (i = 0; i <= maxi; i++)
{
//检测哪个clients 有数据就绪,将clients 赋值给临时变量sockfd
if ( (sockfd = client[i]) < 0)
continue;
//判断一下有数据的clients 在不在rset里
if (FD_ISSET(sockfd, &rset))
{
//读数据到buf
if ( (n = read(sockfd, buf, MAXLINE)) == 0)
{
/* 当client关闭链接时,服务器端也关闭对应链接 */
close(sockfd);
FD_CLR(sockfd, &allset); /* 解除select监控此文件描述符 */
client[i] = -1;
}
else
{
int j;
for (j = 0; j < n; j++)
{
//这里我把发来的信息,转化成大写返回发送回去
buf[j] = toupper(buf[j]);
}
write(sockfd, buf, n);
}
if (--nready == 0)
break;
}
}//for (i = 0; i <= maxi; i++)结束
}//for ( ; ; ) 结束
close(listenfd);
return 0;
}