【高级IO】- 五种 IO 模型 | 多路转接 - select
目录
IO的基本概念
什么是高效的IO?
五种IO模型
阻塞IO
非阻塞IO
信号驱动IO
IO多路转接
异步IO
同步通信VS异步通信(synchronous communication / asynchronous communication)
同步通信VS同步与互斥
阻塞VS非阻塞
其他高级IO
阻塞IO
非阻塞IO
实现函数 SetNoBlock
以非阻塞轮询释放读取标准输入
I/O多路转接之select
初识 select
关于 fd_set 结构
编辑 关于 timeval 结构
socket 就绪条件
socket 基本工作流程
select 服务器
selectServer 类
timeout 时间测试
事件处理
select 的优缺点
select可监控的fd个数
IO的基本概念
I/O(Input / output)就是输入和输出,在冯诺依曼体系中,将数据从输入设备拷贝到内存叫做输入,将数据从内存拷贝到输出设备叫做输出。
· 对文件进行的读写操作本质就是一种IO,文件IO对应的外设就是磁盘。
· 对网络进行的读写操作本质也就是一种IO,网络IO对应的外设就是网卡。
什么是高效的IO?
IO主要分为两个步骤:
· 第一步是等,就是等待IO条件的就绪。
· 第二步就是拷贝,就是IO条件就绪后将数据拷贝到内存或者外设中。
所以 IO = 等 + 数据拷贝,但是实际中 “等” 消耗的时间往往比 “拷贝” 消耗的时间多,因此让 IO 变得更高效,就是要减少 “等” 的时间。
五种IO模型
阻塞IO
阻塞IO就是内核将数据准备好之前,系统调用会一直等待,所有套接字,默认都是阻塞方式,也是最常见的IO模型:
· 在 recvfrom 函数等待数据就绪期间,在用户看来该进程或者线程被阻塞住了,本质是操作系统,将该进程或者线程的状态设置为了某种非 R 状态,然后放入等待队列当中,当数据就绪后,操作系统再将其从等待队列中唤醒,然后该进程或者线程再将数据从内核拷贝到用户空间。
· 阻塞IO,在 “等” + “数据拷贝” 期间都不会返回,在用户看来就像是阻塞住了。
非阻塞IO
如果内核还未将数据准备好,系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码:
非阻塞IO往往需要程序猿自己循环反复尝试读写文件描述符,这个过程称为轮询,这对CPU来说是较大的浪费,一般只有特定场景下使用。
· 每次调用 recvfrom 函数读取数据时,就算底层数据没有就绪,recvfrom 函数也会立马返回错误信息,后续还需要继续不断调用 recvfrom 函数,直到底层有数据就绪,在用户看来该进程或者线程就没有被阻塞住,称为非阻塞IO。
阻塞IO 和 非阻塞IO 区别:
阻塞IO:当数据没有就绪时,后续检测数据是否就绪工作是操作系统发起的。
非阻塞IO:当数据没有就绪时,后续检测数据是否就绪工作是用户发起的。
信号驱动IO
内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
当底层数据就绪的时候,会向当前进程或者线程发送SIGIO信号,因此可以通过signal或者sigaction函数将SIGIO的信号处理程序自定义为需要进行的IO操作,当底层数据就绪时就会自动执行对应的IO操作。
信号的产生是异步的,但信号驱动IO是同步IO的一种:
· 信号的产生是异步的,因为信号在任何时刻都可能会产生。
· 信号驱动IO是同步IO的一种,当底层数据就绪时,当前进程或者线程需要停下来正在做的事情,转而进行数据的拷贝操作,因此当前进程或者线程仍然需要参加IO的过程。
· 一个IO过程是同步还是异步,看当前进程或者线程是否参与到IO的过程中,参与了是同步IO,没有参与就是异步IO。
IO多路转接
也叫做IO多路复用,最核心在与能够同时等待多个文件描述符的就绪状态。
IO多路转接思想:
· 使用 recvfrom 等接口需要进行等,可以将所有的 “等” 的工作交给多路转接接口(select,poll,epoll)
· 多路转接接口一次 “等” 多个文件描述符,因此能够将 “等” 的时间进行重叠,当数据就绪后再调用对应的 recvfrom 等函数进行数据的拷贝,此时这些函数就能直接进行拷贝,而不需要进行 “等” 操作。
异步IO
由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
· 进行异步IO需要调用一些异步IO的接口,异步IO接口调用后立马进行返回。
· 当IO完成后,操作系统会通知应用程序,因此进行异步IO的进程或者线程并不参与IO的所有细节。
同步通信VS异步通信(synchronous communication / asynchronous communication)
同步和异步关注的是消息的通信机制
· 所谓同步:就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到了返回值了,换句话来说,就是由调用者主动等待这个调用的结果。
· 异步则是相反:调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话来说,当一个异步过程调用发出后,调用者不会立即得到结果,而是最调用发出后,被调用者通过状态,通知来通知调用者,或通过回调函数处理这个调用。
同步通信VS同步与互斥
· 进程 / 线程同步:在保证数据安全的前提下,让进程 / 线程 能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,谈论的是进程 / 线程间的一种工作关系。
· 而同步 IO:进程 / 线程 与操作系统之间的关系,谈论的是 进程 / 线程 是否需要主动参与IO过程。
阻塞VS非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(信息,返回值)的状态:
· 阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
· 非阻塞调用指在不能立即得到结果之前,该调用不会阻塞当前线程。
其他高级IO
非阻塞IO,记录锁,系统V流机制,I/O多路转接(I/O多路复用),readv和writev函数以及存储映射IO(mmap)。
阻塞IO
使用read函数从标准输入当中读取数据:
#include <iostream>
#include <unistd.h>
int main()
{// 阻塞IOchar buffer[1024];while (true){printf("Please#: ");fflush(stdout);ssize_t n = read(0, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n - 1] = 0;std::cout << "echo#: " << buffer << std::endl;}else if (n == 0){std::cout << "me too quit!" << std::endl;break;}else{std::cout << "read error" << std::endl;break;}}
}
将程序运行,如果不进行输入操作,此时该进程就会阻塞住,根本原因是底层数据没有进行就绪(无数据),此时read函数在进行阻塞等待:
此时进行输入操作,read函数检测到底层数据就绪时,立马将数据读取从内核拷贝到buffer数组中,进而将数据进行输出:
非阻塞IO
打开文件时默认都是以阻塞的方式打开的,如果要以非阻塞的方式打开某个文件,需要在使用open函数打开文件时,需要携带 O_NONBLOCK,或者 O_NDELAY 选项,此时就能够以非阻塞的方式打开文件。
如果要将已经打开的某个文件或者套接字设置为非阻塞,需要用到 fcntl 函数。
• fd:已经打开的文件描述符
• cmd:需要进行的操作
• ... :可变参数,传入的cmd不同,后面追加的参数也不同。
fcntl 函数有5种功能:
• 复制一个现有的描述符(cmd = F_DUPFD)
• 获得 / 设置文件描述符标记(cmd = F_GETFD 或 F_SETFD)
• 获得 / 设置文件状态标记(cmd = F_GETFL 或 F_SETFL)
• 获得 / 设置异步 I/O 所有权(cmd = F_GETOWN 或 F_SETOWN)
• 获得 / 设置记录锁(cmd = F_GETLK,F_SETLK 或 F_SETLKW)
如果函数调用成功,返回值取决于具体的进行对操作;调用失败,返回 -1,错误码被设置。
实现函数 SetNoBlock
基于 fcntl,实现一个SetNoBlock函数,将文件描述符设置为非阻塞:
void SetNonBlock(int fd)
{// 将fd设置为非阻塞int fl = fcntl(fd, F_GETFL); // 获取一下fd的状态(GETFD 获取值)if (fl < 0){std::cerr << "fcntl: " << strerror(errno) << std::endl;return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
• 使用 F_GETFL 将当前的文件描述符的属性取出来(这是一个位图)
• 再使用 F_SETFL 将文件描述符设置回去,设置回去的同时,加上一个 O_NONBLOCK参数。
以非阻塞轮询释放读取标准输入
在调用read函数之前,先调用SetNonBlock函数将0号文件描述符设置为非阻塞:
#include "util.hpp"
#include <cstdlib>
#include <vector>
#include <functional>
using namespace std;using func_t = function<void()>;#define INIT(v) \do \{ \v.push_back(printlog); \v.push_back(installlog); \} while (0);#define EXITLOG(v) \do \{ \for (const auto &e : v) \{ \e(); \} \} while (0);void printlog()
{std::cout << "this is a print log!" << std::endl;
}void installlog()
{std::cout << "this is a install log!" << std::endl;
}int main()
{// 非阻塞式IO 在非阻塞期间能做自己的事情SetNonBlock(0); // 将fd设置为非阻塞vector<func_t> vf;INIT(vf);// 阻塞IOchar buffer[1024];while (true){// printf("Please#: ");// fflush(stdout);ssize_t n = read(0, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n - 1] = 0;std::cout << "echo#: " << buffer << std::endl;}else if (n == 0){std::cout << "me too quit!" << std::endl;break;}else{// 读取出现错误 显示资源未就绪 错误?根据错误码分析是否是真的read错误,还是无资源// std::cout << "strerror: " << strerror(errno) << "errno: " << errno << std::endl;// break;if(errno == EAGAIN){//非阻塞cout << "此时只是无数据,没有错误!" << endl;EXITLOG(vf);}else if(errno == EINTR){cout<<"接受到了阻断信号,也不是read错误!"<<endl;continue;}else{cout << "strerror: " << strerror(errno) << "errno: " << errno << endl;break;}}sleep(1);}return 0;
}
此时,read 函数以非阻塞方式读取标准输入时,底层数据不就绪,read 函数就会立即返回(以错误的形式),错误码被设置为 EAGAIN 或 EWOULDBLOCK:
在以非阻塞方式进行读取时,在read 函数返回值是 -1 的情况下,还需要根据错误码进一步判断,如果错误码是 EAGAIN 或者 EWOULDBLOCK,说明此时底层数据还没有就绪,还需要进行轮询检测,read 函数在读取到数据之前可能会被其他信号中断,也会以错误的形式返回,错误码被设置为 EINTR,此时应该重新执行 read 函数进行数据读取:
运行程序,此时没有输入数据,程序就会不断调用 read 函数检测底层数据是否就绪:
当进行输入操作后,read 函数就会轮询检测,read 函数立马将数据读取从内核拷贝到 buffer 数组中,并进行输出:
I/O多路转接之select
初识 select
select 的函数原型:
系统提供 select 函数来实现多路复用输入 / 输出模型
• select 系统调用是用来让程序监视多个文件描述符的状态变化的。
• 程序会停在 select 这里等待,直到被监视在文件描述符有一个或者多个发生了状态改变。
参数介绍:
• nfds:需要监视的文件描述符当中,最大的文件描述符值 +1。
• readfds:输入输出型参数,调用时用户告知内核需要检视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪。
• writefds:输入输出型参数,调用时用户告知内核需要检视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪。
• exceptfds:输入输出型参数,调用时用户告知内核需要检视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪。
• timeout:输入输出型参数,调用时由用户设置 select 的等待时间,返回时表示 timeout 的剩余时间。
参数 timeout 的取值:
• NULL:则表示 select 函数没有 timeout,select 将一直被阻塞,直到某个文件描述符上发生了事件
• 0:仅检测描述符集合的状态,然后立即返回,并不等待外部时间的发生。
• 特定的时间值:如果指定的时间段里面没有事件发生,select 将超时返回。
返回值说明:
• 如果函数调用成功,则返回有事件就绪的文件描述符个数,调用失败,返回 -1,错误码被设置。
• timeout时间耗尽,则返回0。
关于 fd_set 结构
本质是一个位图结构,用位图中对应的位置来表示要监视的文件描述符:
调用 select 函数之前,需要定义fd_set对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集合当中,系统提供了一组操作 fd_set 的接口,方便操作位图:
关于 timeval 结构
select 的最后一个参数 timeout,是一个指向 timeval 结构的指针,timeval 结构用于描述一段时间长度,该结构中有两个成员变量,tv_sec 表示秒,tv_usec 表示微秒。
socket 就绪条件
读就绪:
• socket 内核中,接收缓冲区中的字节数,大于等于低水位标记 SO_RECVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
• socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
• 监听的socket上有新的连接请求
• socket 上有未处理的错误
写就绪:
• socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于水位标记 SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
• socket 的写操作被关闭(close 或者 shutdown)对一个写操作被关闭的 socket 进行写操作,会触发SIGPIPE信号
• socket 使用非阻塞 connect 连接成功或失败之后
• socket 上有未读取的错误
异常就绪:
• socket 上收到带外数据(带外数据和TCP的紧急模式相关,TCP报头当中的URG标志位和16位紧急指针搭配使用,就能发送和接收带外数据)
socket 基本工作流程
实现一个简单的 select 服务器,该服务器做的内容就是读取客户端发来的数据进行输出打印即可:
• 初始化服务器,套接字的创建,绑定端口号和IP地址等,设置监听状态
• 定义一个 _fdarray 数组用于保存监听套接字和已经与客户端建立好连接的套接字,一开始只有监听套接字就绪,先把监听套接字添加到 _fdarray 数组中
• 服务器开始运行,一直轮询调用 select 函数,检测读事件是否就绪,有事件就绪了就执行对应的操作
• 在每次调用 select 函数之前,需要定义一个文件描述符集 rfds,把 _fdarray 中的文件描述符依次添加到集合中,让 select 函数检测这些文件描述符中哪些文件描述符的读事件是否就绪。
• 当 select 检测到数据就绪时,就会将读事件就绪的文件描述符设置进入到 rfds 中,这个参数是一个输入输出型参数,就可以知道哪些文件描述符就绪了,并对这些文件描述符进行对应的操作
• 如果就绪的是监听套接字,就调用 accept 函数从底层全连接队列获取已经建立好的连接,并且将对应的套接字添加到 _fdarray 数组中
• 如果就绪的是与客户端建立连接的套接字,就调用 read 函数将客户端发送到数据进行接收,并进行打印输出
• 服务器与客户端建立连接的套接字读事件就绪,也有可能是因为客户端将连接的套接字关闭了,此时服务器应该调用 close 关闭该套接字,并将该套接字从 _fdarray 数组中移除
细节:
1、select 函数除了 nfds之外,其他都是输入输出型参数,当 select 返回时,这些参数的值都已经被修改了,每次调用时 select 函数需要对参数进行重新设置,timeout 也一样需要设置
2、select 需要传入的被监视的文件描述符中最大描述符值 +1,在遍历 _fdarray 时,需要记录最大文件描述符的值。
select 服务器
socket 类:
编写一个 Socket 类,对套接字的接口进行一定程度的封装,为了外部通过作用域直接调用Socket 类当中的封装函数,将这些接口函数定义成为静态成员函数:
// 进行封装 TCP socket 编写
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "logMessage.hpp"
#include "error.hpp"#define gbacklog 5class Sock
{
public:static int Socket(){// 1.创建套接字 面向字节流int listensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "Create Socket Fail!");exit(SOCKET_ERR);}logMessage(NORMAL, "Create Socket Success!");// 处理一下 Time_Wait 导致无法绑定端口号问题int opt = 1;setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));return listensock;}static void Bind(int sock, int port){// 2.bindstruct sockaddr_in peer;bzero(&peer, sizeof(peer));peer.sin_addr.s_addr = INADDR_ANY; // IP地址绑定任意的peer.sin_family = AF_INET;peer.sin_port = htons(port);if (bind(sock, (struct sockaddr *)&peer, sizeof(peer)) < 0){logMessage(FATAL, "Bind Socket Fail!");exit(BIND_ERR);}logMessage(NORMAL, "Bind Socket Success!");}static void Listen(int sock){// 3.Socket 设置为监听状态if (listen(sock, gbacklog) < 0){// 设置失败logMessage(FATAL, "Listen Socket Fail!");exit(LISTEN_ERR);}logMessage(NORMAL, "Listen Socket Success!");}// 获取新连接static int Accpet(int listensock, uint16_t *clientPort, std::string *clientIP){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(FATAL, "Accept Socket Fail!");exit(ACCEPT_ERR);}else{logMessage(NORMAL, "Accpet Socket Success,get a new sock: %d", sock);*clientPort = ntohs(peer.sin_port); // 网络转主机*clientIP = inet_ntoa(peer.sin_addr); // 网络转主机,再转字符串}return sock;}
};
selectServer 类
所编写的服务器在绑定时,不需要显示绑定IP地址,直接设置为 INADDR_ANY 即可,在类的成员变量中,只需要包含监听套接字,和端口号即可。
运行服务器:
select 服务器要做的是不断调用select 函数,当有事件就绪时,做出处理动作
• 需要将数组中所有位置的值初始化为无效,并将监听套接字添加到数组的第一个位置
• select 函数返回后,如果返回0,说明 timeout 时间耗尽,直接进行下一次的 select 调用即可,返回 -1,根据返回的错误码来进一步判断,是否需要下一次继续调用 select 函数,返回值大于0,调用成功,已经有文件描述符的读事件就绪,对就绪事件进一步处理
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <functional>#include "sock.hpp"namespace Server
{using func_t = std::function<std::string(const std::string &)>;const uint16_t defaultPort = 8080;const int fdnum = sizeof(fd_set) * 8;const int defaultnum = -1;class SelectServer{public:SelectServer(func_t f, int port): _func(f), _listensock(-1), _port(port){}~SelectServer(){if (_listensock){close(_listensock);}if (_fdarray){delete[] _fdarray;}}void InitServer(){_listensock = Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);_fdarray = new int[fdnum];for (int i = 0; i < fdnum; i++){_fdarray[i] = defaultnum;}_fdarray[0] = _listensock;}void Start(){// 等 + 处理数据 select返回的是fd的个数for (;;){fd_set rfd;// 初始化rfdFD_ZERO(&rfd);int maxfd = _fdarray[0]; // 最大fd数 +1for (int i = 0; i < fdnum; i++){if (_fdarray[i] == defaultnum){continue;}// 将这个fd 设置进入rfd中FD_SET(_fdarray[i], &rfd);if (_fdarray[i] > maxfd){maxfd = _fdarray[i];}}logMessage(NORMAL, "max fd is: %d\n", maxfd);// 非阻塞 每隔一秒,询问一下//struct timeval _timeval = {1, 0};// int n = select(maxfd + 1, &rfd, nullptr, nullptr, &_timeval);int n = select(maxfd + 1, &rfd, nullptr, nullptr, nullptr);switch (n){case 0:// 超时logMessage(NORMAL, "time out ...");break;case -1:logMessage(WARNING, "select errno code: %d, select errno message: %s", errno, strerror(errno));break;default:// 走到这里说明有fd就绪了,需要进行处理,但是只有 listensock 就绪logMessage(NORMAL, "have event ready!\n");// 进行业务逻辑处理Handerevent(rfd);break;}}}private:int _port;int _listensock;int *_fdarray; // 维护的accpet的fd数组func_t _func;};
}
timeout 时间测试
在运行服务器时,需要实例化一个对象,对select 服务器进行初始化并调用 Start 函数运行服务器:
#include <iostream>
#include <string>
#include <memory>#include "error.hpp"
#include "selectServer.hpp"void Usage(std::string arg)
{std::cout << "\n Usage: \n\t" << arg << " port"<< "\n\t" << std::endl;
}// ./main 8080
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);}std::unique_ptr<Server::SelectServer> us(new Server::SelectServer(transmition, atoi(argv[1])));us->InitServer();us->Start();return 0;
}
此时的服务器 select 函数将 timeout 设置为了 nullptr,因此select 函数调用时会阻塞等待,服务器在第一次调用 select 时,只监视监听套接字(listensock),运行服务器后,客户端没有请求发送过来请求连接,读事件就不会就绪,服务器第一次调用 select 函数中就会进行阻塞等待:
使用 telnet 工具向服务器发起连接请求,此时 select 函数会立马检测到监听套接字的读事件就绪,然后对套接字做出对应的处理逻辑:
如果此时将timeout的值设置为0,select 函数调用后就会进行非阻塞等待,无论被监视的文件描述符是否就绪,检测后都会立马返回,如果有事件就绪,select 函数的返回值就会大于0,没有事件就绪,返回值就会等于0:
struct timeval _timeval = {0, 0};int n = select(maxfd + 1, &rfd, nullptr, nullptr, &_timeval);switch (n){case 0:// 超时logMessage(NORMAL, "time out ...");break;case -1:logMessage(WARNING, "select errno code: %d, select errno message: %s", errno, strerror(errno));break;default:// 走到这里说明有fd就绪了,需要进行处理,但是只有 listensock 就绪logMessage(NORMAL, "have event ready!\n");// 进行业务逻辑处理Handerevent(rfd);break;}
此时如果没有客户端发送连接请求,select 函数就会一致进行轮询检测,每次检测的读事件都不就绪,返回结果都是0,就会造成 time out ... 现象。
如果将 timeout 时间设置为特定的时间,比如这里设置为5s,那么select 函数调用后5秒内会进行阻塞等待,5秒后依旧没有读事件就绪,就会超时返回:
struct timeval _timeval = {5, 0};
运行服务器,此时无客户端发送到连接请求,select 函数调用5秒后都会超时返回:
如果在5秒内有读事件就绪,那么 timeout 就会返回剩余的时间,所以在每次调用select 函数时,都需要对 timeout 时间重新设置。
事件处理
当select 检测到有文件描述符就绪时并成功返回后,就需要对就绪事件进行处理:
• 需要遍历整个 _fdarray 数组当中的文件描述符,依次判断各个文件描述符的都事件是否就绪,如果就绪进行处理
• 如果文件描述符就绪后,还需要判断该文件描述符是否是监听套接字,如果是监听套接字就绪,就需要调用 accept 函数,将底层的连接获取上来,并添加到 _fdarray 数组当中,在下一次调用select 函数之前,将 _fdarray 中的文件描述符设置进入到 rfds 中
• 如果是客户端建立的连接对应的读事件就绪,就需要调用 read 函数读取客户端发来的数据,如果读取成功就将读到的数据进行在服务器端打印,如果读取失败或者客户端关闭了连接,那么服务器就调用close 函数关闭对应的连接,并且将对应的文件描述符从 _fdarray 中移除
void Accpeter(int listensock){// 获取新连接后,直接添加进入到 _fdarray 数组中logMessage(NORMAL, "Accpeter begin ...\n");uint16_t clientPort = 0;std::string clientIP;int sock = Sock::Accpet(listensock, &clientPort, &clientIP);if (sock < 0){return;}int i = 0;for (; i < fdnum; i++){if (_fdarray[i] != defaultnum){continue;}else{break;}}// 找到位置if (i == fdnum){// 说明已经满了logMessage(WARNING, "server is full,please wait ...\n");}else{_fdarray[i] = sock;}// 进行打印Print();logMessage(NORMAL, "Accpeter end ...\n");}void Recver(int sock, int i){// 通过sock这个fd进行接受数据logMessage(NORMAL, "Recver begin ...\n");char buffer[1024];ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 这里读取不会阻塞,只有sock就绪了,才进来if (n > 0){buffer[n - 1] = 0;logMessage(NORMAL, "client# %s", buffer);}else if (n < 0){// 读取错误close(sock);_fdarray[i] = defaultnum;logMessage(WARNING, "recv error# %s", strerror(errno));return;}else // == 0{// client 退出了close(sock);_fdarray[i] = defaultnum;logMessage(NORMAL, "client quit,me too ...");return;}// 此时数据都在buffer当中,处理 requeststd::string response = _func(buffer);write(sock, response.c_str(), response.size());logMessage(NORMAL, "Recver end ...\n");}// 处理逻辑void Handerevent(fd_set &rfd){// 判断是listensock,还是普通sock的for (int i = 0; i < fdnum; i++){if (_fdarray[i] == defaultnum){continue; // 后面需要进行置空,不能break}if (_fdarray[i] == _listensock && FD_ISSET(_fdarray[i], &rfd)){// 需要进行accpetAccpeter(_fdarray[i]);}else if (FD_ISSET(_fdarray[i], &rfd)){// 其他fd 而且就绪Recver(_fdarray[i], i);}else{}}}
select 的优缺点
优点:
• 可以同时等待多个文件描述符,并且只负责等待,实际的IO操作由accept,read,write 函数来完成,这些接口在进行IO操作时不会被阻塞。
• select 同时等待多个文件描述符,因此可以将“等”的事件重叠,提高IO的效率。
缺点:
• 每次调用select,都需要手动设置fd集合,从接口使用角度来说非常不方便
• 每次调用select,都需要把fd集合从用户拷贝到内核态,这个开销在fd很多时会很大
• 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
• select支持的文件描述符数量太小
select可监控的fd个数
fd_set结构本质是一个位图,用每一个比特位来标记一个文件描述符,select可监控的文件描述符个数取决于fd_set类型的比特位个数,通过计算(sizeof(fd_set))* 8 可得总共1024个字节,说明可监控的文件描述符个数为1024,在初始化 _fdarray 数组时,需要将数组大小定义为 1024即可:
const int fdnum = sizeof(fd_set) * 8;
相关文章:

【高级IO】- 五种 IO 模型 | 多路转接 - select
目录 IO的基本概念 什么是高效的IO? 五种IO模型 阻塞IO 非阻塞IO 信号驱动IO IO多路转接 异步IO 同步通信VS异步通信(synchronous communication / asynchronous communication) 同步通信VS同步与互斥 阻塞VS非阻塞 其他高级IO …...

在Linux搭建GitLab私有仓库配置实现远程访问私有仓库Gitlab ——【内网穿透】
🎬 鸽芷咕:个人主页 🔥 个人专栏: 《高效编程技巧》《cpolar》 ⛺️生活的理想,就是为了理想的生活! 文章目录 前言1. 下载Gitlab2. 安装Gitlab3. 启动Gitlab4. 安装cpolar5. 创建隧道配置访问地址6. 固定GitLab访问地址6.1 保留…...

ChatGPT应用于高职教育的四大潜在风险
目前,ChatGPT还是一种仍未成熟的技术,当其介入高职教育生态后,高职院校师生在享受ChatGPT带来的便利的同时,也应该明白ChatGPT引发的风险也会随之进入高职教育领域,如存在知识信息、伦理意识与学生主体方面的风险与挑战…...
uni-app在组件中内嵌webView,实现自定义webView的大小,并处理页面中有webview时其他元素点击事件失效的问题
uni-app在组件中内嵌webView,实现自定义webView的大小,并处理页面中有webview时其他元素点击事件失效的问题 uni-app在组件中内嵌webView,实现自定义webView的大小 setWebviewTop() {// #ifdef APP-PLUSvar currentWebview this.$scope.$g…...

档案开发:增加查询和打卡按钮
档案开发:增加查询和打卡按钮 和单据开发的不同点 没有单据类型不是右击–>特性–>单据主表/单据子表,而是右击–>特性–>选择想要的接口访问器类型是NCVO不需要映射不是项目右键–>新建–>其他–>主子表单据结点,而是…...
redis基础细心讲解,一篇了解常用的缓存技术!
今日内容 redis 1. 概念 2. 下载安装 3. 命令操作1. 数据结构 4. 持久化操作 5. 使用Java客户端操作redis 6. 在ssm项目中使用缓冲进行CRUD操作Redis 1. 概念 redis是一款高性能的NOSQL系列的非关系型数据库 1.1.什么是NOSQL NoSQL(NoSQL = Not Only SQL),意即“不仅仅…...
Three.js之几何体、高光材质、渲染器设置、gui
参考资料 阵列立方体和相机适配体验Threejs常见几何体简介…gui.js库(可视化改变三维场景) 知识点 注:基于Three.jsv0.155.0 长方体:oxGeometry球体:SphereGeometry圆柱:CylinderGeometry矩形平面:PlaneGeometry圆…...

UE4如何连接dmx---摇头矩阵灯具的创建
UE4如何连接dmx---摇头矩阵灯具的创建 开始创建库! 然后我们开始创建多少个灯珠(注意了:这是矩阵灯,是看灯珠的) 那么这里我们创建6X6灯珠 下面设置灯珠的属性,灯珠有什么属性呢,只有颜色属性&…...

网络聊天室
一、项目要求 利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。 问题思考 客户端会不会知道其它客户端地址? UDP客户端不会直接互连,所以不会获知其它客…...

ChatGPT只是玩具:生成式人工智能在不同行业的应用
源自:IT经理网 生成式人工智能的十一个行业用例 打开生成式 AI的正确姿势 声明:公众号转载的文章及图片出于非商业性的教育和科研目的供大家参考和探讨,并不意味着支持其观点或证实其内容的真实性。版权归原作者所有,如转载稿涉及版权等问题&…...
RestFul的风格是什么
RestFul的风格是什么? 当我们谈论RESTful风格时,它指的是一种设计和构建网络应用程序的原则和约定。以下是RESTful风格的一些主要特点: 资源:将应用程序的功能封装为资源,每个资源都有一个唯一的标识符(U…...

【自制C/C++小项目JuLongEditor】使用Windows控制台API来制作一个简单的文本编辑器
2023年8月22日,周二下午 昨天花了一个下午和晚上来制作的, 实现了一些基本的功能, 但由于代码只有130行,所以存在很多不足之处 GitHub:GitHub - JuLongZhiLu/JuLongEditor: C/C小项目,使用Windows控制台…...

中国芯,寻找新赛道迫在眉睫
北京华兴万邦管理咨询有限公司 商瑞 陈皓 近期国内半导体行业的热点可以用两个“有点多”来描述,一个是中国芯群体中上市公司股价闪崩的有点多,另一个是行业和企业的活动有点多。前者说明了许多国内芯片设计企业(fabless商业模式)…...

C++ 好用的格式化库--fmt
背景 fmt 库是一个开源的 C 格式化库,它提供了一种简洁、安全和高效的方式来进行字符串格式化。该库的设计目标是提供与 Python 的字符串格式化语法类似的功能,同时保持 C 的类型安全性和性能。 下载与安装 官网下载 fmt 官网地址:https:…...
微信小程序教学系列(3)
微信小程序教学系列 第三章:小程序高级开发技巧 1. 小程序API的使用 小程序API简介 小程序API是小程序提供的一系列接口,用于实现各种功能和操作。通过调用小程序API,可以实现页面跳转、数据存储、网络请求等功能。 使用小程序API的步骤…...

ORB-SLAM系列算法演进
ORB-SLAM算法是特征点法的代表,当前最新发展的ORB-SLAM3已经将相机模型抽象化,适用范围非常广,虽然ORB-SLAM在算法上的创新并不是很丰富,但是它在工程上的创新确实让人耳目一新,也能更好的为AR、机器人的算法实现落地。…...

solidity0.8.0的应用案例11:透明代理合约
选择器冲突 智能合约中,函数选择器(selector)是函数签名的哈希的前4个字节。例如mint(address account)的选择器为bytes4(keccak256("mint(address)")),也就是0x6a627842. 由于函数选择器仅有4个字节,范围很小,因此两个不同的函数可能会有相同的选择器,例如…...

最新消息:谷歌将在Chromebook上运用UWB技术,无线通信更上一层
超宽带(UWB)技术是一种创新的短距离无线通信技术,具有高速数据传输和精确定位物体位置的优势。尽管该技术已经存在一段时间,但最近开始广泛应用于各种设备中。据最新报道,Pixel Watch 2可能会搭载UWB模块,这…...

php+echarts实现数据可视化实例3
效果 全部代码 <?php include(includes/session.inc); include(includes/SQL_CommonFunctions.inc); ?> <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" …...
ubuntu下安装Sphinx,编译pdf
安装WSL2: 以管理员身份打开PowerShellwsl --install 来安装其他 Linux 发行版wsl --list --verbose 查看安装在 Windows 计算机上的 Linux 发行版列表 安装sphinx: sudo apt-get updatesudo apt-get install python3-sphinxsudo apt-get install lat…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...

Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...
生成 Git SSH 证书
🔑 1. 生成 SSH 密钥对 在终端(Windows 使用 Git Bash,Mac/Linux 使用 Terminal)执行命令: ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 参数说明: -t rsa&#x…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...

NLP学习路线图(二十三):长短期记忆网络(LSTM)
在自然语言处理(NLP)领域,我们时刻面临着处理序列数据的核心挑战。无论是理解句子的结构、分析文本的情感,还是实现语言的翻译,都需要模型能够捕捉词语之间依时序产生的复杂依赖关系。传统的神经网络结构在处理这种序列依赖时显得力不从心,而循环神经网络(RNN) 曾被视为…...

Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...

mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
Vue3中的computer和watch
computed的写法 在页面中 <div>{{ calcNumber }}</div>script中 写法1 常用 import { computed, ref } from vue; let price ref(100);const priceAdd () > { //函数方法 price 1price.value ; }//计算属性 let calcNumber computed(() > {return ${p…...

C++--string的模拟实现
一,引言 string的模拟实现是只对string对象中给的主要功能经行模拟实现,其目的是加强对string的底层了解,以便于在以后的学习或者工作中更加熟练的使用string。本文中的代码仅供参考并不唯一。 二,默认成员函数 string主要有三个成员变量,…...