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 描述不是打开的文件
相关推荐
二等饼干~za8986681 小时前
geo优化源码开发搭建技术分享
大数据·网络·数据库·人工智能·音视频
Hommy881 小时前
【剪映小助手】贴纸处理接口
网络·开源·github·aigc·剪映小助手·视频剪辑自动化
志栋智能2 小时前
超越监控:超自动化巡检提供的主动价值
运维·网络·人工智能·自动化
MAXrxc2 小时前
OSPF综合实验
网络
AIMath~3 小时前
向github中上传文件过大超过50M怎么办
网络·git·github
Sagittarius_A*3 小时前
H3CSE 高性能园区网:SNMP 网络管理协议详解
网络·计算机网络·安全·h3cse
杨充3 小时前
1.1 数据编码设计原理
linux·运维·网络·底层原理·数据编码
缪懿4 小时前
网络层和数据链路层中的常见协议解析
网络·网络协议·java-ee
CoreTK_EMC4 小时前
牙科医疗器械 ESD 静电整改案例|芯通康医疗级方案,护航诊疗安全与合规
网络·学习·emc整改·芯通康