linux网络编程

io多路复用(select,poll,epoll)

select 示例

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <string.h>

constexpr int PORT = 8080;
constexpr int BUFFER_SIZE = 1024;

void setup(int &sockfd) {
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Failed to create socket");
        exit(EXIT_FAILURE);
    }

    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(PORT);

    if (bind(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
        perror("Failed to bind socket");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    if (listen(sockfd, 5) < 0) {
        perror("Failed to listen on socket");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // Set non-blocking mode
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    std::cout << "Server listening on port " << PORT << std::endl;
}

int main() {
    int serverSock, maxfd;
    setup(serverSock);

    fd_set readfds;
    FD_ZERO(&readfds);

    while (true) {
        // Add server socket and standard input to the set
        FD_SET(serverSock, &readfds);
        FD_SET(STDIN_FILENO, &readfds);
        maxfd = std::max(serverSock, STDIN_FILENO);

        // Set timeout for select (2 seconds)
        timeval timeout{};
        timeout.tv_sec = 2;
        timeout.tv_usec = 0;

        int activity = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
        if (activity < 0 && errno != EINTR) {
            perror("select error");
        } else if (activity == 0) {
            std::cout << "Timeout occurred! No activity detected.\n";
            continue;
        }

        // Check if there's activity on the server socket
        if (FD_ISSET(serverSock, &readfds)) {
            sockaddr_in clientAddr{};
            socklen_t addrLen = sizeof(clientAddr);
            int clientSock = accept(serverSock, (struct sockaddr *)&clientAddr, &addrLen);
            if (clientSock >= 0) {
                std::cout << "New connection from " << inet_ntoa(clientAddr.sin_addr) << std::endl;
                close(clientSock);
            }
        }

        // Check if there's input on stdin
        if (FD_ISSET(STDIN_FILENO, &readfds)) {
            char buffer[BUFFER_SIZE];
            ssize_t bytesRead = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
            if (bytesRead > 0) {
                buffer[bytesRead] = '\0';
                std::cout << "Received input: " << buffer << std::endl;
            }
        }
    }

    close(serverSock);
    return 0;
}

  • 优点: 跨平台
  • 缺点:
  1. 监听文件描述符多了之后,慢的一匹,每次都要遍历所有fd
  2. 文件描述符上限1024
  3. 每次调用select,都需要把文件描述符集合从用户态拷贝到内核态

poll 示例

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <vector>

using namespace std;

#define PORT 8888
#define MAX_EVENTS 1024

int main() {
    int server_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};
    vector<pollfd> fds;

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置地址结构
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 初始化pollfd数组
    pollfd fd;
    fd.fd = server_fd;
    fd.events = POLLIN;
    fds.push_back(fd);

    while (true) {
        int num_events = poll(fds.data(), fds.size(), -1); // -1 为无线阻塞,0为不阻塞,其他值表示阻塞的时间
        if (num_events < 0) {
            perror("poll");
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < num_events; ++i) {
            if (fds[i].revents & POLLIN) {
                if (fds[i].fd == server_fd) {
                    // 有新的连接
                    int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
                    if (new_socket < 0) {
                        perror("accept");
                        exit(EXIT_FAILURE);
                    }

                    // 将新连接添加到pollfd数组
                    pollfd new_fd;
                    new_fd.fd = new_socket;
                    new_fd.events = POLLIN;
                    fds.push_back(new_fd);
                } else {
                    // 有数据可读
                    int bytes_read = read(fds[i].fd, buffer, 1024);
                    if (bytes_read <= 0) {
                        // 连接关闭或出错
                        close(fds[i].fd);
                        fds.erase(fds.begin() + i);
                        --i; // 调整索引
                    } else {
                        // 处理接收到的数据
                        cout << "Client: " << buffer << endl;
                        // ...
                    }
                }
            }
        }
    }

    return 0;
}

相比于select,poll扩展了文件描述符的个数,但其还是使用遍历所有描述符的方式检查事件触发

  • 优点
  1. 没有文件描述符个数限制(但是性能瓶颈很明显)
  2. 描述符个数小的场景下,推荐用poll,不用select
  • 缺点
  1. 就是慢,还是遍历的方式,文件描述符多了比select更差
  2. 相比于epoll,没有事件信息

epoll 示例

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <cstring>
#include <vector>

#define PORT 8080
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024

// 创建非阻塞 socket
int create_server_socket() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        std::cerr << "Socket creation failed!" << std::endl;
        return -1;
    }

    // 设置 socket 为非阻塞模式
    int flags = fcntl(server_fd, F_GETFL, 0);
    fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);

    sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        std::cerr << "Binding failed!" << std::endl;
        close(server_fd);
        return -1;
    }

    if (listen(server_fd, 3) < 0) {
        std::cerr << "Listen failed!" << std::endl;
        close(server_fd);
        return -1;
    }

    return server_fd;
}

int main() {
    int server_fd = create_server_socket();
    if (server_fd == -1) {
        return 1;
    }

    // 创建 epoll 实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        std::cerr << "Epoll create failed!" << std::endl;
        close(server_fd);
        return 1;
    }

    // 将 server_fd 加入 epoll 监听
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
        std::cerr << "Epoll_ctl failed!" << std::endl;
        close(server_fd);
        close(epoll_fd);
        return 1;
    }

    std::vector<struct epoll_event> events(MAX_EVENTS);

    while (true) {
        // 等待事件发生
        int num_events = epoll_wait(epoll_fd, events.data(), MAX_EVENTS, -1);  // 阻塞直到有事件
        if (num_events == -1) {
            std::cerr << "Epoll wait failed!" << std::endl;
            break;
        }

        // 处理所有发生的事件
        for (int i = 0; i < num_events; ++i) {
            if (events[i].data.fd == server_fd) {
                // 新的客户端连接
                sockaddr_in client_addr;
                socklen_t client_len = sizeof(client_addr);
                int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
                if (client_fd == -1) {
                    std::cerr << "Accept failed!" << std::endl;
                } else {
                    std::cout << "New client connected!" << std::endl;

                    // 设置新连接为非阻塞
                    int flags = fcntl(client_fd, F_GETFL, 0);
                    fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);

                    // 将 client_fd 添加到 epoll 中
                    event.events = EPOLLIN | EPOLLET;  // 使用边缘触发
                    event.data.fd = client_fd;
                    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
                        std::cerr << "Epoll_ctl failed!" << std::endl;
                        close(client_fd);
                    }
                }
            } else if (events[i].events & EPOLLIN) {
                // 客户端发送数据
                int client_fd = events[i].data.fd;
                char buffer[BUFFER_SIZE];
                ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer));
                if (bytes_read > 0) {
                    std::cout << "Received data: " << std::string(buffer, bytes_read) << std::endl;
                    // 向客户端发送数据
                    write(client_fd, "Hello, client!", 14);
                } else if (bytes_read == 0) {
                    // 客户端关闭连接
                    std::cout << "Client disconnected!" << std::endl;
                    // 从 epoll 中删除 client_fd
                    if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, nullptr) == -1) {
                        std::cerr << "Failed to remove client_fd from epoll!" << std::endl;
                    }
                    close(client_fd);
                } else {
                    std::cerr << "Read error!" << std::endl;
                    close(client_fd);
                }
            }
        }
    }

    // 清理资源
    close(server_fd);
    close(epoll_fd);
    return 0;
}

  • 缺点: 个人认为, epoll在linux下, 没有缺点, 不在linux 那也没有epoll 了, 剩下的无非就是资源啥乱七八糟的

  • epoll 的性能优势来源:

  1. 事件的通知机制:

    • epoll 则采用了更高效的事件通知机制。当一个事件发生时,内核只会通知应用程序,而无需每次遍历整个文件描述符集合。epoll 通过一个基于事件回调的机制,只有在有事件发生时,才将相关的文件描述符通知给应用程序。这样,应用程序无需每次都遍历整个文件描述符集合,而只需要关心那些真正有事件的文件描述符
    • poll/select 通过传递一个文件描述符集合(fd_set)来通知哪些文件描述符发生了事件。在每次调用时,内核都必须遍历这个文件描述符集合,并检查每个描述符的状态,哪怕没有任何事件发生,这个遍历的过程是线性的,时间复杂度是 O(n),其中 n 是文件描述符的数量
  2. 文件描述符管理

    • epoll 用了基于事件的通知机制,称为 水平触发(Level Triggered, LT边缘触发(Edge Triggered, ET)。通过 epoll_ctl 将文件描述符添加到 epoll 的监听队列中,epoll 会记录文件描述符的状态,且只有在文件描述符的状态变化时,内核才会通知应用程序,避免了不必要的遍历和重复通知
    • select/poll 如果你要监听多个文件描述符,文件描述符数量上限通常是固定的(如 FD_SETSIZE),并且在添加、删除文件描述符时,每次都需要重新传递整个文件描述符集合给内核,因此,在动态增加或删除文件描述符时,会有大量的用户态到内核态的拷贝
  3. 事件通知的优化

    • epoll 提供水平触发(Level Triggered, LT) 和 边缘触发(Edge Triggered, ET)两种模式
    • select/poll 仅支持 水平触发

Signal-Driven I/O Model(信号驱动)

信号驱动I/O是通过内核发送信号(如SIGIO)来通知进程某个I/O操作已准备好,而不需要进程主动去检查文件描述符的状态。该模型通常用于异步通知I/O事件的发生,避免了轮询操作,从而减少了CPU的浪费

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <errno.h>

#define PORT 8080

int sockfd;

// 信号处理函数
void sigio_handler(int signo) {
    char buffer[1024];
    int n;

    // 读取套接字数据
    n = read(sockfd, buffer, sizeof(buffer) - 1);
    if (n < 0) {
        if (errno == EAGAIN) {
            printf("No data available\n");
        } else {
            perror("Read error");
        }
        return;
    }
    buffer[n] = '\0';  // Null terminate the string
    printf("Received data: %s\n", buffer);
}

int main() {
    struct sockaddr_in server_addr;
    struct sigaction sa;

    // 创建一个套接字
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("Socket failed");
        exit(EXIT_FAILURE);
    }

    // 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    // 绑定套接字到端口
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 设置套接字为非阻塞模式
    if (fcntl(sockfd, F_SETFL, O_NONBLOCK) < 0) {
        perror("fcntl failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 设置套接字为异步I/O模式,信号SIGIO将在I/O事件发生时发送
    if (fcntl(sockfd, F_SETFL, FASYNC) < 0) {
        perror("fcntl failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 注册SIGIO信号处理函数
    sa.sa_handler = sigio_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGIO, &sa, NULL) < 0) {
        perror("sigaction failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 启动监听
    if (listen(sockfd, 5) < 0) {
        perror("Listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d...\n", PORT);

    // 服务器主循环,等待SIGIO信号
    while (1) {
        pause();  // 等待信号到达
    }

    close(sockfd);
    return 0;
}

  1. 代码中, 定义了 sigio_handler 函数,用于处理 SIGIO 信号,当I/O操作准备好时,内核会发送此信号,并触发该函数,读取数据并进行处理

  2. fcntl(sockfd, F_SETFL, O_NONBLOCK):设置套接字为非阻塞模式
    fcntl(sockfd, F_SETFL, FASYNC):使套接字支持异步I/O,进程可以通过信号接收I/O事件通知

  3. 通过sigaction注册处理SIGIO信号的回调函数。每当I/O事件发生时,内核会发送SIGIO信号,进程就会执行该信号处理函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值