目录
在IO操作中,如果我们阻塞式的等待某个文件描述符资源就绪,那么在等待的过程中,就会浪费大量的时间,造成程序运行的效率低下。在实际的工程应用中,可能存在同时有多个文件描述符就绪的情况,这时如果阻塞等待其他未就绪的文件描述符,其它已经就绪的文件描述符也就暂时无法进行处理。
相比与单纯地阻塞式IO,IO多路转接能够实现这样的功能:当用户所关心的多个文件描述符的其中之一就绪时,就对这个就绪的进行处理。IO多路转接能够大大降低阻塞等待的时间,提高程序IO操作的效率。
函数原型:int select(int nfds, struct fd_set* readfds, struct fd_set* writefds, struct fd_set* exceptfds, struct timval* timeout)
头文件:#include
函数参数:
- nfds:所关注的值最大的文件描述符值+1。
- readfds/writefds/exceptfds:输入输出型参数,设置关注的读/写/异常文件描述符。
- timeout:输入输出型参数,设置最长阻塞时间,获取剩余时间。
返回值:如果执行成功,返回就绪的文件描述符个数,等待超时返回0,等待失败返回-1。
在使用select函数时,有以下几点需要注意:
- struct timeval
- {
- long tv_sec; /* seconds */
- long tv_usec; /* microseconds */
- };
代码2.1展示了如何使用select接口,设置关注标准输入、标准输出和标准错误的读状态,设置最长阻塞时间1s,每次调用select前对fd_set对象和timeout重新设置,避免上一层调用select输出覆盖,在检查到select返回值>0时,还应使用FD_ISSET进一步检查所关注的文件描述符是否真正就绪,下面的代码真正所关系的文件描述符是标准输入0。
代码2.1:select接口的使用方法
- #include <iostream>
- #include <cstring>
- #include <sys/select.h>
- #include <unistd.h>
-
- int main()
- {
- // 读取缓冲区
- char buffer[1024] = { 0 };
-
- while(true)
- {
- // 每次调用select之前,重新设置fd_set类型参数和Timeval阻塞时间
- fd_set rfd;
- FD_SET(0, &rfd);
- FD_SET(1, &rfd);
- FD_SET(2, &rfd); // 设置关心标准输入、标准输出和标准错误
-
- // 设置最长阻塞时间为1s
- struct timeval timeout;
- timeout.tv_sec = 1; timeout.tv_usec = 0;
-
- // 调用select进行IO多路转接
- int n = select(3, &rfd, nullptr, nullptr, &timeout);
- if(n > 0)
- {
- // 如果标准输入没有就绪,那么直接到下一轮循环中去
- if(!FD_ISSET(0, &rfd))
- {
- continue;
- }
-
- ssize_t sz = read(0, buffer, 1023);
- buffer[sz - 1] = '\0';
- std::cout << "Show message# " << buffer << std::endl;
- std::cout << "Remain time: " << timeout.tv_sec << " seconds," << timeout.tv_usec << " microseconds" << std::endl;
-
- if(strcmp(buffer, "quit") == 0)
- {
- break;
- }
- }
- else if(n == 0)
- {
- std::cout << "WARNING, Time out!" << std::endl;
- }
- }
-
- return 0;
- }
本文实现一个基于TCP协议,可以从客户端读取数据的Select服务器。Select服务器的声明见代码2.2,其中包含基本的构造函数和析构函数,还有Handler函数在检测到有就绪的文件描述符后进行处理、Accepter函数用于接受对端连接请求、Reciever函数用于从指定文件描述符中读取数据。
代码2.2:SelectServer的声明(SelectServer.hpp头文件)
- #pragma once
-
- #include "Sock.hpp"
- #include <vector>
- #include <sys/select.h>
- #include <unistd.h>
-
- static const int FD_CAPACITY = 8 * sizeof(fd_set);
- static const int NON_FD = -1;
-
- class SelectServer
- {
- public:
- SelectServer(uint16_t port, const std::string& ip = ""); // 构造函数
- void start(); // Select服务器启动运行函数
- ~SelectServer(); // 析构函数
-
- private:
- void Handler(fd_set& rfd); // 处理就绪文件描述符函数
- void Reciever(int pos); // 内容读取函数
- void Accepter(); // 链接接收函数
- void ShowFdArray(); // 文件描述符输出函数 -- 用于DEBUG
-
- int _listenSock; // 监听套接字fd
- uint16_t _port; // 服务器端口号
- std::string _ip; // ip地址
- std::vector<int> _fd_array; // 文件描述符序列
- };
下面为Select服务器每个成员函数的实现需要注意的一些事项:
代码2.3:日志打印函数的实现(log.hpp头文件)
- #pragma once
- #include <iostream>
- #include <cstdio>
- #include <cstdarg>
- #include <ctime>
-
- #define DEBUG 0
- #define NORMAL 1
- #define WARING 2
- #define ERROR 3
- #define FATAL 4
-
- // 日志等级
- static const char* g_levelMap[5] =
- {
- "DEBUG",
- "NORMAL",
- "WARING",
- "ERROR",
- "FATAL"
- };
-
- // 日志打印哈数,level为日志等级,后面为格式化可变参数
- static void logMessage(int level, const char *format, ...)
- {
- // 1. 输出常规部分
- time_t timeStamp = time(nullptr);
- struct tm *localTime = localtime(&timeStamp);
- printf("[%s] %d-%d-%d, %02d:%02d:%02d\n", g_levelMap[level], localTime->tm_year, localTime->tm_mon, \
- localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec);
-
- // 2. 输出用户自定义部分
- va_list args;
- va_start(args, format);
- vprintf(format, args);
- va_end(args);
- }
代码2.4:网络通信Socket相关函数的实现(Sock.hpp头文件)
- #pragma once
-
- #include "log.hpp"
- #include <iostream>
- #include <string>
- #include <cstring>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
-
- class Sock
- {
- public:
- // 创建socket文件描述符
- static int Socket()
- {
- int sock = socket(AF_INET, SOCK_STREAM, 0);
- if(sock < 0)
- {
- logMessage(FATAL, "socket error, sock:%d\n", sock);
- return -1;
- }
-
- logMessage(NORMAL, "socket success, sock:%d\n", sock);
- return sock;
- }
-
- // 绑定端口号
- static int Bind(int sock, const std::string &ip, uint16_t port)
- {
- struct sockaddr_in tmp;
- memset(&tmp, 0, sizeof(tmp));
- tmp.sin_family = AF_INET; // 网络协议族
- tmp.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str()); // ip地址
- tmp.sin_port = htons(port); // 端口号
- socklen_t len = sizeof(tmp);
-
- if(bind(sock, (struct sockaddr *)&tmp, len) < 0)
- {
- logMessage(FATAL, "bind error!\n");
- return -1;
- }
-
- logMessage(NORMAL, "bind success!\n");
- return 0;
- }
-
- // 设置监听状态
- static int Listen(int sock, int backlog = 10)
- {
- if(listen(sock, backlog) < 0)
- {
- logMessage(FATAL, "listen error!\n");
- return -1;
- }
-
- logMessage(NORMAL, "listen success!\n");
- return 0;
- }
-
- // 接受连接
- static int Accept(int sock, std::string& ip, uint16_t& port)
- {
- struct sockaddr_in peer;
- memset(&peer, 0, sizeof(peer));
- socklen_t len = sizeof(peer);
-
- int fd = accept(sock, (struct sockaddr *)&peer, &len);
- if(fd < 0) return -1;
-
- ip = inet_ntoa(peer.sin_addr);
- port = ntohs(peer.sin_port);
- logMessage(NORMAL, "accept success, [%s-%d]\n", ip.c_str(), port);
-
- return fd;
- }
-
- // 连接对方
- static int Connect(int sock, const std::string &ip, uint16_t port)
- {
- struct sockaddr_in peer;
- memset(&peer, 0, sizeof(peer));
-
- peer.sin_family = AF_INET;
- peer.sin_addr.s_addr = inet_addr(ip.c_str());
- peer.sin_port = htons(port);
-
- socklen_t len = sizeof(peer);
-
- if(connect(sock, (const struct sockaddr *)&peer, len) < 0)
- {
- logMessage(FATAL, "conect error!\n");
- return -1;
- }
- logMessage(NORMAL, "connect success!\n");
-
- return 0;
- }
- };
代码2.5:SelectServer的实现(SelectServer.cc源文件)
- #include "SelectServer.hpp"
-
- SelectServer::SelectServer(uint16_t port, const std::string& ip)
- : _listenSock(-1), _port(port), _ip(ip), _fd_array(FD_CAPACITY, NON_FD)
- {
- // 获取监听套接字
- _listenSock = Sock::Socket();
- if(_listenSock < 0){
- exit(2);
- }
-
- // 绑定端口号
- if(Sock::Bind(_listenSock, ip, _port) < 0){
- exit(3);
- }
-
- // 设置监听状态
- if(Sock::Listen(_listenSock) < 0){
- exit(4);
- }
- }
-
- // Select服务器启动运行函数
- void SelectServer::start()
- {
- while(true)
- {
- fd_set rfd; // select所关注的读取fd
- FD_ZERO(&rfd); // 将文件描述符清零
- _fd_array[0] = _listenSock; // 默认设置_fd_array[0]为listenSock
-
- ShowFdArray();
-
- // 将_fd_array中的有效文件描述符记录到rfd中去
- int maxFd = _listenSock; // 最大文件描述符
- for(const auto fd : _fd_array)
- {
- if(fd != NON_FD)
- {
- FD_SET(fd, &rfd); // 添加文件描述符
- if(fd > maxFd) maxFd = fd; // 更新最大放大
- }
- }
-
- // 设置select,监视文件描述符就绪状态(暂时设置为阻塞)
- int n = select(maxFd + 1, &rfd, nullptr, nullptr, nullptr);
-
- switch(n)
- {
- case 0: // 没有文件描述符就绪
- logMessage(DEBUG, "Time out, without any interest fd prepared!\n");
- break;
- case -1: // select发生错误
- logMessage(ERROR, "Select error, errno:%d, errMsg:%s\n", errno, strerror(errno));
- break;
- default: // 有至少一个文件描述符就绪
- Handler(rfd);
- break;
- }
- }
- }
-
- // 析构函数
- SelectServer::~SelectServer()
- {
- if(_listenSock >= 0)
- close(_listenSock);
- }
-
- // 就绪文件描述符处理函数
- void SelectServer::Handler(fd_set& rfd)
- {
- // 遍历_fd_array,检查有哪个fd就绪了,进行处理
- for(int i = 0; i < FD_CAPACITY; ++i)
- {
- if(_fd_array[i] != NON_FD && FD_ISSET(_fd_array[i], &rfd))
- {
- // 分为listen套接字和普通套接字来处理
- if(_fd_array[i] == _listenSock) Accepter();
- else Reciever(i);
- }
- }
- }
-
- // 数据读取函数
- void SelectServer::Reciever(int pos)
- {
- char buffer[1024];
-
- ssize_t n = recv(_fd_array[pos], buffer, 1023, 0);
- if(n > 0) // 读取成功
- {
- buffer[n] = '\0';
- printf("Recieve message from Client:%s\n", buffer);
- }
- else if(n == 0) // 对端关闭
- {
- logMessage(DEBUG, "Client closed, fd:%d\n", _fd_array[pos]);
- close(_fd_array[pos]);
- _fd_array[pos] = NON_FD;
- }
- else // 读取失败
- {
- logMessage(ERROR, "Recv error, errno:%d, errMsg:%s\n", errno, strerror(errno));
- }
- }
-
- // 链接接收函数
- void SelectServer::Accepter()
- {
- std::string cli_ip;
- uint16_t cli_port; // 客户端ip和端口号
-
- int fd = Sock::Accept(_listenSock, cli_ip, cli_port);
-
- // 连接获取失败 -- fd < 0
- if(fd < 0)
- {
- logMessage(ERROR, "Aeecpt fail, errno:%d, errMsg:%s\n", errno, strerror(errno));
- }
- else // 连接获取成功
- {
- // 将获取到的新连接的fd添加到_fd_array中去
- int index = 0;
- for(; index < FD_CAPACITY; ++index)
- {
- if(_fd_array[index] == NON_FD)
- {
- _fd_array[index] = fd;
- break;
- }
-
- if(index == FD_CAPACITY)
- {
- logMessage(DEBUG, "_fd_array is already full, insert new fd fail, fd:%d\n", fd);
- }
- else
- {
- logMessage(NORMAL, "Insert new fd success, fd:%d\n", fd);
- }
- }
- }
- }
-
- // 打印输出_fd_array
- void SelectServer::ShowFdArray()
- {
- std::cout << "_fd_array[]: " << std::flush;
- for(const auto fd : _fd_array)
- {
- if(fd != NON_FD) std::cout << fd << " ";
- }
- std::cout << std::endl;
- }
缺点:
优点:适用于存在大量fd,但是只要少量处于活跃状态的场景。
函数原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout)
头文件:#include
函数参数:
- fds:struct pollfd类型数组,更确切的应当写为struct pollfd fds[]
- nfds:所关注的文件描述符数量。
- timeout:最长阻塞等待时间,以秒为单位,如果传-1表示一直阻塞等待直到有fd就绪。
返回值:如果执行成功返回就绪的文件描述符个数,返回0表示等待超时,执行失败返回-1。
下面是struct pollfd的定义式,其中成员fd表示文件描述符,events表示请求事件,即用户告知内核需要关注哪些文件描述符,revents为响应时间,即内核告知用于哪些文件描述符已经就绪。
events是用户传给内核的信息,revents是内核传给用户的信息,他们互不干扰,因此即使这里的的fds依旧是输入输出型参数,也不需要每次调用poll之前重新设定struct poll对象的值,这是poll相对于select的一大优势。
- struct pollfd
- {
- int fd; /* file descriptor */
- short events; /* requested events */
- short revents; /* returned events */
- };
events和revents的值及其对于的含义见表3.2,如果为0表示不关注某个fd或这个fd尚未就绪,如果非0,events用于用户告知OS内核需要关注fd的哪些操作(读/写/异常),revents用户OS内核告知用户fd的哪些状态已经就绪。
| events/revents | 含义 |
|---|---|
| POLLIN | 数据(包括普通数据和高优先级数据)可读。 |
| POLLNORMAL | 普通数据可读。 |
| POLLPRI | 高优先级数据可读,如TCP带有紧急指针的报文。 |
| POLLOUT | 数据(包括普通数据和高优先级数据)可写。 |
| POLLOUTNORMAL | 普通数据可写。 |
| POLLRDHUP | 对方关闭TCP,或对端关闭写操作。 |
| POLLERR | 发生错误。 |
| POLLNVAL | 文件描述符没有打开。 |
Poll服务器的实现与Select服务器的实现十分类似,代码3.1为Poll服务器类的声明,与Select不同的是,其中有一个struct pollfd* _fd_array成员变量,这个成员变量为struct pollfd类型数组,用于告知内核哪些fd需要关心,哪些fd已经就绪。其余包括构造函数、析构函数、服务器运行函数start、就绪文件描述符处理函数Handler、获取客户端连接函数Accepter、读取数据函数Reciever。
代码3.1:PollServer服务器声明(PollServer.hpp头文件)
- #pragma once
-
- #include "Sock.hpp"
- #include
- #include
-
- static const int FD_CAPACITY = 100;
- static const int NON_FD = -1;
-
- class PollServer
- {
- public:
- PollServer(uint16_t port, const std::string& ip = "");
- void Start(); // 服务器启动函数
- ~PollServer(); // 析构函数
-
- private:
- void Handler(); // 就绪文件描述符处理函数
- void Reciever(int pos); // 接收信息函数
- void Accepter(); // 接收连接请求
- void ShowFdArray(); // _fd_array打印函数 -- 用于DEBUG
-
- int _listenSock; // 监听套接字
- uint16_t _port; // 服务器进程端口号
- std::string _ip; // 服务器ip
- struct pollfd* _fd_array; // 文件描述符序列
- };
关于Poll服务器的实现,有以下几点需要注意:
代码3.2:PollServer的实现(PollServer.cc源文件)
- #include "PollServer.hpp"
-
- // 构造函数
- PollServer::PollServer(uint16_t port, const std::string& ip)
- : _listenSock(-1), _port(port), _ip(ip)
- , _fd_array(new pollfd[FD_CAPACITY])
- {
- // 获取listen套接字
- _listenSock = Sock::Socket();
- if(_listenSock < 0) {
- exit(2);
- }
-
- // 绑定端口号
- if(Sock::Bind(_listenSock, _ip, _port) < 0) {
- exit(3);
- }
-
- // 设置监听状态
- if(Sock::Listen(_listenSock) < 0) {
- exit(4);
- }
-
- // 初始化pollfd序列
- for(int i = 0; i < FD_CAPACITY; ++i)
- {
- _fd_array[i].fd = -1;
- _fd_array[i].events = _fd_array[i].revents = 0;
- }
-
- // 对listenSock设置读取关心状态
- _fd_array[0].fd = _listenSock;
- _fd_array[0].events = POLLIN;
- }
-
- // 服务器启动函数
- void PollServer::Start()
- {
- while(true)
- {
- ShowFdArray();
- int n = poll(_fd_array, FD_CAPACITY + 1, -1);
- switch(n)
- {
- case 0: // 尚无就绪的文件描述符
- logMessage(DEBUG, "No fd has prepared!\n");
- break;
- case -1: // poll失败
- logMessage(ERROR, "Poll error, errno:%d, errMsg:%s\n", errno, strerror(errno));
- break;
- default: // 有文件描述符就绪
- Handler();
- break;
- }
- }
- }
-
- // 析构函数
- PollServer::~PollServer()
- {
- if(_listenSock >= 0)
- close(_listenSock);
- delete[] _fd_array;
- }
-
-
- // 就绪文件描述符处理函数
- void PollServer::Handler()
- {
- // 遍历查找,有哪一个fd处于就绪状态
- for(int i = 0; i < FD_CAPACITY; ++i)
- {
- if(_fd_array[i].fd != NON_FD && _fd_array[i].revents == POLLIN)
- {
- // 分接收连接请求和读取信息两种情况讨论
- if(_fd_array[i].fd == _listenSock) Accepter();
- else Reciever(i);
- }
- }
- }
-
- // 接收信息函数
- void PollServer::Reciever(int pos)
- {
- char buffer[1024];
- ssize_t n = recv(_fd_array[pos].fd, buffer, 1023, 0);
-
- // 信息读取成功
- if(n > 0)
- {
- buffer[n] = '\0';
- logMessage(NORMAL, "PollServer recieve message success!\n");
- printf("Client Message# %s\n", buffer);
- }
- else if(n == 0) // 对端关闭
- {
- logMessage(DEBUG, "Client closed, fd:%d", _fd_array[pos].fd);
- close( _fd_array[pos].fd);
- _fd_array[pos].fd = NON_FD;
- _fd_array[pos].events = _fd_array[pos].revents = 0;
- }
- else // 读取失败
- {
- logMessage(ERROR, "Get message from Client[%d] success, errno:%d, errMsg:%s\n",
- _fd_array[pos].fd, errno, strerror(errno));
- }
- }
-
- // 接收连接请求
- void PollServer::Accepter()
- {
- std::string cli_ip;
- uint16_t cli_port;
- int fd = Sock::Accept(_listenSock, cli_ip, cli_port);
- if(fd < 0) {
- exit(5);
- }
-
- // 将新的fd添加到_fd_array中去
- int index = 0;
- for(; index < FD_CAPACITY; ++index)
- {
- // 检查_fd_array的空缺位置
- if(_fd_array[index].fd == NON_FD)
- {
- _fd_array[index].fd = fd;
- _fd_array[index].events = POLLIN;
- break;
- }
- }
-
- if(index == FD_CAPACITY) {
- logMessage(DEBUG, "The fd_array has already full, insert new fd fail, fd:%d\n", fd);
- }
- else {
- logMessage(NORMAL, "Insert new fd success, _fd_array[%d]:%d\n", index, fd);
- }
- }
-
- // _fd_array打印函数 -- 用于DEBUG
- void PollServer::ShowFdArray()
- {
- std::cout << "_fd_array[] " << std::flush;
- for(int i = 0; i < FD_CAPACITY; ++i)
- {
- if(_fd_array[i].fd != NON_FD)
- std::cout << _fd_array[i].fd << " ";
- }
- std::cout << std::endl;
- }
优点:
缺点: