IO多路复用和并发服务器

IO多路复用

IO多路复用是一种单线程或者单进程管理多个文件描述符的技术,核心就是通过系统调用监视多个IO操作的状态,当某个IO操作就绪(可读、可写或发生异常)时,通知应用程序处理。

为什么要用多路复用?

传统的单线程阻塞模型,当调用recv后,如果没数据,程序就卡死不动了,只能处理一个链接;多线程/多进程模型是来一个连接开一个线程,一旦线程太多、CPU爆炸、内存爆炸、效率极低,;而IO多路复用使用非阻塞IO写作,用户态和内核态交互极少,不用创建大量线程,不用轮询所有连接,只有真正有事件的才处理,可以高效管理大量连接,提高并发性能。

IO多路复用流程

  1. 应用程序创建一堆socket以及事件交给内核
  2. 线程阻塞等待事件(不占CPU)
  3. 内核监控所有socket,一旦有某个socket有数据/可写,内核调用select修改fd_set通知程序可以开始处理这个了
  4. 程序遍历就绪socket处理读/写/异常

流程图如下:

核心函数:

1.select函数

函数原型:

复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

参数:

  • nfds:要监视的最大的文件描述符+1
  • readfds:要监视的读文件描述符集合,如果不关心,可以传NULL
  • writefds:要监视的写文件描述符集合,如果不关心,可以传NULL
  • exceptfds:要监视的异常文件描述符集合,如果不关心,可以传NULL
  • timeout:超时时间如果是NULL表示永久阻塞

返回值:

  • 成功:返回就绪的文件描述符个数
  • 失败:-1(重置错误码)

2.poll函数

函数原型:

复制代码
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数:

  • fds:一个指向pollfd结构体数组的指针,它描述了要监视的文件描述符及其事件
  • nfdsfds:数组中有效描述符元素的数量
  • timeout:是等待时间,单位为毫秒

返回值:

  • 成功:返回结构体中revents域不为0的文件描述符个数,如果在超时前没有任何事件发生,poll()返回0
  • 失败:-1(重置错误码)

并发服务器

服务器模型

服务器模型有两种:循环服务器和并发服务器

  • 循环服务器:同一时刻只能响应一个客户端请求
  • 并发服务器:同一时刻可以响应多个客户端的请求

TCP服务器默认是一个循环服务器,因为有两个阻塞的函数accept和recv之间互相影响;

UDP服务器默认是一个并发服务器,因为只有一个阻塞函数recvfrom。

多线程并发服务器

实现原理:主线程负责新连接(accept);每个子线程处理一个连接的读写(recv/sned)。

主线程流程如图所示:

子线程流程如图所示:

示例代码如下:

复制代码
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8888
#define SERVER_IP "192.168.179.100"
#define BUFSIZE 128

//自定义结构体,封装客户端连接信息
//用于在线程之间传递
typedef struct struct_MSG {
    int accpetfd;//客户端套接字描述符
    struct sockaddr_in clientaddr;//客户端地址信息
}msg_t;

//创建线程处理函数
void *deal_read_write(void *arg)
{
    msg_t msg = *(msg_t*)arg;
    char buf[BUFSIZE] = {0};
    int nbytes = 0;
    printf("客户端[%s:%d]连接到了服务器\n",inet_ntoa(msg.clientaddr.sin_addr),ntohs(msg.clientaddr.sin_port));

    //客户端数据接收与发送
    while(1) {
        nbytes = recv(msg.accpetfd,buf,BUFSIZE,0);
        if(nbytes == -1) {
            perror("读取客户端信息失败");
        } else if(nbytes == 0) {
            printf("客户端[%s:%d]断开连接\n",inet_ntoa(msg.clientaddr.sin_addr),ntohs(msg.clientaddr.sin_port));
            break;
        }
        printf("客户端[%s:%d]发来数据:%s\n",inet_ntoa(msg.clientaddr.sin_addr),ntohs(msg.clientaddr.sin_port),buf);
        
        //回显数据给客户端
        strcat(buf,"...服务器");
        if(send(msg.accpetfd,buf,BUFSIZE,0) == -1) {
            perror("回显数据失败");
            break;
        }
    }
    close(msg.accpetfd);
    pthread_exit(NULL);
}

int main(int argc,const char *argv[])
{
    //创建套接字,配置服务器地址结构体
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1) {
        perror("创建套接字失败");
        return -1;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    socklen_t serveraddr_len = sizeof(server_addr);

    //绑定,监听,处理客户端的连接
    if(bind(sockfd,(struct sockaddr*)&server_addr,serveraddr_len) == -1) {
        perror("绑定失败");
        return -1;
    }
    if(listen(sockfd,5) == -1) {
        perror("监听失败");
        return -1;
    }
    struct sockaddr_in client_addr;
    socklen_t clientaddr_len = sizeof(client_addr);
    int accpetfd = 0;
    pthread_t pthread_id = 0;
    msg_t msg;
    int ret = 0;

    //主线程接受连接并创建子线程
    while(1) {
        if((accpetfd = accept(sockfd,(struct sockaddr*)&client_addr,&clientaddr_len)) == -1) {
            perror("接受连接失败");
            return -1;
        }
        msg.accpetfd = accpetfd;
        msg.clientaddr = client_addr;

        //创建子线程
        ret = pthread_create(&pthread_id,NULL,deal_read_write,&msg);
        if(ret != 0) {
            fprintf(stderr,"创建子线程失败,错误信息:%s\n",strerror(ret));
            close(sockfd);
            return -1;
        }

        ret = pthread_detach(pthread_id);
        if(ret != 0) {
            fprintf(stderr,"设置线程分离态失败,错误信息:%s\n",strerror(ret));
            close(sockfd);
            return -1;
        }
    }
    close(sockfd);
    return 0;
}

多进程并发服务器

实现原理:fork模型:主进程监听链接,子进程处理请求,父子进程完全独立,崩溃互不影响;预fork优化:启动时预先创建多个子进程(类似Apache),通过共享监听socket(SO_REUSEPORT)实现负载均衡。

主进程流程图如下:

子进程流程图如下:

示例代码如下:

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

#define PORT 8888
#define SERVER_IP "192.168.179.100"
#define BUF_SIZE 128

//信号处理函数
void sig_func(int signum)
{
    wait(NULL); //处理SIGUSR1信号,并回收子进程资源避免僵尸进程

}

int main(int argc,const char *argv[])
{
    //创建套接字与服务器配置
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1) {
        perror("套接字创建失败");
        return -1;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    socklen_t server_len = sizeof(server_addr);

    //绑定,监听,信号注册
    if(bind(sockfd,(struct sockaddr*)&server_addr,server_len) == -1) {
        perror("绑定失败");
        close(sockfd);
        return -1;
    }

    if(listen(sockfd,5) == -1) {
        perror("监听失败");
        close(sockfd);
        return -1;
    }

    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int acceptfd = 0;
    pid_t pid = 0;
    int ret = 0;

    signal(SIGUSR1,sig_func);

    //主循环:接受连接创建子进程
    while (1) {
        acceptfd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
        if(acceptfd == -1) {
            perror("接受连接失败");
            close(sockfd);
        }
        pid = fork();
        if(pid == -1) {
            perror("创建子进程失败");
            close(acceptfd);
            break;
        } else if(pid == 0) {
            //子进程:处理客户端通信
            char buf[BUF_SIZE] = {0};
            int nbytes = 0;
            printf("客户端[%s:%d]连接到服务器\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));

            //接收数据
            nbytes = recv(acceptfd,buf,BUF_SIZE,0);
            if(nbytes == -1) {
                perror("接受失败");
            } else if(nbytes == 0) {
                printf("客户端[%s:%d]断开连接\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
                break;
            }
            printf("客户端[%s:%d]发来数据:%s\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),buf);
            
            strcat(buf,"--来自服务端");
            //回显
            if(send(acceptfd,buf,BUF_SIZE,0) == -1) {
                perror("回显失败");
            }
            close(acceptfd);
            kill(getpid(),SIGUSR1);
            exit(0);
        } else if(pid > 0) {
            //父进程
            close(acceptfd);
        }
    }
    close(sockfd);
    return 0;
}

IO多路复用并发服务器

使用select机制实现并发服务器

流程图如下:

示例代码如下:

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/time.h>

#define PORT 8888              // 服务器监听端口
#define SERVER_IP "192.168.179.100"  // 服务器IP地址
#define BUF_SIZE 128            // 数据缓冲区大小

/**
 * @brief select IO多路复用并发服务器主函数
 * 
 * 该服务器使用select机制实现并发,能够同时处理多个客户端连接。
 * 主要流程:
 * 1. 创建监听套接字
 * 2. 设置服务器地址并绑定
 * 3. 开始监听
 * 4. 初始化select文件描述符集合
 * 5. 进入主循环,调用select监控文件描述符事件
 * 6. 处理新连接和客户端数据
 * 
 * @param argc 参数个数
 * @param argv 参数数组
 * @return 成功返回0,失败返回-1
 */
int main(int argc,const char *argv[])
{
    //创建监听套接字
    int listenfd = socket(AF_INET,SOCK_STREAM,0);
    if(listenfd == -1) {
        perror("创建监听套接字失败");
        return -1;
    }

    //服务器地址配置
    struct sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    socklen_t server_len = sizeof(server_addr);

    //绑定
    int ret = bind(listenfd,(struct sockaddr*)&server_addr,server_len);
    if(ret == -1) {
        perror("绑定失败");
        close(listenfd);
        return -1;
    }

    //监听
    if(listen(listenfd,10) == -1) {
        perror("监听失败");
        close(listenfd);
        return -1;
    }

    // ========== select机制初始化 ==========
    // fd_set是select使用的文件描述符集合类型
    // readfd_save_set: 保存所有需要监控的文件描述符(作为备份)
    // readfd_modify_set: 每次select调用的临时集合(select会修改此集合,只保留就绪的fd)
    fd_set readfd_save_set, readfd_modify_set;
    FD_ZERO(&readfd_save_set);              // 初始化集合,清空所有位
    FD_SET(listenfd, &readfd_save_set);     // 将监听套接字加入集合,监控新连接请求

    int max_fd = listenfd;                  // 当前最大文件描述符,用于select第一个参数
    char buf[BUF_SIZE] = {0};               // 数据接收缓冲区

    printf("开启小型的select并发服务器\n");

    // ========== 主循环:持续监控和处理事件 ==========
    while(1) {
        // 每次调用select前,必须重新设置readfd_modify_set
        // 因为select返回时会修改该集合,只保留就绪的文件描述符
        readfd_modify_set = readfd_save_set;

        // 调用select监控文件描述符
        // 参数:max_fd+1(监控范围), 读集合, 写集合(空), 异常集合(空), 超时时间(空=阻塞)
        int fds = select(max_fd + 1, &readfd_modify_set, NULL, NULL, NULL);
        if(fds == -1) {
            perror("select失败");
            continue;
        }
        // ========== 遍历所有可能的文件描述符,处理就绪事件 ==========
        int event_fd = 3;                    // 从3开始,0/1/2是标准输入/输出/错误
        struct sockaddr_in client_addr;      // 客户端地址结构
        socklen_t client_len = sizeof(client_addr);

        // 遍历[3, max_fd]范围内的所有文件描述符
        for(event_fd = 3; event_fd < max_fd + 1; event_fd++) {
            // 检查当前文件描述符是否在就绪集合中
            if(FD_ISSET(event_fd, &readfd_modify_set)) {
                // ====== 情况1:监听套接字就绪 -> 有新客户端连接 ======
                if(event_fd == listenfd) {
                    // 接受新连接
                    int acceptfd = accept(event_fd, (struct sockaddr*)&client_addr, &client_len);
                    if(acceptfd == -1) {
                        perror("accept失败");
                        continue;
                    }
                    // 将新连接的套接字加入监控集合
                    FD_SET(acceptfd, &readfd_save_set);
                    // 更新最大文件描述符
                    max_fd = max_fd > acceptfd ? max_fd : acceptfd;
                    printf("客户端[%s:%d]连接服务器\n", 
                           inet_ntoa(client_addr.sin_addr), 
                           ntohs(client_addr.sin_port));
                }
                // ====== 情况2:客户端套接字就绪 -> 有数据可读 ======
                else {
                    // 处理客户端数据读写
                    memset(buf, 0, sizeof(buf));  // 清空缓冲区
                    // 接收客户端数据
                    int nbytes = recv(event_fd, buf, BUF_SIZE, 0);
                    if(nbytes == -1) {
                        perror("获取客户端信息失败");
                        continue;
                    }
                    // nbytes == 0 表示客户端主动断开连接
                    else if(nbytes == 0) {
                        printf("客户端[%s:%d]断开连接\n", 
                               inet_ntoa(client_addr.sin_addr), 
                               ntohs(client_addr.sin_port));
                        // 从监控集合中移除该文件描述符
                        FD_CLR(event_fd, &readfd_save_set);
                        // 关闭套接字
                        close(event_fd);
                        continue;
                    }
                    // 打印收到的数据
                    printf("客户端[%s:%d]发来的数据:%s\n", 
                           inet_ntoa(client_addr.sin_addr), 
                           ntohs(client_addr.sin_port), buf);

                    // ====== 回显数据给客户端 ======
                    nbytes = send(event_fd, buf, BUF_SIZE, 0);
                    if(nbytes == -1) {
                        perror("回显失败");
                        continue;
                    }
                }
            }
        }
    }
    return 0;
}

pollfd结构体:

复制代码
struct pollfd{
    int fd;//文件描述符
    short events;//等待的事件
    short revents;//实际发生的事件
};
事件 常值 说明
读事件 POLLIN 普通或优先带数据可读
读事件 POLLPRI 高优先级数据可读
写事件 POLLOUT 普通或优先带数据可写
写事件 POLLWRNORM 普通数据可写
错误事件 POLLERR 发生错误
错误事件 POLLHUP 发生挂起
错误事件 POLLNVAL 描述不是打开的文件
相关推荐
Lust Dusk1 小时前
记一次Linux应急响应题目解析
linux·运维·服务器·网络·安全·网络安全
凌肖战2 小时前
TCP网络通信中setsockopt()函数解决地址已占用问题
服务器·网络·tcp/ip
rcms152702692183 小时前
MKS PDR-C-2C-BCD 电源模块
网络
LATASA3 小时前
【 从0到1构建 Agent Harness学习笔记】
网络·笔记·学习
数据知道3 小时前
SQL 注入从入门到精通:手工注入 + sqlmap 自动化
网络·数据库·sql·网络安全·自动化
GIR1235 小时前
研究更新:2026年网络协议分析工具发展前景与竞争格局报告(附市场占有率与产业链图谱)
网络·网络协议
数据知道5 小时前
OWASP Top 10 实战通关指南:总览篇
网络·安全·网络安全
Yang96115 小时前
从GN-W10A看多业务融合测试:如何统一以太网与SDH的验证标准
网络
砚凝霜6 小时前
软考网络工程师|第 5 章 广域网基础技术完整备考笔记
网络·笔记
国科安芯6 小时前
卫星星务计算机数据管理中SEU容错机制的技术实现研究——以AS32S601存储器ECC架构为例
网络·单片机·算法·fpga开发·架构·云计算·risc-v