『Linux』I/O多路转接之select模型

select函数

系统提供select函数来实现多路复用输入/输出模型

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态变化

接口如下

/* 头文件:sys/select.h */
int select(
	int nfds, fd_set *readfds, fd_set *writefds,
	fd_set *exceptfds, struct timeval *timeout
);
/*
*	参数:
*		nfds:需要监视的最大的文件描述符值+1;
*		readfds:需要检测的可读文件描述符的集合;
*		writefds:需要检测的可写文件描述符的集合;
*		exceptfds:需要检测的异常文件描述符的集合;
*		timeout:用来设置select的等待时间,有三种取值;
*				NULL(表示没有timeout,select将一直阻塞,直到某个文件描述符上发生了事件);
*				0(仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生);
*				特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
*	返回值:
*		成功,返回就绪的描述符个数;等待超时,返回0;监控出错返回-1。
*		可能的错误为:
*			EBADF:文件描述符无效或该文件已关闭;
*			EINTR:次调用被信号中断;
*			EINVAL:参数nfds为负值;
*			ENOMEM:核心内存不足。
*/

使用下面命令打开select.h头文件,查看fd_set结构体如下

[sss@aliyun sys]$ vim /usr/include/sys/select.h

在这里插入图片描述
在这里插入图片描述
其实这个结构就是一个整数数组,更严格的说,是一个“位图”。使用位图中对应的位来表示要监视的文件描述符


这个位图有多大呢
可以看到位图中是一个数组,数组中存储的是__fd_mask类型的元素也就是long int类型的元素,每个元素为8个字节
数组的大小为__FD_SETSIZE / __NFDBITS。首先,我们在select.h头文件中可以看到二者定义如下
在这里插入图片描述
在这里插入图片描述
可以看到,我们在select.h头文件中并没有找到__FD_SETSIZE的大小,我们退回到上级目录,使用grep命令来查找一下这个宏
在这里插入图片描述
从上面可以看到,__FD_SETSIZE的大小为1024,也就是说数组的大小为1024/(8*8)=16,也就是数组中有16个元素,每个元素的大小为8个字节,就是说该数组可以表示16*8*8=1024个位,也就是说最多可以监控1024个文件描述符


还有一种更简单的方法,来查看这个位图有多大,方法如下

#include <iostream>
#include <sys/select.h>

int main(){
	// 查看最多可以监控多少个描述符
	std::cout << sizeof(fd_set) * 8 << std::endl;

return 0;
}

在这里插入图片描述
可以看到,和前面我们计算的一样,select最多可以监控1024个文件描述符
注意fd_set的大小可以调整,可能涉及到重新编译内核


这里还有一组操作fd_set的接口,可以比较方便的操作位图

// 用来清除描述符集set中相关fd的位
void FD_CLR(int fd, fd_set *set);

// 用来测试描述符集set中相关fd的位是否为真
int FD_ISSET(int fd, fd_set *set);

// 用来设置描述符集set中相关fd的位
void FD_SET(int fd, fd_set *set); 

// 用来清除描述符集set的全部位
void FD_ZERO(fd_set *set); 

timeval结构
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则返回,返回值为0
输入下面命令查看

[sss@aliyun linux]$ vim /usr/include/linux/time.h

在这里插入图片描述

select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd
举例如下

// 新建一个文件描述符集
fd_set set;
// 清空文件描述符集
FD_ZERO(&set);
// 此时set为:0000 0000
fd = 5;
// 将fd添加到文件描述符集
FD_SET(fd, &set);
// 此时set为:0001 0000(第5位置1)
fd1 = 2;
fd2 = 1;
// 将fd1、fd2添加到文件描述符集
FD_SET(fd1, &set);
FD_SET(fd2, &set);
// 此时set为:0001 0011(第1、2位置1)
// 阻塞等待文件描述符1、2、5
select(6, &set, 0, 0, 0);
/*
*	若fd=1,fd=2上都发生可读事件,则select返回;
*	此时set变为:0000 0011;
*	注意:没有事件发生的fd=5被清空。
*/

注意将fd加入select监控集的同时,还需要再使用一个数据结构array保存放到select监控集中的fd

  • select返回后,array作为源数据和fd_set进行FD_ISSET判断
  • select返回后会把以前加入的但没有事件发生的fd清空,所以每次开始select前都要重新从array取得fd注意加入(FD_ZERO最先),扫描array的同时取得fd的最大值maxfd,用于select的第一个参数

select使用示例(监控标准输入)

#include <iostream>
#include <sys/select.h>
#include <unistd.h>

#define BUF_SIZE 1024

int main(){
	// 要监控的文件描述符集
	fd_set fds;

	// 清空文件描述符集
	FD_ZERO(&fds);

	// 将标准输入添加到文件描述符集
	FD_SET(0, &fds);

	while(1){
		std::cout << "> ";
		fflush(stdout);

		// 监控标准输入,阻塞监控
		int ret = select(1, &fds, nullptr, nullptr, nullptr);
		// 监控失败
		if(ret < 0){
			perror("select error");
			continue;
		}

		// 检测到标准输入,进行打印
		if(FD_ISSET(0, &fds)){
			char buf[BUF_SIZE] = {0};
			read(0, buf, sizeof(buf) - 1);
			std::cout << "input: " << buf;
		}
		else{
			std::cout << "error! invalid fd!\n";
			continue;
		}

		// 清空监控集合
		FD_ZERO(&fds);

		// 将描述符0添加到监控结合
		FD_SET(0, &fds);
	}

	return 0;
}

编译运行程序,效果如下
在这里插入图片描述

select优缺点

优点

  1. 遵循POSIX标准,可以跨平台,移植性强
  2. select监控的超时时间可以设置的更加精细,可精确到微秒

缺点

  1. select能监控的描述符是有上限的,默认是1024个
  2. select监控的实现是在内核中轮询遍历所有描述符的状态,随着描述符的增多性能会下降
  3. select每次监控都会修改集合,需要用户每次监控时重新添加描述符到集合中
  4. select要监控的集合中的描述符数据,需要每次向内核中拷贝
  5. select不会直接告诉用户哪一个描述符事件就绪,只是告诉用户有事件就绪,需要用户遍历查找

使用select实现字典服务器

socket就绪条件

读就绪

  • socket内核中,接收缓冲区中的字节数,大于等于低水位标志SO_RCVLOWAT。此时可以无阻塞的读该文件描述符,并且返回值大于0;
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
  • 监听的socket上有新的连接请求
  • socket上有未处理的错误

写就绪

  • socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标志SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown)。对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
  • socket使用非阻塞connect连接成功或失败之后
  • socket上有未读取的错误

我们使用前面封装好的TcpSocket类需要的自取连接如下
TcpSocket类


为了方便使用select模型,我们自己封装一个select类,如下:

#pragma once

#include <sys/select.h>
#include "TcpSocket.h"
#include <vector>

class Select{
	public:
		Select()
			: _max_fd(0)
		{
			// 清空监控集合
			FD_ZERO(&_fds);
		}

		// 将描述符添加到监控集合
		bool Add(TcpSocket& sock){
			// 获取套接字描述符
			int fd = sock.GetSockFd();
			// 将fd添加到监控集合
			FD_SET(fd, &_fds);
			// 修改最大文件描述符
			_max_fd = (fd > _max_fd) ? fd : _max_fd;

			return true;
		}

		// 将描述符从监控集合中移除
		bool Delete(TcpSocket& sock){
			// 获取套接字描述符
			int fd = sock.GetSockFd();
			// 将fd从监控集合移除
			FD_CLR(fd, &_fds);

			// 修改最大文件描述符
			for(int i = _max_fd; i >= 0; --i){
				// 判断文件描述符是否在集合中
				if(!FD_ISSET(i, &_fds)){
					continue;
				}

				_max_fd = i;
				break;
			}

			return true;
		}

		// 通过list返回就绪的套接字
		bool Wait(std::vector<TcpSocket>& list){
			fd_set fds = _fds;

			// 开始监控,阻塞监控
			int ret  = select(_max_fd + 1, &fds, NULL, NULL, NULL);
			if(ret < 0){
				perror("select error");
				return false;
			}

			// 监控之后,fds中保存的都是已经就绪的套接字描述符
			for(int i = 0; i <= _max_fd; ++i){
				if(FD_ISSET(i, &fds)){
					// 将就绪的套接字放入list
					TcpSocket sock;
					sock.SetSockFd(i);
					list.push_back(sock);
				}
			}

			return true;
		}

	private:
		// 保存所有要监控的描述符,用的时候拷贝一份
		// 因为select会修改监控集合中的内容
		fd_set _fds;

		// 设定最大描述符
		int _max_fd;
};

下面,我们来封装一个字典服务器

#include "select.h"
#include <unordered_map>

class DictServer{
	public:
		// 构造函数
		DictServer(std::string ip, uint16_t port)
			: _ip(ip)
			, _port(port)
		{}

		// 析构函数
		~DictServer(){
			// 关闭套接字
			_sock.Close();
		}


		// 启动服务器
		bool Start(){
			// 创建套接字
			bool ret = _sock.Socket();
			if(!ret){
				return false;
			}

			// 绑定地址信息
			ret = _sock.Bind(_ip, _port);
			if(!ret){
				return false;
			}

			// 监听
			ret = _sock.Listen();
			if(!ret){
				return false;
			}

			// 实例化一个Select对象
			Select s;
			s.Add(_sock);

			while(1){
				// 保存套接字对象
				std::vector<TcpSocket> list;
				// 等待事件就绪
				ret = s.Wait(list);
				if(!ret){
					continue;
				}

				// 遍历就绪列表
				for(size_t i = 0; i < list.size(); ++i){
					// 就绪的描述符和监听描述符相等
					if(list[i].GetSockFd() == _sock.GetSockFd()){
						// 创建新的套接字和新连接上来的客户端通信
						TcpSocket new_sock;
						ret = _sock.Accept(new_sock);
						if(!ret){
							continue;
						}

						// 将新创建的套接字放入监控集合
						s.Add(new_sock);
					}
					// 就绪的描述符和监听描述符不相等
					else{
						// 接收客户端发来的数据
						std::string recv_buf;
						ret = list[i].Recv(recv_buf);
						// 如果接收失败,删除该描述符
						if(!ret){
							s.Delete(list[i]);
							list[i].Close();
						}

						// 响应数据
						std::string send_buf;

						// 字典中匹配不到
						if(_dict.find(recv_buf) == _dict.end()){
							send_buf = "Query Failed!";
						}
						// 字典中匹配到
						else{
							send_buf = _dict[recv_buf];
						}

						// 向客户端发送响应
						ret = list[i].Send(send_buf);
						if(!ret){
							return false;
						}
					}
				}
			}

			return true;
		}

		// 向字典中添加数据
		void FillDict(std::unordered_map<std::string, std::string>& dict){
			for(const auto& e : dict){
				_dict[e.first] = e.second;
			}
		}

	private:
		// 字典
		std::unordered_map<std::string, std::string> _dict;
		// 套接字对象
		TcpSocket _sock;
		// IP地址
		std::string _ip;
		// 端口号
		uint16_t _port;
};

下面,我们再来封装一个客户端

#include "TcpSocket.h"

class Client{
	public:
		// 构造函数
		Client(std::string ip, uint16_t port)
			: _ip(ip)
		  	, _port(port)
		{}

		// 析构函数
		~Client(){
			// 关闭套接字
			_sock.Close();
		}

		// 启动客户端
		bool Start(){
			// 创建套接字
			bool ret = _sock.Socket();
			if(!ret){
				return false;
			}

			// 向服务器发起连接请求
			ret = _sock.Connect(_ip, _port);
			if(!ret){
				return false;
			}

			while(1){
				// 保存收发数据
				std::string send_buf;
				std::string recv_buf;

				std::cout << "英文: ";
				fflush(stdout);

				// 发送数据到服务端
				std::cin >> send_buf;
				ret = _sock.Send(send_buf);
				if(!ret){
					return false;
				}

				// 接收服务端的响应
				ret = _sock.Recv(recv_buf);
				if(!ret){
					return false;
				}
				std::cout << "译文: " << recv_buf << std::endl;
			}

			return true;
		}

	private:
		// 实例化套接字对象
		TcpSocket _sock;
		// IP地址
		std::string _ip;
		// 端口号
		uint16_t _port;
};

字典服务器服务端

#include "dict_server.h"

int main(){
	// 实例化一个字典服务器对象
	DictServer ds("0.0.0.0", 9999);

	std::unordered_map<std::string, std::string> dict;
	dict["left"] = "左";
	dict["right"] = "右";
	dict["up"] = "上";
	dict["down"] = "下";

	// 字典中添加数据
	ds.FillDict(dict);

	// 启动服务器
	ds.Start();

	return 0;
}

字典服务器客户端

#include "client.h"

int main(){
	// 实例化客户端对象
	Client cli("0", 9999);

	// 运行客户端
	cli.Start();

	return 0;
}

我们再来写一个makefile,如下:

all:client dict_server
client:client.cc
	g++ $^ -o $@ -std=c++0x
dict_server:dict_server.cc
	g++ $^ -o $@ -std=c++0x

编译运行程序如下

  • 首先,编译程序
    在这里插入图片描述
  • 然后,运行服务器
    在这里插入图片描述
  • 最后,运行客户端,效果如下
    在这里插入图片描述
  • 我们再启动一个客户端,进行测试,效果如下
    在这里插入图片描述
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值