【Linux】多路转接select
目录
一、初识select
1.1 select函数原型
1.2 常见的程序片段
二、理解select执行过程
2.1 socket就绪条件
2.2 select特点
2.3 select缺点
三、select使用示例:检测标准输入标准输出
四、select实现字典服务器
一、初识select
系统提供select函数来实现多路转接输入/输出模型。
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。
1.1 select函数原型
select函数原型如下:
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout);
参数解释:
- 参数nfds是需要监视的最大的文件描述符值+1。
- rdset,wrset,exset分别对应于需要监视的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符的集合。
- 参数timeout为结构timeval,用来设置select的等待时间。
参数timeout取值:
- NULL:则表示select没有timeout,select将一直被阻塞,知道某个文件描述符上发生了事件。
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
- 特定的时间值:如果在指定的时间段中没有事件发生,select将超时返回。
关于fd_set结构:


其实这个结构就是一个整数数组,更严格的说,是一个“位图”。使用位图中对应的位来表示要监视的文件描述符。
提供了一组操作fd_set的接口,来比较方便的操作位图。
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中相关fd的位
void FD_ZERO(fd_set *set); // ⽤来清除描述词组set的全部位
关于timevla结构:
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值是0。

函数返回值:
- 执行成功则返回文件描述符状态已改变的个数。
- 如果返回0表示在文件描述符状态改变之前已超过timeout的时间,没有返回。
- 当有错误发生时,则返回-1,错误原因则存于errno,此时参数readfds、writefds、exceptfds和timeout的值变成不可预测。
错误值可能为:
- EBADF:文件描述符为无效的或该文件已关闭。
- EINTR:此调用被信号所中断。
- EINVAL:参数n为负值。
- ENOMEN:核心内存不足。
1.2 常见的程序片段
fd_set readset;
FD_SET(fd, &readset);
select(fd+1, &readset, NULL, NULL, NULL);
if(FD_ISSET(fd, readset)) {// ...
}
二、理解select执行过程
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则一字节长的fd_set最大可以对应8个fd。
- 执行fd_set set; FD_ZERO(&set); 则set用位表示是0000 0000。
- 若fd=5,执行FD_SET(fd, &set); 后set变为0001 0000(第五位变为1)。
- 若再加入fd=2,fd=1,则set变为0001 0011。
- 执行select(6, &set, 0, 0, 0)阻塞等待。
- 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000 0011。注意:没有事件发生的fd=5被清空。
2.1 socket就绪条件
读就绪
- socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT。此时可以无阻塞的读该文件描述符,并且返回值大于0。
- socket TCP通信中,对端关闭连接,此时对该socket读,则返回0。
- 监听的socket上有新的连接请求。
- socket上有未处理的错误。
写就绪
- socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
- socket的写操作被关闭(close过着shutdown)。对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
- socket使用非阻塞connect连接成功或失败之后。
- socket上有未读取的错误。
异常就绪
- socket上收到带外数据。关于带外数据和TCP紧急模式相关(回忆TCP协议头中,有一个紧急指针的字段),感兴趣可以去了解一下。
2.2 select特点
- 可监控的文件描述符个数取决于sizeof(fd_set)的值。例如我这个服务器上sizeof(fd_set)=128,每个bit表示一个文件描述符,则我服务器上支持的最大文件描述符是128*8=1024。
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>int main()
{std::cout << sizeof(fd_set) << std::endl;return 0;
}

- 将fd加入select监控集的同时,还要在使用一个数据结构array保存放到select监控集中的fd。
- 用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断。
- select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array同时取得fd最大值maxfd,用于select的第一个参数。
备注:
fd_set的大小可以调整,可能涉及到重新编译内核。感兴趣可以了解一下。
2.3 select缺点
- 每次调用select,都要手动设置fd集合,从接口使用角度来说也非常不方便。
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
- select支持的文件描述符数量太小了。
三、select使用示例:检测标准输入标准输出
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>int main()
{fd_set read_fds;FD_ZERO(&read_fds);FD_SET(0, &read_fds);while(1) {printf("> ");fflush(stdout);int ret = select(1, &read_fds, nullptr, nullptr, nullptr);if(ret < 0) {perror("select");continue;}if(FD_ISSET(0, &read_fds)) {char buf[1024];ssize_t n = read(0, buf, sizeof(buf) - 1);if(n > 0) {buf[n] = 0;std::cout << buf;}}FD_ZERO(&read_fds);FD_SET(0, &read_fds);}return 0;
}

说明:
当只检测文件描述符0(标准输入)时,因为输入条件只在你有输入信息的时候才成立,所以如果一直不输入,就会产生超时信息。
四、select实现字典服务器
// SelectServer.hpp#pragma once#include <iostream>
#include <string>
#include <memory>
#include <sys/select.h>
#include "Log.hpp"
#include "Socket.hpp"using namespace LogModule;
using namespace SocketModule;#define NUM sizeof(fd_set) * 8const static int gdefaultfd = -1;class SelectServer
{
public:SelectServer(uint16_t port):_port(port),_listen_socket(new TcpSocket),_running(false){}void Init() {_listen_socket->BuildListenMethod(_port);for(int i = 0; i < NUM; i++)_fd_array[i] = gdefaultfd;_fd_array[0] = _listen_socket->GetSockfd();}void Loop() {fd_set fds;_running = true;while(_running) {FD_ZERO(&fds);struct timeval timeout = {10, 0};int maxfd = gdefaultfd;for(int i = 0; i < NUM; i++) {if(_fd_array[i] != gdefaultfd) {FD_SET(_fd_array[i], &fds);if(maxfd < _fd_array[i]) maxfd = _fd_array[i];}}int n = select(maxfd + 1, &fds, nullptr, nullptr, &timeout);switch(n) {case 0:std::cout << "time out..." << std::endl;break;case -1:perror("select");break;default:// 有事件就绪了std::cout << "有事件就绪了,timeout:" << timeout.tv_sec << ":" << timeout.tv_usec << std::endl;Dispatcher(fds); // 将已经就绪的sockfd,派发给指定模块break;}}_running = false;}void Dispatcher(fd_set &fds) {for(int i = 0; i < NUM; i++) {if(_fd_array[i] == gdefaultfd) continue;if(FD_ISSET(_fd_array[i], &fds)) {if(_fd_array[i] == _listen_socket->GetSockfd()) {Accepter();}else Recver(i);}}}void Accepter() {InetAddr client;auto cli_socket = _listen_socket->Accept(&client);if(!cli_socket) return;LOG(LogLevel::DEBUG) << "get a new client, info is: " << client.Addr();// 将得到客户端文件描述符插入到fd集合中int pos = -1;for(int i = 0; i < NUM; i++)if(_fd_array[i] == gdefaultfd) {pos = i;break;}if(pos == -1) {LOG(LogLevel::WARNING) << "服务器已经满了...";close(cli_socket->GetSockfd());}else _fd_array[pos] = cli_socket->GetSockfd();}void Recver(int who) {char buf[1024];ssize_t n = recv(_fd_array[who], buf, sizeof(buf) - 1, 0);if(n == 0) {LOG(LogLevel::DEBUG) << "客户端退出,sockfd: " << _fd_array[who];close(_fd_array[who]);_fd_array[who] = gdefaultfd;}else if(n > 0) {buf[n] = 0;std::cout << "client# " << buf << std::endl;std::string message = "echo# ";message += buf;send(_fd_array[who], message.c_str(), message.size(), 0);}else {LOG(LogLevel::ERROR) << "客户端读取出错,sockfd: " << _fd_array[who];close(_fd_array[who]);_fd_array[who] = gdefaultfd;}}
private:uint16_t _port;Socket* _listen_socket;bool _running;int _fd_array[NUM];
};
// SelectServer.cc#include "SelectServer.hpp"int main(int argc, char* argv[])
{if(argc != 2) {std::cout << "Usage: " << argv[0] << " server_port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);SelectServer ss(port);ss.Init();ss.Loop();return 0;
}
客户端代码使用的是我们之前学习TCP套接字写的TcpClient.cc的代码,这里就不重复了。


