> 技术文档 > 多路转接select服务器

多路转接select服务器

目录

select函数原型

select服务器

select的缺点


前面介绍过多路转接就是能同时等待多个文件描述符,这篇文章介绍一下多路转接方案中的select的使用

select函数原型

#include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set*exceptfds, struct timeval *timeout);

先介绍一下各个参数以及返回值

多路转接需要等待多个文件描述符的事件就绪,所以用户势必需要告诉操作系统,他关心的是哪些文件描述符,以及关心这些文件描述符上的读事件还是写事件。读事件就绪就是这个文件描述符的缓冲区不为空有数据能读,写事件就绪就是缓冲区不为满可以写入。除了这两种常见的事件外,还可以关心某个文件描述符的异常事件。

再来看select的参数,nfds是一个整数,可以告诉操作系统需要关心哪些文件描述符,具体来说,nfds是需要关心的文件描述符的最大值 + 1,可以预想到select函数会遍历小于等于nfds - 1的文件描述符,查看是否有事件就绪

struct timeval { time_t tv_sec; /* Seconds */ //秒 suseconds_t tv_usec; /* Microseconds */ //毫秒};

timeout表示select的等待时间,timeout也可为空,表示阻塞等待直到某个文件描述符发生事件,timeout为0表示不等待事件发生,其他自定义值表示若在这段时间内没有事件发生,则超时返回。

返回值为0表示超时返回;为-1表示有错误发生,并设置错误码errno;为正数表示在timeout时间内事件就绪的文件描述符个数

为了介绍剩下的三个参数,先介绍一下fd_set

我们已经通过fds告诉操作系统要关心哪些文件描述符,timeout设置了等待时间,现在还需要告诉操作系统要关心哪些文件描述符的读事件或写事件

从抽象的层面上理解,fd_set是一个集合,是一个文件描述符的集合,readfds是关心读事件的文件描述符集合,writefds是关心写事件的文件描述符集合,exceptfds是关心异常事件的文件描述符集合。

还需要指出,这三个参数还是输出型参数,操作系统会将等待后事件就绪的文件描述符加入集合,

比如关心4,5,6的读事件,若就绪了4和5,集合就会变成4,5,这也为写代码带来了麻烦

从具体实现上来看,fd_set是一个位图,有若干个比特位表示文件描述符,值为1表示关心这个文件描述符,为0表示不关心,举个例子

00011111001

下标从0开始的话,这个位图表示关心3,4,5,6,7,10号文件描述符,其余的都不关心

/* fd_set for select and pselect. */typedef struct { /* XPG4.2 requires this member name. Otherwise avoid the name from 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封装了一个大小固定的数组,数组的每个比特位都可以记录是否关心这个文件描述符

作为用户,想对fd_set操作,操作系统也提供了相关的接口

// 将文件描述符fd从集合set中删除void FD_CLR(int fd, fd_set *set);// 判断文件描述符fd是否在集合set中int FD_ISSET(int fd, fd_set *set);// 将fd放入集合set中void FD_SET(int fd, fd_set *set);// 清空集合setvoid FD_ZERO(fd_set *set);

select服务器

到这里,select已经可以等待多个文件描述符的一些事件了,可以来搭一个简单的服务器,接收多个用户的消息,回显在屏幕上

这里只给出select_server的代码,其他文件的代码对理解select不重要,只需要了解套接字的使用便可轻松看懂,若要查看其他文件的代码,详见rokobo/wsl_code - Gitee.com

在这份代码中,需要等待的事件有等待客户端的连接和等待客户端发消息,由于连接建立后会创建文件描述符,文件描述符会变多,需要一个数据结构把这些文件描述符管理起来,这里选择了原生数组,因为可以直观的感受到select的缺点之一,存在大量遍历,性能不够高。

#include \"socket.hpp\"#include \"Log.hpp\"#include #include #include #include using namespace SocketModule;using namespace LogModule;class select_server{// sizeof可以得到底层数组的字节数,乘8得到比特数static const int NUM = sizeof(fd_set) * 8;public: select_server() :_listen_sock(std::make_shared()), _is_running(false) {} void init(int port) { _listen_sock->BuildTcpSocketMethod(port); for(int i=0;iFd(); } void loop() { _is_running = true; int listenfd = _listen_sock->Fd(); fd_set readset; while(_is_running) { //readset作为输出参数,select返回后可能被修改,需要清空后重新设置 FD_ZERO(&readset); int max_fd = 0; for(int i=0;i max_fd ? fds[i] : max_fd;  FD_SET(fds[i], &readset); } } struct timeval timeout = {2, 0}; int ret = select(max_fd + 1, &readset, nullptr, nullptr, &timeout); if(ret == -1) { LOG(LogLevel::ERROR) << \"Error message: \" << strerror(ret); continue; } else if(ret == 0) { LOG(LogLevel::INFO) << \"Time out\\n\"; continue; } else { LOG(LogLevel::INFO) <Accepter(&client); if(client_sock == nullptr) { LOG(LogLevel::ERROR) <Fd(); if(client_fd < 0) { LOG(LogLevel::ERROR) << \"Client fd error\"; return; } //将client_fd加入到fds中 //如果fds满了,关闭连接 int i=0; for(i=0;i<NUM;++i) { if(fds[i] == -1) { fds[i] = client_fd; LOG(LogLevel::INFO) << \"Accept success: \" <Fd() << \" \" << client.Addr(); break; } } if(i == NUM) { LOG(LogLevel::ERROR) <Close(); return; } } void recver(int who) { int fd = fds[who]; std::string buffer; auto client_sock = std::make_shared(fd); ssize_t ret = client_sock->Recv(&buffer); if(ret == -1) { LOG(LogLevel::ERROR) << \"Recv error\" <Close(); //将fd从fds中删除 fds[who] = -1; return; } else if(ret == 0) { LOG(LogLevel::INFO) << \"Client closed: \" <Fd(); client_sock->Close(); //将fd从fds中删除 fds[who] = -1; return; } else { LOG(LogLevel::INFO) << \"Recv success: \" << buffer; return; } } void dispatcher(fd_set &readset) { //找到所有合法的fd,分发 for(int i=0;iFd()) {  accepter(fds[i]); } //分发给处理IO的函数 else {  recver(i); } } } } void stop() {}private: std::shared_ptr _listen_sock; int fds[NUM]; bool _is_running;};

主函数

#include \"select_server.hpp\"#include int main(){ select_server s_svr; s_svr.init(8080); s_svr.loop(); return 0;}

 

select的缺点

从代码中大量的遍历,甚至select底层还要遍历,可以感受到select有太多遍历,效率不高,而且fd_set的底层数组是静态的无法扩容,能同时关心的文件描述符有限,而且需要用户自己去定义数据结构管理需要关心的文件描述符,更是增加了编码的复杂性,每次调用select,都需要把fd_set从用户态拷贝到内核态,这个拷贝的开销在fd很多时开销很大