int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
参数说明:
nfds:需要监视的最大文件描述符值加1。
readfds、writefds、exceptfds:分别表示可读、可写和异常事件的文件描述符集合。这些集合是用fd_set结构体表示的。
timeout:指定超时时间,如果为NULL,则select函数会一直阻塞,直到有事件发生。如果指定为0,则select函数会立即返回,用于轮询文件描述符的状态。
select函数的返回值是就绪文件描述符的个数。如果返回0,表示超时;如果返回-1,表示出错。
使用select函数的步骤如下:
(1)准备需要监视的文件描述符集合,并初始化它们。
(2)调用select函数,进行文件描述符的监视。
(3)检查select函数的返回值,判断哪些文件描述符就绪。
(4)对就绪的文件描述符进行相应的操作
#include
#include
#include
int main() {
int sockfd;
fd_set readfds;
struct timeval timeout;
// 创建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 设置需要监视的文件描述符集合
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
// 设置超时时间
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// 监视文件描述符
int ret = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
std::cout << "select error" << std::endl;
} else if (ret == 0) {
std::cout << "timeout" << std::endl;
} else {
if (FD_ISSET(sockfd, &readfds)) {
std::cout << "socket is readable" << std::endl;
}
}
return 0;
解释:
首先创建了一个socket,并将其添加到readfds集合中。然后设置了一个超时时间为5秒,调用select函数进行文件描述符的监视。如果select函数返回0,表示超时;如果返回-1,表示出错;如果返回大于0,表示有文件描述符就绪。通过FD_ISSET宏来判断sockfd是否就绪,如果就绪则输出"socket is readable"。
需要注意的是,在使用select函数之前,需要将文件描述符集合进行初始化,并将需要监视的文件描述符添加到对应的集合中。另外,如果监视的文件描述符集合很大,可以使用FD_SETSIZE常量来表示文件描述符集合的大小。
select的返回值与recv函数的返回值十分类似,都分>0,=0,<0三种情况。
(1) >0:有事件发生
(2)=0:timeout,超时
此时需要先FD_ZERO(),FD_SET();再select。
原因:
要监视的文件描述符在位图中被置1,当有事件发生时,文件描述符仍为1,但没有事件发生的文件描述符会被置0,所以,要想监视所有的文件描述符,第2此循环需要重新将所有的文件描述符进行FD_SET();置1设置。
(3)<0:出错。
在出错的情况下,当错误为EINTR,认为连接是正常的,继续接收
if(errno == EINTR)continue;
报此错误需要继续select;不需要FD_ZERO(),FD_SET();
select函数没有(errno == EAGAIN)||errno == EWOULDBLOCK)错误;
这两种错误是recv和send是会出现的错误
关系:
select()与socket是否阻塞没有任何关系,也就是说,无论socket是阻塞状态还是非阻塞态都可以与select进行搭配。
原因:
当某个设备是阻塞的时候,相当于处于sleep的状态,即:让出CPU,
但当设备处于非阻塞态时,会不断的轮询,占用CPU,故默认的是阻塞态。
1)select函数是多路IO复用,故当有多路IO的时候,会发挥优势:
2)当某一路IO处于阻塞态时,select监视到其他的IO有读或写,就可以让其他路的IO读写;如果没有select,当有一路IO阻塞,其他的IO即使可以读写,也无法进行。
取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set); 则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环加fd,取maxfd,select返回后利用FD_ISSET判断是否有事件发生。
带外数据(out—of—band data),有时也称为加速数据(expedited data),
是指连接双方中的一方发生重要事情,想要迅速地通知对方。
这种通知在已经排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。
带外数据设计为比普通数据有更高的优先级。
带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。
#include
int FD_ZERO(int fd, fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用
int FD_SET(int fd, fd_set *fdset); //设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位
当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位,操作如下:
fd_set rset;
int fd;
FD_ZERO(&rset);
FD_SET(fd, &rset);
FD_SET(stdin, &rset);
然后调用select函数,拥塞等待文件描述符事件的到来;如果超过设定的时间,则不再等待,继续往下执行
select(fd+1, &rset, NULL, NULL,NULL);
select返回后,用FD_ISSET测试给定位是否置位:
if(FD_ISSET(fd, &rset)
{
...
//do something
}
结论:
select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
C++中使用多路复用技术来处理socket可以提高程序的性能和效率。多路复用允许一个进程同时监听多个socket,当任何一个socket准备好进行读写操作时,进程就可以对其进行相应的处理。
在C++中,可以使用select、poll或者epoll等系统调用来实现多路复用。这些系统调用可以监视一组socket的状态变化,包括可读、可写、异常等事件。
代码示例:
#include
#include
#include
#include
#include
#include
#include
#include
int main() {
// 创建socket
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == -1) {
std::cerr << "Failed to create socket" << std::endl;
return -1;
}
// 设置socket地址
sockaddr_in serverAddress{};
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons(8080);
serverAddress.sin_addr.s_addr = INADDR_ANY;
// 绑定socket地址
if (bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) == -1) {
std::cerr << "Failed to bind socket" << std::endl;
close(serverSocket);
return -1;
}
// 监听socket
if (listen(serverSocket, SOMAXCONN) == -1) {
std::cerr << "Failed to listen on socket" << std::endl;
close(serverSocket);
return -1;
}
// 设置文件描述符集合
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(serverSocket, &readSet);
// 循环等待socket事件
while (true) {
// 复制文件描述符集合
fd_set tempSet = readSet;
// 调用select等待事件发生
if (select(FD_SETSIZE, &tempSet, nullptr, nullptr, nullptr) == -1) {
std::cerr << "Failed to select" << std::endl;
close(serverSocket);
return -1;
}
// 遍历文件描述符集合
for (int fd = 0; fd < FD_SETSIZE; ++fd) {
// 检查是否有事件发生
if (FD_ISSET(fd, &tempSet)) {
// 如果是serverSocket有事件发生,表示有新的连接请求
if (fd == serverSocket) {
sockaddr_in clientAddress{};
socklen_t clientAddressSize = sizeof(clientAddress);
// 接受新的连接
int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddress, &clientAddressSize);
if (clientSocket == -1) {
std::cerr << "Failed to accept connection" << std::endl;
close(serverSocket);
return -1;
}
std::cout << "New connection accepted" << std::endl;
// 将新的连接加入文件描述符集合
FD_SET(clientSocket, &readSet);
}
// 否则,表示有数据可读
else {
// 读取数据
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t bytesRead = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (bytesRead == -1) {
std::cout << "Failed to receive data" << std::endl;
close(serverSocket);
return -1;
}
// 处理数据
if (bytesRead == 0) {
std::cout << "Connection closed" << std::endl;
close(fd);
FD_CLR(fd, &readSet);
} else {
std::cout << "Received data: " << buffer << std::endl;
}
}
}
}
}
// 关闭socket
close(serverSocket);
return 0;
}