前言: select用的少现在,而且写起来很复杂,本篇所讲的select IO模型主要是用于处理读就绪事件的,至于写就绪不关心。
select函数来实现多路复用输入/输出模型:
select函数它是负责等这个过程的,等成功了就通知上层来缓冲区拷贝,这个等的方式是可以自己设置的,比如:阻塞等,非阻塞等都行。注意:它只负责等,不负责拷贝或者写入。
函数参数:
- nfds 是要监测的fd中,最大的fd+1,它相当于一个边界控制。
- fd_set *readfds, fd_set *writefds, fd_set *exceptfds,这三个参数是输入输出型参数,输入:你告诉内核你要关心那些fd。输出:你关心的fd有谁就绪了。总共有三个参数,它们分别是可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
- timeout这个结构体就是一个时间线。就用它来控制等的方式,如果将它给成null,那么就是阻塞式等。如果给成{0,0},那就是非阻塞式等。如果给成{5,0},那就是5秒前为阻塞式等,五秒后为非阻塞式等(相当于超时处理)。
函数返回值:
- 大于 0:成功,返回集合中已就绪的文件描述符的总个数
- 等于 - 1:函数调用失败
- 等于 0:超时,没有检测到就绪的文件描述符
fd_set: 这个类型它就是一个位图,可以通过操作位图来表示你要关心哪些fd。它的大小是128字节。128*8=1024个比特位,也就是说可以检测1024个文件描述符,这个范围其实不是很大。
看一下它的结构:
typedef struct{/* XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace. */
#ifdef __USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif} fd_set;
但这个位图,不能直接操作,必须使用函数接口:
举个例子,就取fd_set的最后8个比特位:fd_set i
;
(1)先是利用FD_ZERO(&i)清空位图。
(2)将fd = 5,设置到位图i中,FD_SET(5,&i)。那就是将第五个比特位设置为1。
(3)再把fd = 1,fd =2 ,都设置进去:
(4)那么就可以利用select进行检测,假如这些描述符组合,我只关心读事件就绪:
select(6,&i,null,null,null),为啥是6,最大文件描述符fd是5,5+1 = 6。最后一个参数设置为null,就是阻塞式等待。
(5)假如现在fd=5的读事件就绪了,但是2和1都不就绪,那也返回了,位图就变为:
注意了:2和1处都被抹除,只有5处为1,因为只有5的读事件就绪了。那么肯定有疑问了,我本来要关心fd=1,2,5,三个文件描述符,虽然只有5事件读就绪,你把1,2都给抹除了,那么下次我还要重新设置位图吗?还得把1和2重新设置进位图?答案是:需要,这就是select难的地方。得有一个数组,提前把你要关心的文件描述符存进去。
那么我是如何知道5事件就绪了呢?还得操作位图:FD_ISSET(5, &i),这就是帮助检测的,如果返回值是>0,那么就说明关系的fd事件已经就绪,<0,就表示未就绪。
(6) 要是不关心某个fd了,就可以用void FD_CLR(int fd, fd_set *set);
那么知道了select的基本操作函数,我们就基于select来完成一个简易的读取数据服务器,客户端就不写了,我们主要看的是服务器中使用select的基本逻辑。
#include
#include
#include
#include
#include
#include
#include
#include 这是在定义一个存放fd集合的数组,记忆功能
#define NUM (sizeof(fd_set) * 8)
int fd_array[NUM];void useage()
{std::cout << "please use"<< "./select_sever"<< "+"<< "端口号" << std::endl;
}int main(int argv, char *argc[])
{ 启动服务器的方式if (argv != 2){useage();exit(1);} 使服务器进去listen状态int listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd < 0){std::cerr << "listen_fd failed" << std::endl;exit(2);}std::cout << "listen_fd:" << listen_fd << std::endl;struct sockaddr_in my_sock;my_sock.sin_family = AF_INET;my_sock.sin_port = htons(atoi(argc[1]));my_sock.sin_addr.s_addr = INADDR_ANY;if (bind(listen_fd, (const struct sockaddr *)&my_sock, sizeof(my_sock)) < 0){std::cerr << "bind errno" << std::endl;exit(3);}if (listen(listen_fd, 5) < 0){std::cerr << "listen errno" << std::endl;exit(4);} 请问这里可以accept吗?绝对不可以,因为accept它是从listen_fd中拿连接,这也是读取事件,所以要用select进行管理。 初始化一下 fd_arryfor (int i = 0; i < NUM; i++){fd_array[i] = -1;} 我们只关心读事件,那么先得有一个位图fd_Setfd_set rfds;fd_array[0] = listen_fd; // 这个是不变的,一上来就只有一个套接字要监听读事件,那就是listen_fdwhile (true){ 清空位图FD_ZERO(&rfds); 把关心的事件设置进位图,并且更新MAX_fd(select的第一个参数)int MAX_fd = fd_array[0];for (int i = 0; i < NUM; i++){if (fd_array[i] == -1)continue;FD_SET(fd_array[i], &rfds);if (MAX_fd < fd_array[i]){MAX_fd = fd_array[i];}}开始select等待int n = select(MAX_fd + 1, &rfds, NULL, NULL, NULL);switch (n){case -1:std::cerr << "select error" << std::endl;break;case 0:std::cout << "time out" << std::endl;break;default:std::cout << "有读事件就绪" << std::endl;std::cout<<"********************************************"<if (fd_array[i] == -1)continue;if (FD_ISSET(fd_array[i], &rfds)){if (fd_array[i] == listen_fd){std::cout << "有新的连接" << std::endl;// 进行连接struct sockaddr_in user;socklen_t size_sockaddr = sizeof(user);int sock = accept(listen_fd, (struct sockaddr *)&user, &size_sockaddr);if (sock < 0){std::cerr << "accept errno" << std::endl;exit(5);}std::cout<<"连接成功:"<<"fd = "<if (fd_array[pos] == -1)break;}// 对pos位置进行管理,看pos位置是否合法:if (pos < NUM){fd_array[pos] = sock;}else{std::cout << "服务器满载,关闭此连接" << std::endl;std::cout<<"********************************************"< 走到这里,说明就是普通的读取事件,普通套接字的读事件就绪。 到这可以读吗?当然可以,人家都通知你读了std::cout << "套接字为" << fd_array[i] << "有读取事件就绪" << std::endl;char buffer[1024] = {0};ssize_t N = recv(fd_array[i], buffer, sizeof(buffer) - 1, 0);if (N > 0){buffer[N] = 0;std::cout << "client[" << fd_array[i] << "]#" << buffer << std::endl;std::cout<<"********************************************"<std::cout << "对端连接关闭" << std::endl;close(fd_array[i]);// 注意,我们要把它在数组中的存储一并去除,这件事很重要fd_array[i] = -1;std::cout<<"********************************************"<std::cout << "读取失败"<< "主动关闭连接" << std::endl;close(fd_array[i]);// 注意,我们要把它在数组中的存储一并去除,这件事很重要fd_array[i] = -1;std::cout<<"********************************************"<
看看结果:
(1) 服务器起来
(2) telnet 连接
(3) 看现象,这是连接成功了,并且被select管理起来的
(4) 客户端发送数据
(5) 服务器接收
(6) 客户端退出,服务端:
至于这个代码的讲解,我都在注释里讲清楚了,不好理解。
其实,里面的select默认用的是阻塞等待,如果想要实验 select 的等待方式,其实只需要把函数的最后参数 timeout 改改就可以了。
分析一下select IO模型,它非常依赖第三方数组,原因有两点:
那么就可以总结一下它的优缺点: