当前位置: 首页 > article >正文

不同IO模型服务器的简单实现

1. 阻塞 I/O(Blocking I/O)

在阻塞 I/O 模型下,进程在执行 I/O 操作时会被阻塞,直到数据被成功读取或写入。这个模型是最基础的模型,适用于连接较少的应用场景。每个连接都会阻塞主线程,直到完成相应的 I/O 操作。

代码示例:阻塞 I/O 服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 2048          // 服务器端口
#define BUFFER_SIZE 128    // 缓冲区大小

int main() {
    int sockfd, clientfd;                // 服务端套接字和客户端套接字
    struct sockaddr_in serveraddr, clientaddr;   // 服务器和客户端地址结构体
    socklen_t len = sizeof(clientaddr);   // 用于接收客户端地址长度的变量
    char buffer[BUFFER_SIZE];             // 数据缓冲区

    // 创建服务端套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);   // 使用 IPv4 地址族,TCP 流套接字
    if (sockfd == -1) {                       // 错误检查
        perror("socket");                     // 打印错误信息
        exit(EXIT_FAILURE);                   // 退出程序
    }

    memset(&serveraddr, 0, sizeof(serveraddr)); // 初始化服务器地址
    serveraddr.sin_family = AF_INET;           // 设置协议族为 IPv4
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 绑定所有网络接口
    serveraddr.sin_port = htons(PORT);        // 绑定端口

    // 将套接字绑定到指定地址和端口
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
        perror("bind"); // 打印错误信息
        exit(EXIT_FAILURE); // 退出程序
    }

    // 开始监听客户端连接
    if (listen(sockfd, 10) == -1) {
        perror("listen"); // 打印错误信息
        exit(EXIT_FAILURE); // 退出程序
    }

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

    // 主循环,等待客户端连接
    while (1) {
        // 阻塞接收客户端连接请求
        clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        if (clientfd == -1) {
            perror("accept"); // 错误处理
            continue; // 继续监听其他客户端连接
        }

        // 阻塞接收客户端的数据
        int count = recv(clientfd, buffer, BUFFER_SIZE, 0);
        if (count == -1) {
            perror("recv"); // 错误处理
            close(clientfd); // 关闭当前客户端连接
            continue; // 继续监听
        }
        buffer[count] = '\0'; // 确保接收到的数据是以 '\0' 结尾的字符串

        // 打印接收到的数据
        printf("Received: %s\n", buffer);

        // 阻塞发送数据回客户端
        send(clientfd, buffer, count, 0);

        close(clientfd); // 关闭客户端连接
    }

    close(sockfd); // 关闭服务器套接字
    return 0;
}

解释

  • socket(): 创建一个 TCP 套接字。
  • bind(): 将套接字绑定到指定的端口和地址上。
  • listen(): 启动监听,等待客户端连接。
  • accept(): 阻塞地接受客户端的连接请求。如果没有连接,程序将一直阻塞。
  • recv(): 阻塞地读取客户端数据。如果客户端没有发送数据,recv() 将一直阻塞。
  • send(): 阻塞地将数据发送回客户端。

2. 非阻塞 I/O(Non-blocking I/O)

在非阻塞 I/O 模型下,I/O 操作不会阻塞进程。如果数据不可用,调用会立即返回,而不会让进程等待。通常会配合 errno 来检查是否需要重试。

代码示例:非阻塞 I/O 服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>

#define PORT 2048
#define BUFFER_SIZE 128

int main() {
    int sockfd, clientfd;
    struct sockaddr_in serveraddr, clientaddr;
    socklen_t len = sizeof(clientaddr);
    char buffer[BUFFER_SIZE];

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

    // 设置套接字为非阻塞
    fcntl(sockfd, F_SETFL, O_NONBLOCK);

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

    // 绑定
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 开始监听
    if (listen(sockfd, 10) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

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

    while (1) {
        clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        if (clientfd == -1) {
            // 如果客户端连接不可用,则跳过并继续监听
            if (errno != EWOULDBLOCK) {
                perror("accept");
            }
            continue;
        }

        // 非阻塞读取数据
        int count = recv(clientfd, buffer, BUFFER_SIZE, 0);
        if (count > 0) {
            buffer[count] = '\0';
            printf("Received: %s\n", buffer);
            // 非阻塞发送数据回客户端
            send(clientfd, buffer, count, 0);
        }

        close(clientfd);  // 关闭客户端连接
    }

    close(sockfd);  // 关闭服务器套接字
    return 0;
}

解释

  • 使用 fcntl() 函数将套接字设置为非阻塞模式。这样 recv()accept() 都不会阻塞进程。如果没有数据或没有连接可用,函数将立即返回并设置 errnoEAGAINEWOULDBLOCK
  • 适用于需要处理大量连接的场景,但需要增加对 errno 错误的处理和重试机制。

3. 多路复用 I/O(I/O Multiplexing)

多路复用 I/O 允许服务器监控多个文件描述符(如多个客户端连接),并在某个文件描述符准备好进行 I/O 操作时进行处理。常用的方法是 select()poll()epoll()

代码示例:多路复用 I/O 服务器(使用 select()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <arpa/inet.h>

#define PORT 2048
#define BUFFER_SIZE 128
#define MAX_CLIENTS 10

int main() {
    int sockfd, clientfd, maxfd;
    struct sockaddr_in serveraddr, clientaddr;
    socklen_t addrlen = sizeof(clientaddr);
    char buffer[BUFFER_SIZE];

    fd_set read_fds, temp_fds;

    // 创建服务器套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

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

    // 绑定
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 开始监听
    if (listen(sockfd, MAX_CLIENTS) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 初始化读文件描述符集合
    FD_ZERO(&read_fds);
    FD_SET(sockfd, &read_fds);
    maxfd = sockfd;

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

    while (1) {
        // 复制文件描述符集合,用于 select()
        temp_fds = read_fds;

        // 调用 select 等待多个文件描述符的状态变化
        int activity = select(maxfd + 1, &temp_fds, NULL, NULL, NULL);
        if (activity == -1) {
            perror("select");
            exit(EXIT_FAILURE);
        }

        // 如果有新的连接请求,accept 连接
        if (FD_ISSET(sockfd, &temp_fds)) {
            clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &addrlen);
            if (clientfd == -1) {
                perror("accept");
                continue;
            }

            FD_SET(clientfd, &read_fds);  // 添加新客户端到监视列表
            if (clientfd > maxfd) {
                maxfd = clientfd;  // 更新最大文件描述符
            }

            printf("New connection from %s\n", inet_ntoa(clientaddr.sin_addr));
        }

        // 检查每个已连接的客户端
        for (int i = 0; i <= maxfd; i++) {
            if (FD_ISSET(i, &temp_fds)) {
                int count = recv(i, buffer, BUFFER_SIZE, 0);
                if (count == 0) {
                    // 客户端关闭连接
                    close(i);
                    FD_CLR(i, &read_fds);
                } else if (count > 0) {
                    buffer[count] = '\0';
                    printf("Received: %s\n", buffer);
                    send(i, buffer, count, 0);  // 发送回客户端
                }
            }
        }
    }

    close(sockfd);  // 关闭服务器套接字
    return 0;
}

解释

  • 使用 select() 监控多个客户端连接。FD_SET() 将套接字添加到文件描述符集合中,FD_ISSET() 检查文件描述符是否就绪。
  • 适用于处理中等规模的并发连接,使用 select() 可同时监控多个连接。

4. 信号驱动 I/O(Signal-driven I/O)

信号驱动 I/O 在套接字准备好进行 I/O 操作时,会向进程发送一个信号,通知进程去处理该 I/O 操作。进程会注册一个信号处理函数来响应。

5. 异步 I/O(Asynchronous I/O)

异步 I/O 是最先进的 I/O 模型,进程发出 I/O 请求后,不需要阻塞,操作系统在操作完成时通知进程。这通常通过回调函数来处理。

由于信号驱动 I/O 和异步 I/O 在实现上更复杂且需要额外的支持(例如 aio 库或操作系统提供的特性),因此在这里只提供了阻塞、非阻塞和多路复用 I/O 模型的详细实现和解释。


http://www.kler.cn/a/515490.html

相关文章:

  • Android OpenGL(六) 纹理
  • SQL-leetcode—1164. 指定日期的产品价格
  • 微前端qiankun的基本使用(vue-element-admin作为项目模版)
  • 【AI编辑器】字节跳动推出AI IDE——Trae,专为中文开发者深度定制
  • pikachu靶场-敏感信息泄露概述
  • 变频器硬件接线
  • 【R语言】数学运算
  • 迷你世界玩家准备界面UI设计制作触发器
  • QT+VS2022 应用程序无法启动0x000007b问题记录
  • Linux环境部署——MySQL忘记密码
  • 【Java】Java抛异常到用户界面公共封装
  • 分享一款WebSocket在线测试工具,使用简单方便
  • 《探秘:人工智能如何为鸿蒙Next元宇宙网络传输与延迟问题破局》
  • springBoot tomcat
  • 【玩转全栈】----用户管理案例
  • 信号失真度测试仪、音频失真度测试仪、失真度仪、全自动数字失真度测量仪
  • 没有公网IP实现seafile本地IP访问和虚拟局域网IP同时访问和上传文件
  • 3D Vision--计算点到平面的距离
  • ComfyUI实现老照片修复——AI修复老照片(ComfyUI-ReActor / ReSwapper)尚待完善
  • vue post删除 兼容批量删除和单个删除
  • 实现 iOS 自定义高斯模糊文字效果的 UILabel(文末有Demo)
  • 设计模式的艺术-开闭原则
  • MySQL-日志与主从复制(包含如何中途加入从节点)
  • Java 和 JWT(JSON Web Tokens)实现 token 鉴权
  • 【面试常见问题】
  • vue3+elementPlus之后台管理系统(从0到1)(day3-管理员管理)