系统提供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
#include
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模型的关键在于理解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使用示例(监控标准输入):
#include
#include
#include
#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;
}
优点:
缺点:
读就绪:
写就绪:
我们使用前面封装好的TcpSocket类,需要的自取,连接如下:
TcpSocket类
为了方便使用select模型,我们自己封装一个select类,如下:
#pragma once
#include
#include "TcpSocket.h"
#include
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
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
编译运行程序如下: