select模型
一、模型概述
select系统调用的的用途
是:在一段指定的时间内,监听用户感兴趣的文件描述符
上可读、可写和异常等事件。
select 机制的优势
为什么会出现select模型?
先看一下下面的这句代码:
int iResult = recv(s, buffer,1024);
这是用来接收数据的,在默认的阻塞模式
下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。
再看代码:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式
了。不过你跟踪一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。
看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。
select模型的出现就是为了解决上述问题。
select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度
。
如上所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求
。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的
。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
二、select函数
2.1select函数的功能和调用顺序
使用select函数时统一监视多个文件描述符的:
1、 是否存在套接字接收数据?
2、 无需阻塞传输数据的套接字有哪些?
3、 哪些套接字发生了异常?
select函数调用过程:
由上图知,调用select函数需要一些准备工作,调用后还需要查看结果。
2.2设置文件描述符
select可以同时监视多个文件描述符(套接字)。
此时需要先将文件描述符集中到一起。集中时也要按照监视项(接收,传输,异常)进行区分,即按照上述3种监视项分成三类。
使用fd_set数组变量执行此项操作,该数组是存有0和1的位数组。
最左端的位表示文件描述符0(位置)。如果该位值为1,则表示该文件描述符是监视对象
。
图上显然监视对象为fd1和fd3。
操作fd_set的值由如下宏来完成:
FD_ZERO(fd_set* fdset)
: 将fd_set变量的所有位初始化为0。
FD_SET(int fd, fd_set* fdset)
:在参数fdset集合中注册文件描述符fd的信息。
FD_CLR(int fd, fd_set* fdset)
:参数fdset集合中清除文件描述符fd的信息。
FD_ISSET(int fd, fd_set* fdset)
: 文件描述符fd是否被置位。
2.3 设置监视范围及超时
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
参数详情:
maxfdp
:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的。(监视范围
)
readset
:要检查读事件的容器,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读文件,则根据timeout参数判断是否超时,若超时则返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
writeset
:要检查写事件的容器,主要关心写事件。
exceptset
:用来监视文件错误信息。
timeout
:超时时间
- 永远等待下去:仅在有一个描述字准备好I/O时才返回,为此,我们将timeout设置为空指针
NULL
。 - 等待固定时间:在有一个描述字准备好I/O是返回,但不超过由timeout参数所指timeval结构中指定的秒数和微秒数
struct timeval
{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
};
- 根本不等待:检查描述字后立即返回,这称为轮询。定时器的值必须为0
本来select函数只有在监视文件描述符发生变化时才返回,未发生变化会进入阻塞状态。指定超时时间就是为了防止这种情况发生。
将上述结构体填入时间值,然后将结构体地址值传给select函数的最后一个参数,此时,即使文件描述符中未发生变化,只要过了指定时间,也可以从函数返回。不过这种情况下,select函数返回0。
返回值
:错误返回-1,超时返回0。因关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。
三、举例(服务器与客户端)
/*selectServer.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 8000
#define INET_ADDRSTRLEN 16
void perr_exit(const char *s)
{
perror(s);
exit(1);
}
int main(int argc, char *argv[])
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
//自定义数组
int nready, client[FD_SETSIZE]; /* FD_SETSIZE 默认为 1024 ,所监听的文件描述符*/
ssize_t n;
//用到集合 关心读事件 allset保存原来的样子
fd_set rset, allset;
//缓冲区
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
socklen_t cliaddr_len;
struct sockaddr_in cliaddr, servaddr;
//创建套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
//绑定
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
//监听
listen(listenfd, 20); /* 默认最大128 */
//初始值 开始的最大
maxfd = listenfd; /* 初始化 */
maxi = -1; /* client[]的下标 */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* 用-1初始化client[] */
FD_ZERO(&allset);//将allset集合清零
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
for ( ; ; )
{
rset = allset; /* 每次循环时都从新设置select监控信号集 */
//nready 总数 超时NULL 永久等待满足才返回
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
//是哪一个要连接请求,是否在读集合里, 传入传出参数 出来的时候也是一个
if (FD_ISSET(listenfd, &rset))
{ /* new client connection */
cliaddr_len = sizeof(cliaddr);
//会阻塞吗?直接调用accept 不会阻塞 返回新的文件描述符
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
// 打印哪个IP PORT
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));
//监听的时候加到自定义的集合中,找到任意一个为-1的塞进去
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0)
{
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
/* 达到select能监控的文件个数上限 1024 */
if (i == FD_SETSIZE)
{
fputs("too many clients\n", stderr);
exit(1);
}
//allset之前放listened 是不是有两个了
FD_SET(connfd, &allset); /* 添加一个新的文件描述符到监控信号集里 */
if (connfd > maxfd)
maxfd = connfd; /* select第一个参数需要 */
if (i > maxi)
maxi = i; /* 更新client[]最大下标值 */
if (--nready == 0)
continue; /* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,负责处理未
处理完的就绪文件描述符 */
}
for (i = 0; i <= maxi; i++)
{
if ( (sockfd = client[i]) < 0)/* 检测哪个clients 有数据就绪 */
continue;
if (FD_ISSET(sockfd, &rset))//返回值非零是否在集合里
{
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;
}
}
}
close(listenfd);
return 0;
}
/*selectClient.c*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL)
{
write(sockfd, buf, strlen(buf));
n = read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
write(STDOUT_FILENO, buf, n);
}
close(sockfd);
return 0;
}
参考:
1、https://www.cnblogs.com/skyfsm/p/7079458.html
2、https://blog.csdn.net/y396397735/article/details/55004775