IO多路复用

本文详细讲解了同步IO与异步IO的区别,介绍了阻塞、非阻塞和IO多路复用模型,重点剖析了SELECT、POLL和EPOLL三种IO多路复用方法的原理、函数解析以及实战实例。通过比较,揭示它们的优缺点和适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.同步IO和异步IO

1.同步IO

同步IO指的是用户进程触发I/O操作并等待或者轮询的去查看I/O操作是否就绪。同步IO的执行者是IO操作的发起者。同步IO需要发起者进行内核态到用户态的数据拷贝过程,所以这里必须阻塞。

2.异步IO

异步IO是指用户进程触发I/O操作以后就立即返回,继续开始做自己的事情,而当I/O操作已经完成的时候会得到I/O完成的通知。异步IO的执行者不是IO操作的发起者这就是同步IO与异步IO的根本区别。异步IO的执行者是内核线程,内核线程将数据从内核态拷贝到用户态,所以这里没有阻塞。

举个通俗点的例子:

同步IO:你是商店老板,你去工厂进货,到了工厂发现此时工厂货物不足,于是你在工厂等候,并不时地询问工厂老板货物是否准备好了,直至货物齐全,你带着货物回到商店。这就是同步IO,在你等待的时候,进程就是阻塞状态。

异步IO:你还是商店老板,你去工厂进货,到了工厂发现此时工厂货物不足,但这次热心的商店老板承诺等货物齐全将货物给你送过去,于是你回到了商店,继续做你自己的其它事情,直至收到工厂老板货物送达的通知。这就是异步IO,你在得知货物不足后回到了商店做其它事,所以在这个模型中进程是没有阻塞的。

2.五种IO模型

  1. 阻塞IO(blocking IO)
  2. 非阻塞IO(nonblocking IO)
  3. IO多路复用(IO multiplexing)
  4. 信号驱动IO(signal driven IO)不常用
  5. 异步IO (asynchronous IO)

1.阻塞IO

一个典型的读操作流程(来自《UNIX网络编程》):

在这里插入图片描述
在这个流程中,如果数据没准备好则需要一直等待数据的准备,所以是阻塞的,同时也是同步IO。

2.非阻塞IO

典型的非阻塞IO模型一般如下:

在这里插入图片描述
非阻塞IO在发现数据没有准备好后会返回用户进程空间,继续做它自己的事,并不会像阻塞IO一样等待数据。但是在非阻塞IO中用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。

3.IO多路复用

关于IO多路复用的理解让我们回到商店老板的例子,商店老板又去进货,但这次不是去一家工厂进货,而是去很多家,但所有工厂都是存货不足(该死的工厂老板),于是商店老板回到了商店等待通知。这次监视工厂货物准备与通知商店老板的是select/poll与epoll。(IO多路复用可以是阻塞IO也可以是非阻塞IO,但一般情况下是非阻塞IO。还有一点值得注意的是,poll与select只会返回有货物的通知,但并不会返回具体是哪一家工厂,需要用户进程自己遍历一遍找出。

在这里插入图片描述

3.SELECT

1.函数解析

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
/* nfds :   最大的文件描述符加1。
readfds: 用于检查可读的。
writefds:用于检查可写性。
exceptfds:用于检查异常的数据。
timeout:一个指向timeval结构的指针,用于决定select等待I/o的最长时间。如果为空将一直等待。
timeval结构的定义:

struct timeval{
long tv_sec; // seconds
long tv_usec; // microseconds
}

返回值:  >0  是已就绪的文件句柄的总数, =0 超时, <0 表示出错,错误: errno  */

2.实例

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>

int main(void){

    int server_socked,client_socked;
    int server_len,client_len;

    struct sockaddr_in server_address;
    struct sockaddr_in client_address;

    int result;
    fd_set readfds,testfds;              //保存文件描述符的集合

    server_socked=socket(AF_INET,SOCK_STREAM,0);
    server_address.sin_family=AF_INET;
    server_address.sin_addr.s_addr = inet_addr("192.168.241.129");
    server_address.sin_port = htons(666);
    server_len=sizeof(server_address);

    bind(server_socked,(struct sockaddr*)&server_address,server_len);
    listen(server_socked,5);
    FD_ZERO(&readfds);
    FD_SET(server_socked,&readfds);

    while(1){

         testfds = readfds;
         char str=0;

         printf("正在等待连接!\n");
         result = select(FD_SETSIZE, &testfds, (fd_set*)0, (fd_set*)0, (struct timeval*)0);                       //调用select监视testfds中的套接字,一旦可读就返回

         int count;
         for(count=0;count<FD_SETSIZE;count++){

             if(FD_ISSET(count, &testfds)){       //FD_ISSET()判断count是否是testfds中的

                if(count==server_socked){                        //处理客户端对服务器端发起的连接

                   client_len = sizeof(client_address);
                   client_socked = accept(server_socked,(struct sockaddr*)&client_address, &client_len);                //服务器端套接字可读,说明客户端有连接请求,调用accept接收客户端套接字
                   FD_SET(client_socked, &readfds);                   //将客户端套接字加入到select监视集合中
                   printf("增加了一个新的连接: %d\n", client_socked);
                }
                else{                                           //处理客户端发送的数据

                    int tmp=read(count,&str,1);                            //读取客户端发送的数据

                    if(tmp==0){

                    }
                    else{
                        printf("读到的字符是:%c",str);
                    }

                }

             }
         }

    }

    return 0;
}

在服务器端运行以上代码后,打开cmd,运行 telnet 192.168.241.129 666 即可连接上服务器端。如下图:

在这里插入图片描述
然后回车运行,输入单个字符,就会在服务器端显示出来。

4.POLL

1.函数解析:

 int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  /*    输入参数:
            fds://可以传递多个结构体,也就是说可以监测多个驱动设备所产生的事件,只要有一个产生了请求事件,就能立即返回
            struct pollfd {
                  int fd;                //文件描述符   open打开的那个
                  short events;     //请求的事件类型,监视驱动文件的事件掩码*/  POLLIN | POLLOUT
                  short revents;    //驱动文件实际返回的事件
            }
            nfds:  //监测驱动文件的个数
            timeout://超时时间,单位是ms
事件类型events 可以为下列值:
        POLLIN           有数据可读
        POLLRDNORM 有普通数据可读,等效与POLLIN
        POLLPRI         有紧迫数据可读
        POLLOUT        写数据不会导致阻塞
        POLLER          指定的文件描述符发生错误
        POLLHUP        指定的文件描述符挂起事件
        POLLNVAL      无效的请求,打不开指定的文件描述符
返回值:
        有事件发生  返回revents域不为0的文件描述符个数
        超时:return 0
        失败:return  -1   错误:errno    */

2.实例

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>

#define MAX_FD  8192
struct pollfd  fds[MAX_FD];                   //存放事件的结构体数组
int cur_max_fd = 0;


int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;

    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = inet_addr("192.168.241.129");
    server_address.sin_port = htons(555);
    server_len = sizeof(server_address);
    bind(server_sockfd, (struct sockaddr*)&server_address, server_len);
    listen(server_sockfd, 5);

    fds[server_sockfd].fd = server_sockfd;                    //将服务器端套接字存放到结构体中
    fds[server_sockfd].events = POLLIN;                       //监听事件为读事件
    fds[server_sockfd].revents = 0;                           //标记位,事件发生时该标志位被改为1,表示事件以发生
    if(cur_max_fd <= server_sockfd)                           //cur_max_fd用于遍历事件fd,因此必须比任何一个server_sockfd或client_sockfd大
    {
        cur_max_fd = server_sockfd + 1;
    }


    while (1)
    {

        char ch;
        int i, fd;
        int nread;

        printf("等待连接\n");
        result = poll(fds, cur_max_fd, 2000);
        if (result < 0)
        {
            perror("server5");
            exit(1);

        }


        for (i = 0; i < cur_max_fd; i++)                        //遍历所有fd
        {

            if (fds[i].revents)                                 //找到读事件发生了的事件
            {

                fd = fds[i].fd;
                if (fd == server_sockfd)                        //如果是服务器端套接字读事件发生,则表示客户端有连接请求,则创建客户端套接字,并加入到监视事件中
                {
                    client_len = sizeof(client_address);
                    client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_address, &client_len);

                    fds[client_sockfd].fd = client_sockfd;
                    fds[client_sockfd].events = POLLIN;
                    fds[client_sockfd].revents = 0;


                    if(cur_max_fd <= client_sockfd)
                    {
                        cur_max_fd = client_sockfd + 1;
                    }

                    printf("adding client on fd %d\n", client_sockfd);
                }
                else                                            //不是服务器端事件,则是客户端事件发生,说明客户端向服务器端发送了数据,读出并输出
                {
                    nread = read(fd, &ch, 1);
                    if (nread == 0)
                    {
                        close(fd);
                        memset(&fds[i], 0, sizeof(struct pollfd));
                        printf("removing client on fd %d\n", fd);
                    }
                    else
                    {

                        printf("读到的字符是:%c\n",ch);
                    }

                }
            }
        }
    }

    return 0;
}

使用cmd作为客户端,操作方法与select一样,可参照上文。

5.EPOLL

1.函数解析

1.创建EPOLL 句柄
int epoll_create(int size); 

2.向EPOLL对象中添加、修改或者删除感兴趣的事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
    op取值:EPOLL_CTL_ADD        添加新的事件到epoll中
           EPOLL_CTL_MOD       修改EPOLL中的事件   
           EPOLL_CTL_DEL          删除epoll中的事件

     events取值:
                   EPOLLIN   表示有数据可以读出(接受连接、关闭连接)
           EPOLLOUT  表示连接可以写入数据发送(向服务器发起连接,连接成功事件)
           EPOLLERR  表示对应的连接发生错误
	 EPOLLHUP  表示对应的连接被挂起
3.收集在epoll监控的事件中已经发生的事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 

 epfd: epoll的描述符。

events:则是分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。

 maxevents: 本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。

timeout: 表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,立刻返回,不会等待。-1表示无限期阻塞

关键结构:
struct epoll_event{
	__uint32_t  events;
	epoll_data_t data;
}

typedef union epoll_data{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t

2.实例

客户端输入一个字母并发送给服务器,服务器接收并打印,再发送回给客户端,如果是小写就转换为大写再发回给客户端,客户端接收服务器发回的字母并输出。

1.server.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<assert.h>
#include<fcntl.h>
#include<unistd.h>                                                      //先明确一些逻辑,每一个套接字需要一个事件结构体用来监视读或写,每一个事件结构体需要一个ConnectStat结构体保存一些事件结构体不好保存的数据

typedef struct _ConnectStat  ConnectStat;


struct _ConnectStat {

        int fd;
        char data;
        char  age[64];
        struct epoll_event _ev;
        int  status;//0 -未登录   1 - 已登陆
        //response_handler handler;//不同页面的处理函数
};


ConnectStat * stat_init(int fd);
void connect_handle(int new_fd);
void do_http_respone(ConnectStat * stat);
void do_http_request(ConnectStat * stat);


static int epfd = 0;

void usage(const char* argv)
{
        printf("%s:[ip][port]\n", argv);
}


void set_nonblock(int fd)
{
        int fl = fcntl(fd, F_GETFL);
        fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}


int startup(char* _ip, int _port)  //创建一个套接字,绑定,检测服务器
{
        //sock
        //1.创建套接字
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock < 0)
        {
                perror("sock");
                exit(2);
        }

        int opt = 1;
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

        //2.填充本地 sockaddr_in 结构体(设置本地的IP地址和端口)
        struct sockaddr_in local;
        local.sin_port = htons(_port);
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = inet_addr(_ip);

        //3.bind()绑定
        if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
                perror("bind");
                exit(3);
        }
        //4.listen()监听 检测服务器
        if (listen(sock, 5) < 0)
        {
                perror("listen");
                exit(4);
        }
        //sleep(1000);
        return sock;    //这样的套接字返回
}


int main(int argc, char *argv[])
{
        if (argc != 3)     //检测参数个数是否正确
        {
                usage(argv[0]);
                exit(1);
        }

        int listen_sock = startup(argv[1], atoi(argv[2]));      //创建一个绑定了本地 ip 和端口号的套接字描述符


        //1.创建epoll
        epfd = epoll_create(256);    //可处理的最大句柄数256个
        if (epfd < 0)
        {
                perror("epoll_create");
                exit(5);
        }

        struct epoll_event _ev;       //epoll结构填充
        ConnectStat * stat = stat_init(listen_sock);
        _ev.events = EPOLLIN;         //初始关心事件为读
        _ev.data.ptr = stat;                            //每个事件都绑定一个ConnectStat,事件可能是ConnectStat里的,也可能是外面的

        //2.托管
        epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &_ev);  //将listen sock添加到epfd中,关心读事件,每个套接字都绑定一个事件,用于监视该套接字的行为

        struct epoll_event revs[64];                        //事件数组,事件发生时,与套接字绑定的事件将被赋值到该数组中

        int timeout = -1;                                  //timeout为负一,表示epoll无限期阻塞
        int num = 0;
        int done = 0;

        while (!done)
        {
                //epoll_wait()相当于在检测事件
                switch ((num = epoll_wait(epfd, revs, 64, timeout)))  //返回需要处理的事件数目  64表示 事件有多大
                {
                case 0:                  //返回0 ,表示监听超时
                        printf("timeout\n");
                        break;
                case -1:                 //出错
                        perror("epoll_wait");
                        break;
                default:                 //大于零 即就是返回了需要处理事件的数目
                {
                        struct sockaddr_in peer;
                        socklen_t len = sizeof(peer);

                        int i;
                        for (i = 0; i < num; i++)
                        {
                                ConnectStat * stat = (ConnectStat *)revs[i].data.ptr;

                                int rsock = stat->fd; //准确获取哪个事件的描述符
                                if (rsock == listen_sock) //如果是初始的 就接受,建立链接,将客户端套接字加入到监视集合中
                                {
                                        int new_fd = accept(listen_sock, (struct sockaddr*)&peer, &len);

                                        if (new_fd > 0)
                                        {

                                                printf("get a new client:%s:%d\n", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
                                                connect_handle(new_fd);
                                        }
                                }
                                else
                                {
                                        if (revs[i].events == EPOLLIN)
                                        {
                                                do_http_request((ConnectStat *)revs[i].data.ptr);
                                        }
                                        else if (revs[i].events == EPOLLOUT)
                                        {
                                                do_http_respone((ConnectStat *)revs[i].data.ptr);
                                        }
                                        else
                                        {
                                        }
                                }
                        }
                }
                break;
                }
        }
        return 0;
}


ConnectStat * stat_init(int fd) {                                     //将事件结构体与ConnectStat结构体联系起来,并将一些数据存入到ConnectStat中

        ConnectStat * temp = NULL;
        temp = (ConnectStat *)malloc(sizeof(ConnectStat));

        if (!temp) {
                fprintf(stderr, "malloc failed. reason: %m\n");
                return NULL;
        }

        memset(temp, '\0', sizeof(ConnectStat));
        temp->fd = fd;
        temp->status = 0;
        //temp->handler = welcome_response_handler;

        return temp;
}



void  connect_handle(int new_fd) {

        ConnectStat *stat = stat_init(new_fd);
        set_nonblock(new_fd);

        stat->_ev.events = EPOLLIN;
        stat->_ev.data.ptr = stat;

        epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &stat->_ev);    //二次托管,监视客户端套接字读端

}


void do_http_request(ConnectStat * stat){                      //客户端套接字可读,读出,修改对应事件结构体监视事件为写事件

     int fd=stat->fd;
     int count=read(fd,&stat->data,1);

     if(count<=0){

        printf("读取错误!\n");
        exit(3);
     }

     printf("读取到的字符是:%c\n",stat->data);

     if('a'<stat->data<'z'){                                    //将小写字母改为大写字母
        stat->data-=32;
     }


     stat->_ev.events = EPOLLOUT;

     epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&stat->_ev);
}


void do_http_respone(ConnectStat * stat){                       //客户端套接字可写,写入,修改对应事件结构体监视事件为读事件

     int fd=stat->fd;
     int count=write(fd,&stat->data,1);

     if(count<=0){

        printf("写入失败!\n");
        exit(4);
     }

     stat->_ev.events = EPOLLIN;

     epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&stat->_ev);
}

注:服务器端代码运行时要带上IP地址和端口号。如图:

在这里插入图片描述

2.client.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<assert.h>
#include<fcntl.h>
#include<unistd.h>

static int epfd = 0;

int main()
{

    int client_sockfd;
    int len;
    struct sockaddr_in address;//服务器端网络地址结构体
    int result;

    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("192.168.241.129");
    address.sin_port = htons(666);
    len = sizeof(address);

    result = connect(client_sockfd, (struct sockaddr*)&address, len);
    if (result == -1)
    {
        perror("oops: client2");
        exit(1);
    }

    epfd = epoll_create(256);    //可处理的最大句柄数256个
    if (epfd < 0)
    {
        perror("epoll_create");
        exit(5);
    }


    struct epoll_event _ev;
    _ev.events=EPOLLOUT;

    //2.托管
    epoll_ctl(epfd, EPOLL_CTL_ADD, client_sockfd, &_ev);  //将listen sock添加到epfd中,关心读事件,每个套接字都绑定一个事件,用于监视该套接字的行为

    struct epoll_event revs[64];                        //事件数组,事件发生时,与套接字绑定的事件将被赋值到该数组中
    int timeout = -1;                                  //timeout为负一,表示epoll无限期阻塞
    int num = 0;
    int done = 0;

    while (!done)
    {
                char data;
                //epoll_wait()相当于在检测事件
                switch ((num = epoll_wait(epfd, revs, 64, timeout)))  //返回需要处理的事件数目  64表示 事件有多大
                {
                        case 0:                  //返回0 ,表示监听超时
                              printf("timeout\n");
                              break;
                        case -1:                 //出错
                              perror("epoll_wait");
                              break;
                        default:                 //大于零 即就是返回了需要处理事件的数目
                        {

                                  if(revs[0].events==EPOLLOUT){                    //写事件准备好

                                     printf("请输入一个字符\n");
                                     scanf("%c",&data);

                                     getchar();                                 //读取回车

                                     int count=write(client_sockfd,&data,1);
                                     if(count<=0){
                                        printf("写入失败!\n");
                                        exit(1);
                                     }

                                     revs[0].events=EPOLLIN;                           //修改监视状态
                                     epoll_ctl(epfd, EPOLL_CTL_MOD, client_sockfd, &revs[0]);
                                  }
                                  else if(revs[0].events==EPOLLIN){                 //读事件准备好

                                         int count=read(client_sockfd,&data,1);
                                         if(count<=0){

                                            printf("读取失败!\n");
                                            exit(2);
                                         }

                                         printf("读取到的字符是:%c\n",data);

                                         revs[0].events=EPOLLOUT;                             //修改监视状态
                                         epoll_ctl(epfd,EPOLL_CTL_MOD,client_sockfd,&revs[0]);
                                  }


                        }

                }
    }

    return 0;
}

6.SELECT,POLL,EPOLL的区别

上文提到过,SELECT与POLL只在有事件发生时返回,并不返回具体的发生事件,需要我们一个个遍历找到。而EPOLL返回具体的事件结构体。这是EPOLL与SELECT,POLL的区别之一。

再一个,SELECT内部使用的是数组,因此它支持的文件描述符是有限制的,POLL使用的是链表,文件描述符没有限制。而EPOLL使用的是红黑树,文件描述符无限制而且还很高效。

7.总结

说是总结,其实只是概括一下我初学IO多路复用时一些一直不理解的点。结合对于网络编程的一些理解,便于理解IO多路复用。

首先,网络编程服务器端与客户端都会创建一个套接字,会返回一个文件描述符,而在Linux下操作文件就是要通过文件描述符的,因此创建套接字我们可以看成是创建了一个网络上的特殊文件,它可以完成客户端与服务器端的通信,数据交换。

看到这可能会产生一个疑问,既要完成两端的通信,又为什么要两个中间文件呢。按我的理解是:这两个套接字文件的作用是不一样的,有一个套接字主要是完成客户端与服务器端的连接。监视这个套接字,一旦有了读请求,就说明有客户端要连上服务器了,使用 accept() 函数就能获取这个套接字。而通过 accept() 获取的套接字是用于两端的通信的,也就是数据交换。

以上就是我的个人理解。

### IO多路复用的概念 IO多路复用是一种同步I/O模型,允许一个线程同时监控多个文件描述符(如套接字)。这种技术使得程序可以在等待多个输入源中的任何一个变为可用时不会被阻塞。当任意一个文件描述符准备好了读取或写入操作,则会立即通知应用程序去处理相应的工作[^1]。 ### 实现原理 在Linux系统下,可以通过`select`, `poll` 或者更高效的`epoll` 来实现这一功能。这些函数可以让进程一次性监视多个文件描述符,并告知哪些已经准备好进行通信活动。具体来说: - **Select**: 可以监听一定数量的文件描述符集合,在指定时间内检查它们是否有待处理的数据。 - **Poll**: 类似于`select`但是没有最大文件数目的限制,并且性能更好一些因为不需要每次调用都重新构建文件列表。 - **Epoll**: 是一种更为先进的接口,特别适合大量并发连接的情况。它可以注册感兴趣的事件并只报告那些确实发生了变化的对象,从而减少了不必要的上下文切换和资源消耗[^4]。 对于每一个可能发生变化的状态——比如可读、可写或是异常情况发生——都可以设置回调函数以便及时响应。这种方式不仅提高了效率而且简化了编程逻辑。 ### 应用场景 在网络服务端开发领域广泛应用着IO多路复用的技术,尤其是在高负载情况下需要保持大量的活跃TCP连接时表现尤为突出。例如Web服务器(Nginx), 缓存数据库(Redis) 都采用了类似的架构来优化其性能[^5]。 #### Nginx 和 Redis 的例子 这两个软件均采用Reactor模式下的IO多路复用来支持大规模并发请求: - 对于Nginx而言, 主要负责接收新到来的HTTP请求(`accept`)而具体的业务逻辑则由工作进程中完成. - 而像Redis这样的键值存储系统则是完全依赖单个工作循环就能高效地应对成千上万的同时在线客户端. ```python import select import socket server_socket = socket.socket() server_socket.bind(('localhost', 8080)) server_socket.listen(5) inputs = [server_socket] while True: readable, writable, exceptional = select.select(inputs,[],[]) for s in readable: if s is server_socket: client_socket, addr = s.accept() inputs.append(client_socket) else: data = s.recv(1024) if not data: inputs.remove(s) s.close() else: print(f"Received {data.decode()}") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值