清远医院网站建设方案,大连建设网煤气查询,广告中国第一,网站主体信息目录
1 - I/O多路转接之select
1.1 - 初识select
1.2 - select函数原型
1.3 - 关于fd_set结构
1.4 - 关于timeval结构
2 - 理解select执行过程
2.1 - Socket就绪条件
2.2 - select特点
2.3 - select缺点
3 - select使用示例…目录
1 - I/O多路转接之select
1.1 - 初识select
1.2 - select函数原型
1.3 - 关于fd_set结构
1.4 - 关于timeval结构
2 - 理解select执行过程
2.1 - Socket就绪条件
2.2 - select特点
2.3 - select缺点
3 - select使用示例
3.1 - 只检测标准输入
3.2 - 实现字典服务器 1 - I/O多路转接之select
1.1 - 初识select
系统提供select函数来实现多路复用输入/输出模型。
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会停在select这里等待直到被监视的文件描述符有一个或多个发生了状态改变。
1.2 - select函数原型
select函数原型如下
#include sys/select.hint select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
参数解释
参数nfds是需要监视的最大的文件描述符值1。rdsetwrsetexset分别对应于需要检测的可读文件描述符的集合可写文件描述符的集合及异常文件描述符的集合。参数timeout为结构timeval用来设置select()的等待时间。
参数timeout取值
NULL则表示 select()没有timeoutselect将一直被阻塞直到某个文件描述符上发生了事件。0仅检测描述符集合的状态然后立即返回并不等待外部事件的发生。特定的时间值如果在指定的时间段里没有事件发生select将超时返回。
1.3 - 关于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 的全部位
1.4 - 关于timeval结构
timeval结构用于描述一段时间长度如果在这个时间内需要监视的描述符没有事件发生则函数返回返回值为0。 函数返回值
执行成功则返回文件描述词状态已改变的个数。如果返回0代表在描述词状态改变前已超过timeout时间没有返回。当有错误发生时则返回-1错误原因存于errno此时参数readfdswritefdsexceptfds和timeout的值变成不可预测。
错误值可能为
EBADF文件描述词为无效的或该文件已关闭。EINTR此调用被信号所中断。EINVAL参数n为负值。ENOMEM核心内存不足。
常见的程序片段如下
fs_set readset
FD_SET(fd, readset);
select(fd 1, readset, NULL, NULL, NULL);
if (FD_ISSET(fd, readset)) { …… }
2 - 理解select执行过程
理解select模型的关键在于理解fd_set为说明方便取fd_set长度为1字节fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
执行fd_set setFD_ZERO(set)则set用位表示是0000,0000。若fd5执行 FD_SET(fd,set)后set变为 0001,0000(第5位置为1) 。若再加入fd2fd1则set变为0001,0011。执行select(6,set,0,0,0)阻塞等待。若fd1fd2上都发生可读事件则select返回此时set变为0000,0011。注意没有事件发生的fd5被清空。
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上有未读取的错误。
2.2 - select特点
可监控的文件描述符个数取决于sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)512每bit表示一个文件描述符则我服务器上支持的最大文件描述符是512*84096。将fd加入select监控集的同时还要再使用一个数据结构array保存放到select监控集中的fd。 一是用于再select返回后array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先)扫描array的同时取得fd最大值maxfd用于select的第一个参数。
2.3 - select缺点
每次调用select都需要手动设置fd集合从接口使用角度来说也非常不便。每次调用select都需要把fd集合从用户态拷贝到内核态这个开销在fd很多时会很大。同时每次调用select都需要在内核遍历传递进来的所有fd这个开销在fd很多时也很大。select支持的文件描述符数量太小。
3 - select使用示例
3.1 - 只检测标准输入
#include stdio.h
#include unistd.h
#include sys/select.hint main()
{fd_set read_fds;FD_ZERO(read_fds);FD_SET(0, read_fds);for (;;) {printf( );fflush(stdout);int ret select(1, read_fds, NULL, NULL, NULL);if (ret 0) {perror(select);continue;}if (FD_ISSET(0, read_fds)) {char buf[1024] { 0 };read(0, buf, sizeof(buf) - 1);printf(input: %s, buf);}else {printf(error! invaild fd\n);continue;}FD_ZERO(read_fds);FD_SET(0, read_fds);}return 0;
}
说明
当只检测文件描述符0(标准输入)时因为输入条件只有在你有输入信息的时候才成立所以如果一直不输入就会产生超时信息。
3.2 - 实现字典服务器
tcp_select_server.hpp
#pragma once#include vector
#include unordered_map
#include functional
#include sys/select.h
#include tcp_socket.hpp// 必要的调试函数
inline void PrintFdSet(fd_set* fds, int max_fd)
{printf(select fds: );for (int i 0; i max_fd 1; i) {if (!FD_ISSET(i, fds)) {continue;}printf(%d , i);}printf(\n);
}typedef std::functionvoid(const std::string req, std::string*resp) Handler;// 把 Select 封装成一个类. 这个类虽然保存很多 TcpSocket 对象指针, 但是不管理内存
class Selector
{
public:Selector() {// [注意!] 初始化千万别忘了!!max_fd_ 0;FD_ZERO(read_fds_);}bool Add(const TcpSocket sock) {int fd sock.GetFd();printf([Selector::Add] %d\n, fd);if (fd_map_.find(fd) ! fd_map_.end()) {printf(Add failed! fd has in Selector!\n);return false;}fd_map_[fd] sock;FD_SET(fd, read_fds_);if (fd max_fd_) {max_fd_ fd;}return true;}bool Del(const TcpSocket sock) {int fd sock.GetFd();printf([Selector::Del] %d\n, fd);if (fd_map_.find(fd) fd_map_.end()) {printf(Del failed! fd has not in Selector!\n);return false;}fd_map_.erase(fd);FD_CLR(fd, read_fds_);// 重新找到最大的文件描述符, 从右往左找比较快for (int i max_fd_; i 0; --i) {if (!FD_ISSET(i, read_fds_)) {continue;}max_fd_ i;break;}return true;}// 返回读就绪的文件描述符集bool Wait(std::vectorTcpSocket* output) {output-clear();// [注意] 此处必须要创建一个临时变量, 否则原来的结果会被覆盖掉fd_set tmp read_fds_;// DEBUGPrintFdSet(tmp, max_fd_);int nfds select(max_fd_ 1, tmp, NULL, NULL, NULL);if (nfds 0) {perror(select);return false;}// [注意!] 此处的循环条件必须是 i max_fd_ 1for (int i 0; i max_fd_ 1; i) {if (!FD_ISSET(i, tmp)) {continue;}output-push_back(fd_map_[i]);}return true;}private:fd_set read_fds_;int max_fd_;// 文件描述符和 socket 对象的映射关系std::unordered_mapint, TcpSocket fd_map_;
};class TcpSelectServer
{
public:TcpSelectServer(const std::string ip, uint16_t port) : ip_(ip),port_(port) {}bool Start(Handler handler) const {// 1. 创建 socketTcpSocket listen_sock;bool ret listen_sock.Socket();if (!ret) {return false;}// 2. 绑定端口号ret listen_sock.Bind(ip_, port_);if (!ret) {return false;}// 3. 进行监听ret listen_sock.Listen(5);if (!ret) {return false;}// 4. 创建 Selector 对象Selector selector;selector.Add(listen_sock);// 5. 进入事件循环for (;;) {std::vectorTcpSocket output;bool ret selector.Wait(output);if (!ret) {continue;}// 6. 根据就绪的文件描述符的差别, 决定后续的处理逻辑for (size_t i 0; i output.size(); i) {if (output[i].GetFd() listen_sock.GetFd()) {// 如果就绪的文件描述符是 listen_sock, 就执行 accept, 并加入到 select 中TcpSocket new_sock;listen_sock.Accept(new_sock, NULL, NULL);selector.Add(new_sock);}else {// 如果就绪的文件描述符是 new_sock, 就进行一次请求的处理std::string req, resp;bool ret output[i].Recv(req);if (!ret) {selector.Del(output[i]);// [注意!] 需要关闭 socketoutput[i].Close();continue;}// 调用业务函数计算响应handler(req, resp);// 将结果写回到客户端output[i].Send(resp);}} // end for} // end for (;;)return true;}private:std::string ip_;uint16_t port_;
}; 感谢各位大佬支持
互三啦