c++socket的select函数以及多路复用

c++socket的select函数以及多路复用

  • 1.select函数原型
    • 使用示例及解释
  • 2.select()函数返回值
  • 3.select()函数与socket阻塞的关系与原因
  • 4.select函数的意义
  • 5.深入理解select模型
    • (1)特点
    • (2)带外数据
    • (2)select函数相关的常见的几个宏
    • (3)select使用范例及结论
  • 6.多路复用及代码示例

1.select函数原型

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常量来表示文件描述符集合的大小。

2.select()函数返回值

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是会出现的错误

3.select()函数与socket阻塞的关系与原因

关系:
select()与socket是否阻塞没有任何关系,也就是说,无论socket是阻塞状态还是非阻塞态都可以与select进行搭配。
原因:
当某个设备是阻塞的时候,相当于处于sleep的状态,即:让出CPU,
但当设备处于非阻塞态时,会不断的轮询,占用CPU,故默认的是阻塞态。

4.select函数的意义

1)select函数是多路IO复用,故当有多路IO的时候,会发挥优势:
2)当某一路IO处于阻塞态时,select监视到其他的IO有读或写,就可以让其他路的IO读写;如果没有select,当有一路IO阻塞,其他的IO即使可以读写,也无法进行。

5.深入理解select模型

(1)特点

取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判断是否有事件发生。

(2)带外数据

带外数据(out—of—band data),有时也称为加速数据(expedited data),
是指连接双方中的一方发生重要事情,想要迅速地通知对方。
这种通知在已经排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。
带外数据设计为比普通数据有更高的优先级。
带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。

(2)select函数相关的常见的几个宏

#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); //测试某个位是否被置位

(3)select使用范例及结论

当声明了一个文件描述符集后,必须用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请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

6.多路复用及代码示例

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;
}


你可能感兴趣的:(c++,笔记,c++,数据库,sql)