随笔 - 3  文章 - 0  评论 - 0  阅读 - 40

【技术学习】网络学习--使用select的IO多路复用的ftp服务器

上一篇文章复习了一下最基础的服务器代码,这次再将代码改为io多路复用的方式。

select函数是一种用于实现I/O多路复用的系统调用。它可以监视多个文件描述符,判断它们是否处于可读、可写或异常等事件状态,并在一个或多个文件描述符就绪时进行处理。

这种方式避免了使用多线程或多进程来同时处理多个文件描述符的大量系统开销,不必创建进程/线程,也不必维护这些进程/线程,提高了程序的效率。

当然这种方式也有缺点,当文件描述符过多的时候,select的遍历整个文件描述符集合操作就显得比较麻烦了。

#include <sys/select.h>
#include <sys/time.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

第一个参数需要用最大文件描述符再加一,确保监听返回,然后是读、写、异常文件描述符集合,超时时间(如果为NULL则为阻塞等待)。

在使用select之前,首先要创建一个文件描述符的集合,将其初始化,,还要把listenfd放入这个集合。创建一个最大文件描述符,直接指向监听描述符。

fd_set rfds;
FD_ZERO(&rfds); // 初始化集合
FD_SET(listenfd, &rfds); // 将监听套接字加入集合中
int max_fd = listenfd;

然后写while循环里的select

复制代码
while (1)
{
    fd_set rset = rfds; //通过这个赋值操作,可以将所有在 allfds 中的文件描述符设置为在 readfds 中处于就绪状态。这样,在调用 select 函数时,只需要监视 readfds 集合,即可判断其中哪些文件描述符已经准备好进行读取操作。
    int fd_num = select(max_fd+1, &rset, NULL, NULL, NULL);
    if (fd_num == -1) 
    {
        std::cerr << "Error in select." << std::endl;
        return -1;
    }
}
复制代码

接下来去判断listenfd是否就绪,如果就绪,就代表监听到连接请求了,我们就accept接收它,并且将新建的connfd放入rfds集合之中。并调整max_fd的位置

复制代码
if (FD_ISSET(listenfd, &rset)){

       struct sockaddr_in client;
        socklen_t len = sizeof(client);
        // 接受连接请求
        connfd = accept(listenfd, (struct sockaddr *)&client, &len);
        if (connfd < 0) {
            std::cerr << "Error in accepting connection." << std::endl;
            return -1;
        }

        FD_SET(connfd, &rfds);

        if (connfd > max_fd){
            max_fd = connfd; //更新最大文件描述符
        }

        if (--fd_num == 0) { 
            continue; //如果当前计数器为最后一个,则表示所有文件描述符已经处理完毕,可以结束当前的循环,进入下一轮循环。
        } 
    }
复制代码

然后就是读写监听操作,循环去检查所有读操作。

复制代码
for (i = listenfd+1;i <= max_fd;i ++)  {
            if (FD_ISSET(i, &rset)) {
                // 读取客户端发来的数据
                bzero(buffer, sizeof(buffer));
                ret = recv(i, buffer, sizeof(buffer), 0);
                if (ret < 0) {
                    std::cerr << "Error in reading data." << std::endl;
                    FD_CLR(i, &rfds); //从集合中删除该文件描述符
                    close(i); //关闭连接套接字
                    continue;
                }
                else if(ret == 0){
                    FD_CLR(i, &rfds);
                    // 关闭连接套接字
                    close(i);
                }
                else{
                    // 输出客户端发送的消息
                    std::cout << "Client message: " << buffer << std::endl;

                    // 发送响应给客户端
                    const char *response = "Hello from server!";
                    if (send(i, response, strlen(response), 0) < 0) {
                        std::cerr << "Error in sending response." << std::endl;
                        FD_CLR(i, &rfds); //从集合中删除该文件描述符
                        close(i); //关闭连接套接字
                        continue;
                    }
                }
                
                if (--fd_num == 0) { 
                    break; //如果当前计数器为最后一个,则表示所有文件描述符已经处理完毕,可以结束for循环。
                } 
            }
        }
复制代码

这样就解决了一次请求就开一次线程的系统消耗,所有的套接字都放在一起管理,我们可以把select理解成酒店大堂经理,合理的管理着所有顾客需求

整体代码如下,

复制代码
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>

#define MAXLNE  4096

int main() {
    int listenfd, connfd;
    struct sockaddr_in servaddr{};
    char buffer[MAXLNE]{};
    int ret = 0;

    // 创建套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
        std::cerr << "Failed to create socket." << std::endl;
        return -1;
    }

    // 设置服务器地址
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080); // 指定端口号(这里使用8080)
    servaddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定套接字到指定地址和端口
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "Failed to bind socket." << std::endl;
        return -1;
    }

    // 监听连接请求
    if (listen(listenfd, 5) < 0) { // 允许同时处理最多5个连接请求
        std::cerr << "Error in listening." << std::endl;
        return -1;
    }

    std::cout << "Server started. Listening for incoming connections..." << std::endl;

    fd_set rfds;
    FD_ZERO(&rfds); // 初始化集合
    FD_SET(listenfd, &rfds); // 将监听套接字加入集合中
    int max_fd = listenfd;

    while (1){
        fd_set rset = rfds; //通过这个赋值操作,可以将所有在 allfds 中的文件描述符设置为在 readfds 中处于就绪状态。这样,在调用 select 函数时,只需要监视 readfds 集合,即可判断其中哪些文件描述符已经准备好进行读取操作。
        int fd_num = select(max_fd+1, &rset, NULL, NULL, NULL);
        if (fd_num == -1){
            std::cerr << "Error in select." << std::endl;
            return -1;
        }
    
        if (FD_ISSET(listenfd, &rset)){
            
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // 接受连接请求
            connfd = accept(listenfd, (struct sockaddr *)&client, &len);
            if (connfd < 0) {
                std::cerr << "Error in accepting connection." << std::endl;
                return -1;
            }

            FD_SET(connfd, &rfds);

            if (connfd > max_fd){
                max_fd = connfd; //更新最大文件描述符
            }

            if (--fd_num == 0) { 
                continue; //如果当前计数器为最后一个,则表示所有文件描述符已经处理完毕,可以结束当前的循环,进入下一轮循环。
            } 
        }
    
        int i = 0;
        for (i = listenfd+1;i <= max_fd;i ++)  {
            if (FD_ISSET(i, &rset)) {
                // 读取客户端发来的数据
                bzero(buffer, sizeof(buffer));
                ret = recv(i, buffer, sizeof(buffer), 0);
                if (ret < 0) {
                    std::cerr << "Error in reading data." << std::endl;
                    FD_CLR(i, &rfds); //从集合中删除该文件描述符
                    close(i); //关闭连接套接字
                    continue;
                }
                else if(ret == 0){
                    FD_CLR(i, &rfds);
                    // 关闭连接套接字
                    close(i);
                }
                else{
                    // 输出客户端发送的消息
                    std::cout << "Client message: " << buffer << std::endl;

                    // 发送响应给客户端
                    const char *response = "Hello from server!";
                    if (send(i, response, strlen(response), 0) < 0) {
                        std::cerr << "Error in sending response." << std::endl;
                        FD_CLR(i, &rfds); //从集合中删除该文件描述符
                        close(i); //关闭连接套接字
                        continue;
                    }
                }
                
                if (--fd_num == 0) { 
                    break; //如果当前计数器为最后一个,则表示所有文件描述符已经处理完毕,可以结束for循环。
                } 
            }
        }
    }

    // 关闭服务器套接字
    close(listenfd);

    return 0;
}
复制代码

 

posted on   TempleD  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示